From cad2fe1f39f7ce850692feb4a857477717325337 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 9 May 2025 13:56:27 +0200 Subject: [PATCH 001/213] Return with the correct copied length (#3804) --- sharedsock/sock_linux.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sharedsock/sock_linux.go b/sharedsock/sock_linux.go index 74ac6c163..1c22e7869 100644 --- a/sharedsock/sock_linux.go +++ b/sharedsock/sock_linux.go @@ -234,7 +234,7 @@ func (s *SharedSocket) read(receiver receiver) { } // ReadFrom reads packets received in the packetDemux channel -func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) { +func (s *SharedSocket) ReadFrom(b []byte) (int, net.Addr, error) { var pkt rcvdPacket select { case <-s.ctx.Done(): @@ -263,8 +263,7 @@ func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) { decodedLayers := make([]gopacket.LayerType, 0, 3) - err = parser.DecodeLayers(pkt.buf, &decodedLayers) - if err != nil { + if err := parser.DecodeLayers(pkt.buf, &decodedLayers); err != nil { return 0, nil, err } @@ -273,8 +272,8 @@ func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) { Port: int(udp.SrcPort), } - copy(b, payload) - return int(udp.Length), remoteAddr, nil + n := copy(b, payload) + return n, remoteAddr, nil } // WriteTo builds a UDP packet and writes it using the specific IP version writer From d5b52e86b6386e7dab2d370d9f49da7e96893e3b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 9 May 2025 14:01:21 +0200 Subject: [PATCH 002/213] [client] Ignore irrelevant route changes to tracked network monitor routes (#3796) --- .../networkmonitor/check_change_bsd.go | 2 +- .../networkmonitor/check_change_windows.go | 56 ++- .../check_change_windows_test.go | 404 ++++++++++++++++++ client/internal/networkmonitor/monitor.go | 5 +- .../routemanager/systemops/systemops.go | 15 + .../systemops/systemops_windows.go | 11 +- 6 files changed, 464 insertions(+), 29 deletions(-) create mode 100644 client/internal/networkmonitor/check_change_windows_test.go diff --git a/client/internal/networkmonitor/check_change_bsd.go b/client/internal/networkmonitor/check_change_bsd.go index bb327a877..f5eb2c739 100644 --- a/client/internal/networkmonitor/check_change_bsd.go +++ b/client/internal/networkmonitor/check_change_bsd.go @@ -19,7 +19,7 @@ import ( func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error { fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC) if err != nil { - return fmt.Errorf("failed to open routing socket: %v", err) + return fmt.Errorf("open routing socket: %v", err) } defer func() { err := unix.Close(fd) diff --git a/client/internal/networkmonitor/check_change_windows.go b/client/internal/networkmonitor/check_change_windows.go index 582865738..814584863 100644 --- a/client/internal/networkmonitor/check_change_windows.go +++ b/client/internal/networkmonitor/check_change_windows.go @@ -13,7 +13,7 @@ import ( func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error { routeMonitor, err := systemops.NewRouteMonitor(ctx) if err != nil { - return fmt.Errorf("failed to create route monitor: %w", err) + return fmt.Errorf("create route monitor: %w", err) } defer func() { if err := routeMonitor.Stop(); err != nil { @@ -38,35 +38,49 @@ func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) er } func routeChanged(route systemops.RouteUpdate, nexthopv4, nexthopv6 systemops.Nexthop) bool { - intf := "" - if route.Interface != nil { - intf = route.Interface.Name - if isSoftInterface(intf) { - log.Debugf("Network monitor: ignoring default route change for soft interface %s", intf) - return false - } + if intf := route.NextHop.Intf; intf != nil && isSoftInterface(intf.Name) { + log.Debugf("Network monitor: ignoring default route change for next hop with soft interface %s", route.NextHop) + return false + } + + // TODO: for the empty nexthop ip (on-link), determine the family differently + nexthop := nexthopv4 + if route.NextHop.IP.Is6() { + nexthop = nexthopv6 } switch route.Type { - case systemops.RouteModified: - // TODO: get routing table to figure out if our route is affected for modified routes - log.Infof("Network monitor: default route changed: via %s, interface %s", route.NextHop, intf) - return true - case systemops.RouteAdded: - if route.NextHop.Is4() && route.NextHop != nexthopv4.IP || route.NextHop.Is6() && route.NextHop != nexthopv6.IP { - log.Infof("Network monitor: default route added: via %s, interface %s", route.NextHop, intf) - return true - } + case systemops.RouteModified, systemops.RouteAdded: + return handleRouteAddedOrModified(route, nexthop) case systemops.RouteDeleted: - if nexthopv4.Intf != nil && route.NextHop == nexthopv4.IP || nexthopv6.Intf != nil && route.NextHop == nexthopv6.IP { - log.Infof("Network monitor: default route removed: via %s, interface %s", route.NextHop, intf) - return true - } + return handleRouteDeleted(route, nexthop) } return false } +func handleRouteAddedOrModified(route systemops.RouteUpdate, nexthop systemops.Nexthop) bool { + // For added/modified routes, we care about different next hops + if !nexthop.Equal(route.NextHop) { + action := "changed" + if route.Type == systemops.RouteAdded { + action = "added" + } + log.Infof("Network monitor: default route %s: via %s", action, route.NextHop) + return true + } + return false +} + +func handleRouteDeleted(route systemops.RouteUpdate, nexthop systemops.Nexthop) bool { + // For deleted routes, we care about our tracked next hop being deleted + if nexthop.Equal(route.NextHop) { + log.Infof("Network monitor: default route removed: via %s", route.NextHop) + return true + } + return false +} + func isSoftInterface(name string) bool { return strings.Contains(strings.ToLower(name), "isatap") || strings.Contains(strings.ToLower(name), "teredo") } diff --git a/client/internal/networkmonitor/check_change_windows_test.go b/client/internal/networkmonitor/check_change_windows_test.go new file mode 100644 index 000000000..29ff34dca --- /dev/null +++ b/client/internal/networkmonitor/check_change_windows_test.go @@ -0,0 +1,404 @@ +package networkmonitor + +import ( + "net" + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" +) + +func TestRouteChanged(t *testing.T) { + tests := []struct { + name string + route systemops.RouteUpdate + nexthopv4 systemops.Nexthop + nexthopv6 systemops.Nexthop + expected bool + }{ + { + name: "soft interface should be ignored", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Name: "ISATAP-Interface", // isSoftInterface checks name + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.2"), + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + }, + expected: false, + }, + { + name: "modified route with different v4 nexthop IP should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.2"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + }, + expected: true, + }, + { + name: "modified route with same v4 nexthop (IP and Intf Index) should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + }, + expected: false, + }, + { + name: "added route with different v6 nexthop IP should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteAdded, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::2"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + expected: true, + }, + { + name: "added route with same v6 nexthop (IP and Intf Index) should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteAdded, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + expected: false, + }, + { + name: "deleted route matching tracked v4 nexthop (IP and Intf Index) should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteDeleted, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + }, + expected: true, + }, + { + name: "deleted route not matching tracked v4 nexthop (different IP) should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteDeleted, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.3"), // Different IP + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{ + Index: 1, Name: "eth0", + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + }, + expected: false, + }, + { + name: "modified v4 route with same IP, different Intf Index should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: true, + }, + { + name: "modified v4 route with same IP, one Intf nil, other non-nil should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: nil, // Intf is nil + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, // Tracked Intf is not nil + }, + expected: true, + }, + { + name: "added v4 route with same IP, different Intf Index should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteAdded, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: true, + }, + { + name: "deleted v4 route with same IP, different Intf Index should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteDeleted, + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ // This is the route being deleted + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv4: systemops.Nexthop{ // This is our tracked nexthop + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index + }, + expected: false, // Because nexthopv4.Equal(route.NextHop) will be false + }, + { + name: "modified v6 route with different IP, same Intf Index should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::3"), // Different IP + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: true, + }, + { + name: "modified v6 route with same IP, different Intf Index should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: true, + }, + { + name: "modified v6 route with same IP, same Intf Index should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteModified, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: false, + }, + { + name: "deleted v6 route matching tracked nexthop (IP and Intf Index) should return true", + route: systemops.RouteUpdate{ + Type: systemops.RouteDeleted, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: true, + }, + { + name: "deleted v6 route not matching tracked nexthop (different IP) should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteDeleted, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::3"), // Different IP + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv6: systemops.Nexthop{ + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: false, + }, + { + name: "deleted v6 route not matching tracked nexthop (same IP, different Intf Index) should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteDeleted, + Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0), + NextHop: systemops.Nexthop{ // This is the route being deleted + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv6: systemops.Nexthop{ // This is our tracked nexthop + IP: netip.MustParseAddr("2001:db8::1"), + Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index + }, + expected: false, + }, + { + name: "unknown route type should return false", + route: systemops.RouteUpdate{ + Type: systemops.RouteUpdateType(99), // Unknown type + Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0), + NextHop: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.1"), + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + }, + nexthopv4: systemops.Nexthop{ + IP: netip.MustParseAddr("192.168.1.2"), // Different from route.NextHop + Intf: &net.Interface{Index: 1, Name: "eth0"}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := routeChanged(tt.route, tt.nexthopv4, tt.nexthopv6) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsSoftInterface(t *testing.T) { + tests := []struct { + name string + ifname string + expected bool + }{ + { + name: "ISATAP interface should be detected", + ifname: "ISATAP tunnel adapter", + expected: true, + }, + { + name: "lowercase soft interface should be detected", + ifname: "isatap.{14A5CF17-CA72-43EC-B4EA-B4B093641B7D}", + expected: true, + }, + { + name: "Teredo interface should be detected", + ifname: "Teredo Tunneling Pseudo-Interface", + expected: true, + }, + { + name: "regular interface should not be detected as soft", + ifname: "eth0", + expected: false, + }, + { + name: "another regular interface should not be detected as soft", + ifname: "wlan0", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isSoftInterface(tt.ifname) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/client/internal/networkmonitor/monitor.go b/client/internal/networkmonitor/monitor.go index 5896b66b6..accdd9c9d 100644 --- a/client/internal/networkmonitor/monitor.go +++ b/client/internal/networkmonitor/monitor.go @@ -118,9 +118,12 @@ func (nw *NetworkMonitor) Stop() { } func (nw *NetworkMonitor) checkChanges(ctx context.Context, event chan struct{}, nexthop4 systemops.Nexthop, nexthop6 systemops.Nexthop) { + defer close(event) for { if err := checkChangeFn(ctx, nexthop4, nexthop6); err != nil { - close(event) + if !errors.Is(err, context.Canceled) { + log.Errorf("Network monitor: failed to check for changes: %v", err) + } return } // prevent blocking diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index 5c117b94d..fd511fc20 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -1,6 +1,7 @@ package systemops import ( + "fmt" "net" "net/netip" "sync" @@ -15,6 +16,20 @@ type Nexthop struct { Intf *net.Interface } +// Equal checks if two nexthops are equal. +func (n Nexthop) Equal(other Nexthop) bool { + return n.IP == other.IP && (n.Intf == nil && other.Intf == nil || + n.Intf != nil && other.Intf != nil && n.Intf.Index == other.Intf.Index) +} + +// String returns a string representation of the nexthop. +func (n Nexthop) String() string { + if n.Intf == nil { + return n.IP.String() + } + return fmt.Sprintf("%s @ %d (%s)", n.IP.String(), n.Intf.Index, n.Intf.Name) +} + type ExclusionCounter = refcounter.Counter[netip.Prefix, struct{}, Nexthop] type SysOps struct { diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go index ad325e123..f66161595 100644 --- a/client/internal/routemanager/systemops/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -33,8 +33,7 @@ type RouteUpdateType int type RouteUpdate struct { Type RouteUpdateType Destination netip.Prefix - NextHop netip.Addr - Interface *net.Interface + NextHop Nexthop } // RouteMonitor provides a way to monitor changes in the routing table. @@ -231,15 +230,15 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI intf, err := net.InterfaceByIndex(idx) if err != nil { log.Warnf("failed to get interface name for index %d: %v", idx, err) - update.Interface = &net.Interface{ + update.NextHop.Intf = &net.Interface{ Index: idx, } } else { - update.Interface = intf + update.NextHop.Intf = intf } } - log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.Interface) + log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.NextHop.Intf) dest := parseIPPrefix(row.DestinationPrefix, idx) if !dest.Addr().IsValid() { return RouteUpdate{}, fmt.Errorf("invalid destination: %v", row) @@ -262,7 +261,7 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI update.Type = updateType update.Destination = dest - update.NextHop = nexthop + update.NextHop.IP = nexthop return update, nil } From 2f34e984b0051c5b0ad1992d88b93c62165bf765 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Fri, 9 May 2025 15:06:34 +0300 Subject: [PATCH 003/213] [client] Add TCP support to DNS forwarder service listener (#3790) [client] Add TCP support to DNS forwarder service listener --- client/internal/dnsfwd/forwarder.go | 104 ++++++++++++++++++++++------ client/internal/dnsfwd/manager.go | 14 ++++ 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/client/internal/dnsfwd/forwarder.go b/client/internal/dnsfwd/forwarder.go index 8f6a31f47..45b479632 100644 --- a/client/internal/dnsfwd/forwarder.go +++ b/client/internal/dnsfwd/forwarder.go @@ -33,6 +33,8 @@ type DNSForwarder struct { dnsServer *dns.Server mux *dns.ServeMux + tcpServer *dns.Server + tcpMux *dns.ServeMux mutex sync.RWMutex fwdEntries []*ForwarderEntry @@ -50,22 +52,41 @@ func NewDNSForwarder(listenAddress string, ttl uint32, firewall firewall.Manager } func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error { - log.Infof("listen DNS forwarder on address=%s", f.listenAddress) - mux := dns.NewServeMux() + log.Infof("starting DNS forwarder on address=%s", f.listenAddress) - dnsServer := &dns.Server{ + // UDP server + mux := dns.NewServeMux() + f.mux = mux + f.dnsServer = &dns.Server{ Addr: f.listenAddress, Net: "udp", Handler: mux, } - f.dnsServer = dnsServer - f.mux = mux + // TCP server + tcpMux := dns.NewServeMux() + f.tcpMux = tcpMux + f.tcpServer = &dns.Server{ + Addr: f.listenAddress, + Net: "tcp", + Handler: tcpMux, + } f.UpdateDomains(entries) - return dnsServer.ListenAndServe() -} + errCh := make(chan error, 2) + go func() { + log.Infof("DNS UDP listener running on %s", f.listenAddress) + errCh <- f.dnsServer.ListenAndServe() + }() + go func() { + log.Infof("DNS TCP listener running on %s", f.listenAddress) + errCh <- f.tcpServer.ListenAndServe() + }() + + // return the first error we get (e.g. bind failure or shutdown) + return <-errCh +} func (f *DNSForwarder) UpdateDomains(entries []*ForwarderEntry) { f.mutex.Lock() defer f.mutex.Unlock() @@ -77,31 +98,41 @@ func (f *DNSForwarder) UpdateDomains(entries []*ForwarderEntry) { } oldDomains := filterDomains(f.fwdEntries) - for _, d := range oldDomains { f.mux.HandleRemove(d.PunycodeString()) + f.tcpMux.HandleRemove(d.PunycodeString()) } newDomains := filterDomains(entries) for _, d := range newDomains { - f.mux.HandleFunc(d.PunycodeString(), f.handleDNSQuery) + f.mux.HandleFunc(d.PunycodeString(), f.handleDNSQueryUDP) + f.tcpMux.HandleFunc(d.PunycodeString(), f.handleDNSQueryTCP) } f.fwdEntries = entries - log.Debugf("Updated domains from %v to %v", oldDomains, newDomains) } func (f *DNSForwarder) Close(ctx context.Context) error { - if f.dnsServer == nil { - return nil + var result *multierror.Error + + if f.dnsServer != nil { + if err := f.dnsServer.ShutdownContext(ctx); err != nil { + result = multierror.Append(result, fmt.Errorf("UDP shutdown: %w", err)) + } } - return f.dnsServer.ShutdownContext(ctx) + if f.tcpServer != nil { + if err := f.tcpServer.ShutdownContext(ctx); err != nil { + result = multierror.Append(result, fmt.Errorf("TCP shutdown: %w", err)) + } + } + + return nberrors.FormatErrorOrNil(result) } -func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) { +func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) *dns.Msg { if len(query.Question) == 0 { - return + return nil } question := query.Question[0] log.Tracef("received DNS request for DNS forwarder: domain=%v type=%v class=%v", @@ -123,20 +154,53 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) { if err := w.WriteMsg(resp); err != nil { log.Errorf("failed to write DNS response: %v", err) } - return + return nil } ctx, cancel := context.WithTimeout(context.Background(), upstreamTimeout) defer cancel() ips, err := net.DefaultResolver.LookupNetIP(ctx, network, domain) if err != nil { - f.handleDNSError(w, resp, domain, err) - return + f.handleDNSError(w, query, resp, domain, err) + return nil } f.updateInternalState(domain, ips) f.addIPsToResponse(resp, domain, ips) + return resp +} + +func (f *DNSForwarder) handleDNSQueryUDP(w dns.ResponseWriter, query *dns.Msg) { + + resp := f.handleDNSQuery(w, query) + if resp == nil { + return + } + + opt := query.IsEdns0() + maxSize := dns.MinMsgSize + if opt != nil { + // client advertised a larger EDNS0 buffer + maxSize = int(opt.UDPSize()) + } + + // if our response is too big, truncate and set the TC bit + if resp.Len() > maxSize { + resp.Truncate(maxSize) + } + + if err := w.WriteMsg(resp); err != nil { + log.Errorf("failed to write DNS response: %v", err) + } +} + +func (f *DNSForwarder) handleDNSQueryTCP(w dns.ResponseWriter, query *dns.Msg) { + resp := f.handleDNSQuery(w, query) + if resp == nil { + return + } + if err := w.WriteMsg(resp); err != nil { log.Errorf("failed to write DNS response: %v", err) } @@ -179,7 +243,7 @@ func (f *DNSForwarder) updateFirewall(matchingEntries []*ForwarderEntry, prefixe } // handleDNSError processes DNS lookup errors and sends an appropriate error response -func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, resp *dns.Msg, domain string, err error) { +func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, query, resp *dns.Msg, domain string, err error) { var dnsErr *net.DNSError switch { @@ -191,7 +255,7 @@ func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, resp *dns.Msg, domai } if dnsErr.Server != "" { - log.Warnf("failed to resolve query for domain=%s server=%s: %v", domain, dnsErr.Server, err) + log.Warnf("failed to resolve query for type=%s domain=%s server=%s: %v", dns.TypeToString[query.Question[0].Qtype], domain, dnsErr.Server, err) } else { log.Warnf(errResolveFailed, domain, err) } diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index e4a23450f..91abce823 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -33,6 +33,7 @@ type Manager struct { statusRecorder *peer.Status fwRules []firewall.Rule + tcpRules []firewall.Rule dnsForwarder *DNSForwarder } @@ -107,6 +108,13 @@ func (m *Manager) allowDNSFirewall() error { } m.fwRules = dnsRules + tcpRules, err := m.firewall.AddPeerFiltering(nil, net.IP{0, 0, 0, 0}, firewall.ProtocolTCP, nil, dport, firewall.ActionAccept, "") + if err != nil { + log.Errorf("failed to add allow DNS router rules, err: %v", err) + return err + } + m.tcpRules = tcpRules + return nil } @@ -117,7 +125,13 @@ func (m *Manager) dropDNSFirewall() error { mErr = multierror.Append(mErr, fmt.Errorf("failed to delete DNS router rules, err: %v", err)) } } + for _, rule := range m.tcpRules { + if err := m.firewall.DeletePeerRule(rule); err != nil { + mErr = multierror.Append(mErr, fmt.Errorf("failed to delete DNS router rules, err: %v", err)) + } + } m.fwRules = nil + m.tcpRules = nil return nberrors.FormatErrorOrNil(mErr) } From 20f59ddecb524a622ca749732bbc0d586f5f4363 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 12 May 2025 19:48:47 +0200 Subject: [PATCH 004/213] [signal] adjust log levels (#3813) --- signal/peer/peer.go | 4 ++-- signal/server/signal.go | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/signal/peer/peer.go b/signal/peer/peer.go index ed2360d67..da160bee9 100644 --- a/signal/peer/peer.go +++ b/signal/peer/peer.go @@ -79,7 +79,7 @@ func (registry *Registry) Register(peer *Peer) { p, loaded := registry.Peers.LoadOrStore(peer.Id, peer) if loaded { pp := p.(*Peer) - log.Warnf("peer [%s] is already registered [new streamID %d, previous StreamID %d]. Will override stream.", + log.Debugf("peer [%s] is already registered [new streamID %d, previous StreamID %d]. Will override stream.", peer.Id, peer.StreamID, pp.StreamID) registry.Peers.Store(peer.Id, peer) return @@ -104,7 +104,7 @@ func (registry *Registry) Deregister(peer *Peer) { pp := p.(*Peer) if peer.StreamID < pp.StreamID { registry.Peers.Store(peer.Id, p) - log.Warnf("attempted to remove newer registered stream of a peer [%s] [newer streamID %d, previous StreamID %d]. Ignoring.", + log.Debugf("attempted to remove newer registered stream of a peer [%s] [newer streamID %d, previous StreamID %d]. Ignoring.", peer.Id, pp.StreamID, peer.StreamID) return } diff --git a/signal/server/signal.go b/signal/server/signal.go index 3cae7e860..746c4b0b1 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -6,7 +6,6 @@ import ( "io" "time" - "github.com/netbirdio/signal-dispatcher/dispatcher" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" @@ -15,6 +14,8 @@ import ( "google.golang.org/grpc/status" gproto "google.golang.org/protobuf/proto" + "github.com/netbirdio/signal-dispatcher/dispatcher" + "github.com/netbirdio/netbird/signal/metrics" "github.com/netbirdio/netbird/signal/peer" "github.com/netbirdio/netbird/signal/proto" @@ -69,7 +70,7 @@ func NewServer(ctx context.Context, meter metric.Meter) (*Server, error) { // Send forwards a message to the signal peer func (s *Server) Send(ctx context.Context, msg *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { - log.Debugf("received a new message to send from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) + log.Tracef("received a new message to send from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) if _, found := s.registry.Get(msg.RemoteKey); found { s.forwardMessageToPeer(ctx, msg) @@ -112,7 +113,7 @@ func (s *Server) ConnectStream(stream proto.SignalExchange_ConnectStreamServer) return err } - log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) + log.Tracef("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) _, err = s.dispatcher.SendMessage(stream.Context(), msg) if err != nil { @@ -149,7 +150,7 @@ func (s *Server) DeregisterPeer(p *peer.Peer) { } func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedMessage) { - log.Debugf("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) + log.Tracef("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) getRegistrationStart := time.Now() // lookup the target peer where the message is going to From efb0edfc4cb709db947cc29aadea7fc7d6e974e3 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 12 May 2025 23:52:29 +0200 Subject: [PATCH 005/213] [signal] adjust signal log levels 2 (#3817) --- signal/peer/peer.go | 2 +- signal/server/signal.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/signal/peer/peer.go b/signal/peer/peer.go index da160bee9..2252e877e 100644 --- a/signal/peer/peer.go +++ b/signal/peer/peer.go @@ -79,7 +79,7 @@ func (registry *Registry) Register(peer *Peer) { p, loaded := registry.Peers.LoadOrStore(peer.Id, peer) if loaded { pp := p.(*Peer) - log.Debugf("peer [%s] is already registered [new streamID %d, previous StreamID %d]. Will override stream.", + log.Tracef("peer [%s] is already registered [new streamID %d, previous StreamID %d]. Will override stream.", peer.Id, peer.StreamID, pp.StreamID) registry.Peers.Store(peer.Id, peer) return diff --git a/signal/server/signal.go b/signal/server/signal.go index 746c4b0b1..bb11ee347 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -169,7 +169,7 @@ func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedM // forward the message to the target peer if err := dstPeer.Stream.Send(msg); err != nil { - log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) + log.Tracef("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) // todo respond to the sender? s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError))) return From ee1cec47b370914f19e7eb5188f0abafccc326b9 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 13 May 2025 15:21:06 +0200 Subject: [PATCH 006/213] [client, android] Do not propagate empty routes (#3805) If we get domain routes the Network prefix variable in route structure will be invalid (engine.go:1057). When we handower to Android the routes, we must to filter out the domain routes. If we do not do it the Android code will get "invalid prefix" string as a route. --- client/internal/routemanager/notifier/notifier.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/internal/routemanager/notifier/notifier.go b/client/internal/routemanager/notifier/notifier.go index ebdd60323..25a3a71e0 100644 --- a/client/internal/routemanager/notifier/notifier.go +++ b/client/internal/routemanager/notifier/notifier.go @@ -32,6 +32,10 @@ func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { func (n *Notifier) SetInitialClientRoutes(clientRoutes []*route.Route) { nets := make([]string, 0) for _, r := range clientRoutes { + // filter out domain routes + if r.IsDynamic() { + continue + } nets = append(nets, r.Network.String()) } sort.Strings(nets) From 0cd4b601c3b23fd24cea96b9241aae0568e819fc Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 14 May 2025 11:15:50 +0300 Subject: [PATCH 007/213] [management] Add connection type filter to Network Traffic API (#3815) --- management/server/http/api/openapi.yml | 7 +++++++ management/server/http/api/types.gen.go | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index bf40777fc..5ddfb5332 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -4308,6 +4308,13 @@ paths: schema: type: string enum: [TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP] + - name: connection_type + in: query + description: Filter by connection type + required: false + schema: + type: string + enum: [P2P, ROUTED] - name: direction in: query description: Filter by direction diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index e108c6884..c2efb835a 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -186,6 +186,12 @@ const ( GetApiEventsNetworkTrafficParamsTypeTYPEUNKNOWN GetApiEventsNetworkTrafficParamsType = "TYPE_UNKNOWN" ) +// Defines values for GetApiEventsNetworkTrafficParamsConnectionType. +const ( + GetApiEventsNetworkTrafficParamsConnectionTypeP2P GetApiEventsNetworkTrafficParamsConnectionType = "P2P" + GetApiEventsNetworkTrafficParamsConnectionTypeROUTED GetApiEventsNetworkTrafficParamsConnectionType = "ROUTED" +) + // Defines values for GetApiEventsNetworkTrafficParamsDirection. const ( GetApiEventsNetworkTrafficParamsDirectionDIRECTIONUNKNOWN GetApiEventsNetworkTrafficParamsDirection = "DIRECTION_UNKNOWN" @@ -1784,6 +1790,9 @@ type GetApiEventsNetworkTrafficParams struct { // Type Filter by event type Type *GetApiEventsNetworkTrafficParamsType `form:"type,omitempty" json:"type,omitempty"` + // ConnectionType Filter by connection type + ConnectionType *GetApiEventsNetworkTrafficParamsConnectionType `form:"connection_type,omitempty" json:"connection_type,omitempty"` + // Direction Filter by direction Direction *GetApiEventsNetworkTrafficParamsDirection `form:"direction,omitempty" json:"direction,omitempty"` @@ -1800,6 +1809,9 @@ type GetApiEventsNetworkTrafficParams struct { // GetApiEventsNetworkTrafficParamsType defines parameters for GetApiEventsNetworkTraffic. type GetApiEventsNetworkTrafficParamsType string +// GetApiEventsNetworkTrafficParamsConnectionType defines parameters for GetApiEventsNetworkTraffic. +type GetApiEventsNetworkTrafficParamsConnectionType string + // GetApiEventsNetworkTrafficParamsDirection defines parameters for GetApiEventsNetworkTraffic. type GetApiEventsNetworkTrafficParamsDirection string From 21584611218eeb98dd86c4c43dd5c773517ac1bf Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Wed, 14 May 2025 17:48:51 +0200 Subject: [PATCH 008/213] [management,client] PKCE add flag parameter prompt=login or max_age (#3824) --- .../workflows/test-infrastructure-files.yml | 1 + client/internal/auth/pkce_flow.go | 7 +- client/internal/auth/pkce_flow_test.go | 40 +- client/internal/pkce_auth.go | 4 + infrastructure_files/base.setup.env | 2 + infrastructure_files/management.json.tmpl | 3 +- infrastructure_files/tests/setup.env | 1 + management/client/common/types.go | 19 + management/proto/management.pb.go | 354 +++++++++--------- management/proto/management.proto | 2 + management/server/grpcserver.go | 1 + management/server/types/config.go | 3 + 12 files changed, 254 insertions(+), 183 deletions(-) create mode 100644 management/client/common/types.go diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index 174b7d205..8c2d21c8f 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -179,6 +179,7 @@ jobs: grep -A 7 Relay management.json | grep "rel://$CI_NETBIRD_DOMAIN:33445" grep -A 7 Relay management.json | egrep '"Secret": ".+"' grep DisablePromptLogin management.json | grep 'true' + grep LoginFlag management.json | grep 0 - name: Install modules run: go mod tidy diff --git a/client/internal/auth/pkce_flow.go b/client/internal/auth/pkce_flow.go index c5bd84cd5..d955679ae 100644 --- a/client/internal/auth/pkce_flow.go +++ b/client/internal/auth/pkce_flow.go @@ -101,7 +101,12 @@ func (p *PKCEAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowIn oauth2.SetAuthURLParam("audience", p.providerConfig.Audience), } if !p.providerConfig.DisablePromptLogin { - params = append(params, oauth2.SetAuthURLParam("prompt", "login")) + if p.providerConfig.LoginFlag.IsPromptLogin() { + params = append(params, oauth2.SetAuthURLParam("prompt", "login")) + } + if p.providerConfig.LoginFlag.IsMaxAge0Login() { + params = append(params, oauth2.SetAuthURLParam("max_age", "0")) + } } authURL := p.oAuthConfig.AuthCodeURL(state, params...) diff --git a/client/internal/auth/pkce_flow_test.go b/client/internal/auth/pkce_flow_test.go index 4510ed338..f42cce51f 100644 --- a/client/internal/auth/pkce_flow_test.go +++ b/client/internal/auth/pkce_flow_test.go @@ -7,15 +7,36 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/client/internal" + mgm "github.com/netbirdio/netbird/management/client/common" ) func TestPromptLogin(t *testing.T) { + const ( + promptLogin = "prompt=login" + maxAge0 = "max_age=0" + ) + tt := []struct { - name string - prompt bool + name string + loginFlag mgm.LoginFlag + disablePromptLogin bool + expect string }{ - {"PromptLogin", true}, - {"NoPromptLogin", false}, + { + name: "Prompt login", + loginFlag: mgm.LoginFlagPrompt, + expect: promptLogin, + }, + { + name: "Max age 0 login", + loginFlag: mgm.LoginFlagMaxAge0, + expect: maxAge0, + }, + { + name: "Disable prompt login", + loginFlag: mgm.LoginFlagPrompt, + disablePromptLogin: true, + }, } for _, tc := range tt { @@ -28,7 +49,7 @@ func TestPromptLogin(t *testing.T) { AuthorizationEndpoint: "https://test-auth-endpoint.com/authorize", RedirectURLs: []string{"http://127.0.0.1:33992/"}, UseIDToken: true, - DisablePromptLogin: !tc.prompt, + LoginFlag: tc.loginFlag, } pkce, err := NewPKCEAuthorizationFlow(config) if err != nil { @@ -38,11 +59,12 @@ func TestPromptLogin(t *testing.T) { if err != nil { t.Fatalf("Failed to request auth info: %v", err) } - pattern := "prompt=login" - if tc.prompt { - require.Contains(t, authInfo.VerificationURIComplete, pattern) + + if !tc.disablePromptLogin { + require.Contains(t, authInfo.VerificationURIComplete, tc.expect) } else { - require.NotContains(t, authInfo.VerificationURIComplete, pattern) + require.Contains(t, authInfo.VerificationURIComplete, promptLogin) + require.NotContains(t, authInfo.VerificationURIComplete, maxAge0) } }) } diff --git a/client/internal/pkce_auth.go b/client/internal/pkce_auth.go index 34eb2df1c..f020d7a91 100644 --- a/client/internal/pkce_auth.go +++ b/client/internal/pkce_auth.go @@ -12,6 +12,7 @@ import ( "google.golang.org/grpc/status" mgm "github.com/netbirdio/netbird/management/client" + "github.com/netbirdio/netbird/management/client/common" ) // PKCEAuthorizationFlow represents PKCE Authorization Flow information @@ -41,6 +42,8 @@ type PKCEAuthProviderConfig struct { ClientCertPair *tls.Certificate // DisablePromptLogin makes the PKCE flow to not prompt the user for login DisablePromptLogin bool + // LoginFlag is used to configure the PKCE flow login behavior + LoginFlag common.LoginFlag } // GetPKCEAuthorizationFlowInfo initialize a PKCEAuthorizationFlow instance and return with it @@ -100,6 +103,7 @@ func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL UseIDToken: protoPKCEAuthorizationFlow.GetProviderConfig().GetUseIDToken(), ClientCertPair: clientCert, DisablePromptLogin: protoPKCEAuthorizationFlow.GetProviderConfig().GetDisablePromptLogin(), + LoginFlag: common.LoginFlag(protoPKCEAuthorizationFlow.GetProviderConfig().GetLoginFlag()), }, } diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index 4b1376921..ebc38a11f 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -59,6 +59,7 @@ NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken} NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"} NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false} NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN=${NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN:-false} +NETBIRD_AUTH_PKCE_LOGIN_FLAG=${NETBIRD_AUTH_PKCE_LOGIN_FLAG:-1} NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE # Dashboard @@ -122,6 +123,7 @@ export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT export NETBIRD_AUTH_PKCE_USE_ID_TOKEN export NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN +export NETBIRD_AUTH_PKCE_LOGIN_FLAG export NETBIRD_AUTH_PKCE_AUDIENCE export NETBIRD_DASH_AUTH_USE_AUDIENCE export NETBIRD_DASH_AUTH_AUDIENCE diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index aa1739c61..c0e57b4fd 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -95,7 +95,8 @@ "Scope": "$NETBIRD_AUTH_SUPPORTED_SCOPES", "RedirectURLs": [$NETBIRD_AUTH_PKCE_REDIRECT_URLS], "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN, - "DisablePromptLogin": $NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN + "DisablePromptLogin": $NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN, + "LoginFlag": $NETBIRD_AUTH_PKCE_LOGIN_FLAG } } } diff --git a/infrastructure_files/tests/setup.env b/infrastructure_files/tests/setup.env index 2945e1c43..dc1d6e014 100644 --- a/infrastructure_files/tests/setup.env +++ b/infrastructure_files/tests/setup.env @@ -28,3 +28,4 @@ NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=$CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH NETBIRD_TURN_EXTERNAL_IP=1.2.3.4 NETBIRD_RELAY_PORT=33445 NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN=true +NETBIRD_AUTH_PKCE_LOGIN_FLAG=0 diff --git a/management/client/common/types.go b/management/client/common/types.go new file mode 100644 index 000000000..699617574 --- /dev/null +++ b/management/client/common/types.go @@ -0,0 +1,19 @@ +package common + +// LoginFlag introduces additional login flags to the PKCE authorization request +type LoginFlag uint8 + +const ( + // LoginFlagPrompt adds prompt=login to the authorization request + LoginFlagPrompt LoginFlag = iota + // LoginFlagMaxAge0 adds max_age=0 to the authorization request + LoginFlagMaxAge0 +) + +func (l LoginFlag) IsPromptLogin() bool { + return l == LoginFlagPrompt +} + +func (l LoginFlag) IsMaxAge0Login() bool { + return l == LoginFlagMaxAge0 +} diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 9d7fdc682..ceec3f643 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.9 +// protoc v5.29.3 // source: management.proto package proto @@ -2194,6 +2194,8 @@ type ProviderConfig struct { RedirectURLs []string `protobuf:"bytes,10,rep,name=RedirectURLs,proto3" json:"RedirectURLs,omitempty"` // DisablePromptLogin makes the PKCE flow to not prompt the user for login DisablePromptLogin bool `protobuf:"varint,11,opt,name=DisablePromptLogin,proto3" json:"DisablePromptLogin,omitempty"` + // LoginFlags sets the PKCE flow login details + LoginFlag uint32 `protobuf:"varint,12,opt,name=LoginFlag,proto3" json:"LoginFlag,omitempty"` } func (x *ProviderConfig) Reset() { @@ -2305,6 +2307,13 @@ func (x *ProviderConfig) GetDisablePromptLogin() bool { return false } +func (x *ProviderConfig) GetLoginFlag() uint32 { + if x != nil { + return x.LoginFlag + } + return 0 +} + // Route represents a route.Route object type Route struct { state protoimpl.MessageState @@ -3596,7 +3605,7 @@ var file_management_proto_rawDesc = []byte{ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x9a, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, @@ -3622,180 +3631,181 @@ var file_management_proto_rawDesc = []byte{ 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, - 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, - 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, - 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, - 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, - 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, - 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, - 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, - 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, - 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, - 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, - 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, - 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, - 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, - 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, - 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, - 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, - 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, + 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, + 0x6c, 0x61, 0x67, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, + 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, + 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, + 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, + 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, + 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, + 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, + 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, + 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, + 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, + 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, - 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, - 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, - 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, - 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, - 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, - 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, - 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, - 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, - 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, - 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, - 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, - 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, - 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, - 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, - 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, - 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, - 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, - 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, - 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, - 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, - 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, - 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, - 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, - 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, + 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, + 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, + 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, + 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, + 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, + 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, + 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, + 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, + 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, + 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, + 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, + 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, + 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, + 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, + 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, + 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, + 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, - 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, - 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, + 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, + 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, + 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, - 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, - 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, - 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, - 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, + 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, + 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, + 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management/proto/management.proto b/management/proto/management.proto index f0dc16ce2..41a01fce4 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -374,6 +374,8 @@ message ProviderConfig { repeated string RedirectURLs = 10; // DisablePromptLogin makes the PKCE flow to not prompt the user for login bool DisablePromptLogin = 11; + // LoginFlags sets the PKCE flow login details + uint32 LoginFlag = 12; } // Route represents a route.Route object diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 43d35f643..178120002 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -847,6 +847,7 @@ func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.En RedirectURLs: s.config.PKCEAuthorizationFlow.ProviderConfig.RedirectURLs, UseIDToken: s.config.PKCEAuthorizationFlow.ProviderConfig.UseIDToken, DisablePromptLogin: s.config.PKCEAuthorizationFlow.ProviderConfig.DisablePromptLogin, + LoginFlag: uint32(s.config.PKCEAuthorizationFlow.ProviderConfig.LoginFlag), }, } diff --git a/management/server/types/config.go b/management/server/types/config.go index 7a16b20a1..13e0f62b5 100644 --- a/management/server/types/config.go +++ b/management/server/types/config.go @@ -3,6 +3,7 @@ package types import ( "net/netip" + "github.com/netbirdio/netbird/management/client/common" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/util" ) @@ -156,6 +157,8 @@ type ProviderConfig struct { RedirectURLs []string // DisablePromptLogin makes the PKCE flow to not prompt the user for login DisablePromptLogin bool + // LoginFlag is used to configure the PKCE flow login behavior + LoginFlag common.LoginFlag } // StoreConfig contains Store configuration From adf494e1ac45a642d6be5113b39d98b62843aa89 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Wed, 14 May 2025 17:50:21 +0200 Subject: [PATCH 009/213] [management] fix a bug with missed extra dns labels for a new peer (#3798) --- management/domain/validate.go | 23 +- management/domain/validate_test.go | 105 +++---- management/server/peer.go | 23 +- management/server/peer_test.go | 265 ++++++++++++++++-- management/server/testdata/extended-store.sql | 7 +- 5 files changed, 307 insertions(+), 116 deletions(-) diff --git a/management/domain/validate.go b/management/domain/validate.go index a42aebe6f..bf2af7116 100644 --- a/management/domain/validate.go +++ b/management/domain/validate.go @@ -8,6 +8,8 @@ import ( const maxDomains = 32 +var domainRegex = regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`) + // ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList. func ValidateDomains(domains []string) (List, error) { if len(domains) == 0 { @@ -17,8 +19,6 @@ func ValidateDomains(domains []string) (List, error) { return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains) } - domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`) - var domainList List for _, d := range domains { @@ -37,27 +37,20 @@ func ValidateDomains(domains []string) (List, error) { return domainList, nil } -// ValidateDomainsStrSlice checks if each domain in the list is valid -func ValidateDomainsStrSlice(domains []string) ([]string, error) { +// ValidateDomainsList checks if each domain in the list is valid +func ValidateDomainsList(domains []string) error { if len(domains) == 0 { - return nil, nil + return nil } if len(domains) > maxDomains { - return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains) + return fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains) } - domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`) - - var domainList []string - for _, d := range domains { d := strings.ToLower(d) - if !domainRegex.MatchString(d) { - return domainList, fmt.Errorf("invalid domain format: %s", d) + return fmt.Errorf("invalid domain format: %s", d) } - - domainList = append(domainList, d) } - return domainList, nil + return nil } diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go index c9c042d9d..30efcd9a9 100644 --- a/management/domain/validate_test.go +++ b/management/domain/validate_test.go @@ -97,110 +97,89 @@ func TestValidateDomains(t *testing.T) { } } -// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function. -func TestValidateDomainsStrSlice(t *testing.T) { - // Generate a slice of valid domains up to maxDomains +func TestValidateDomainsList(t *testing.T) { validDomains := make([]string, maxDomains) - for i := 0; i < maxDomains; i++ { + for i := range maxDomains { validDomains[i] = fmt.Sprintf("example%d.com", i) } tests := []struct { - name string - domains []string - expected []string - wantErr bool + name string + domains []string + wantErr bool }{ { - name: "Empty list", - domains: nil, - expected: nil, - wantErr: false, + name: "Empty list", + domains: nil, + wantErr: false, }, { - name: "Single valid ASCII domain", - domains: []string{"sub.ex-ample.com"}, - expected: []string{"sub.ex-ample.com"}, - wantErr: false, + name: "Single valid ASCII domain", + domains: []string{"sub.ex-ample.com"}, + wantErr: false, }, { - name: "Underscores in labels", - domains: []string{"_jabber._tcp.gmail.com"}, - expected: []string{"_jabber._tcp.gmail.com"}, - wantErr: false, + name: "Underscores in labels", + domains: []string{"_jabber._tcp.gmail.com"}, + wantErr: false, }, { // Unlike ValidateDomains (which converts to punycode), // ValidateDomainsStrSlice will fail on non-ASCII domain chars. - name: "Unicode domain fails (no punycode conversion)", - domains: []string{"münchen.de"}, - expected: nil, - wantErr: true, + name: "Unicode domain fails (no punycode conversion)", + domains: []string{"münchen.de"}, + wantErr: true, }, { - name: "Invalid domain format - leading dash", - domains: []string{"-example.com"}, - expected: nil, - wantErr: true, + name: "Invalid domain format - leading dash", + domains: []string{"-example.com"}, + wantErr: true, }, { - name: "Invalid domain format - trailing dash", - domains: []string{"example-.com"}, - expected: nil, - wantErr: true, + name: "Invalid domain format - trailing dash", + domains: []string{"example-.com"}, + wantErr: true, }, { - // The function stops on the first invalid domain and returns an error, - // so only the first domain is definitely valid, but the second is invalid. - name: "Multiple domains with a valid one, then invalid", - domains: []string{"google.com", "invalid_domain.com-"}, - expected: []string{"google.com"}, - wantErr: true, + name: "Multiple domains with a valid one, then invalid", + domains: []string{"google.com", "invalid_domain.com-"}, + wantErr: true, }, { - name: "Valid wildcard domain", - domains: []string{"*.example.com"}, - expected: []string{"*.example.com"}, - wantErr: false, + name: "Valid wildcard domain", + domains: []string{"*.example.com"}, + wantErr: false, }, { - name: "Wildcard with leading dot - invalid", - domains: []string{".*.example.com"}, - expected: nil, - wantErr: true, + name: "Wildcard with leading dot - invalid", + domains: []string{".*.example.com"}, + wantErr: true, }, { - name: "Invalid wildcard with multiple asterisks", - domains: []string{"a.*.example.com"}, - expected: nil, - wantErr: true, + name: "Invalid wildcard with multiple asterisks", + domains: []string{"a.*.example.com"}, + wantErr: true, }, { - name: "Exactly maxDomains items (valid)", - domains: validDomains, - expected: validDomains, - wantErr: false, + name: "Exactly maxDomains items (valid)", + domains: validDomains, + wantErr: false, }, { - name: "Exceeds maxDomains items", - domains: append(validDomains, "extra.com"), - expected: nil, - wantErr: true, + name: "Exceeds maxDomains items", + domains: append(validDomains, "extra.com"), + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ValidateDomainsStrSlice(tt.domains) - // Check if we got an error where expected + err := ValidateDomainsList(tt.domains) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } - - // Compare the returned domains to what we expect - assert.Equal(t, tt.expected, got) }) } } diff --git a/management/server/peer.go b/management/server/peer.go index 9ff80442e..8bc6cdb05 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -556,6 +556,10 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s return fmt.Errorf("failed to get free IP: %w", err) } + if err := domain.ValidateDomainsList(peer.ExtraDNSLabels); err != nil { + return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err) + } + registrationTime := time.Now().UTC() newPeer = &nbpeer.Peer{ ID: xid.New().String(), @@ -767,10 +771,11 @@ func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, lo // we couldn't find this peer by its public key which can mean that peer hasn't been registered yet. // Try registering it. newPeer := &nbpeer.Peer{ - Key: login.WireGuardPubKey, - Meta: login.Meta, - SSHKey: login.SSHKey, - Location: nbpeer.Location{ConnectionIP: login.ConnectionIP}, + Key: login.WireGuardPubKey, + Meta: login.Meta, + SSHKey: login.SSHKey, + Location: nbpeer.Location{ConnectionIP: login.ConnectionIP}, + ExtraDNSLabels: login.ExtraDNSLabels, } return am.AddPeer(ctx, login.SetupKey, login.UserID, newPeer) @@ -875,16 +880,6 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels") } - extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels) - if err != nil { - return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err) - } - - if !slices.Equal(peer.ExtraDNSLabels, extraLabels) { - peer.ExtraDNSLabels = extraLabels - shouldStorePeer = true - } - if shouldStorePeer { if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { return err diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 406c3e49e..9c1de0659 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -10,6 +10,7 @@ import ( "net/netip" "os" "runtime" + "strings" "testing" "time" @@ -1290,15 +1291,21 @@ func Test_RegisterPeerByUser(t *testing.T) { Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()}, SSHEnabled: false, LastLogin: util.ToPtr(time.Now()), + ExtraDNSLabels: []string{ + "extraLabel1", + "extraLabel2", + }, } addedPeer, _, _, err := am.AddPeer(context.Background(), "", existingUserID, newPeer) require.NoError(t, err) + assert.Equal(t, newPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels) peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, addedPeer.Key) require.NoError(t, err) assert.Equal(t, peer.AccountID, existingAccountID) assert.Equal(t, peer.UserID, existingUserID) + assert.Equal(t, newPeer.ExtraDNSLabels, peer.ExtraDNSLabels) account, err := s.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) @@ -1339,15 +1346,12 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - existingSetupKeyID := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" _, err = s.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) - newPeer := &nbpeer.Peer{ - ID: xid.New().String(), + newPeerTemplate := &nbpeer.Peer{ AccountID: existingAccountID, - Key: "newPeerKey", UserID: "", IP: net.IP{123, 123, 123, 123}, Meta: nbpeer.PeerSystemMeta{ @@ -1358,31 +1362,96 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { DNSLabel: "newPeer.test", Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()}, SSHEnabled: false, + ExtraDNSLabels: []string{ + "extraLabel1", + "extraLabel2", + }, } - addedPeer, _, _, err := am.AddPeer(context.Background(), existingSetupKeyID, "", newPeer) + testCases := []struct { + name string + existingSetupKeyID string + expectedGroupIDsInAccount []string + expectAddPeerError bool + expectedErrorMsgSubstring string + }{ + { + name: "Successful registration with setup key allowing extra DNS labels", + existingSetupKeyID: "A2C8E62B-38F5-4553-B31E-DD66C696CEBD", + expectAddPeerError: false, + expectedGroupIDsInAccount: []string{"cfefqs706sqkneg59g2g", "cfefqs706sqkneg59g4g"}, + }, + { + name: "Failed registration with setup key not allowing extra DNS labels", + existingSetupKeyID: "A2C8E62B-38F5-4553-B31E-DD66C696CEBB", + expectAddPeerError: true, + expectedErrorMsgSubstring: "setup key doesn't allow extra DNS labels", + }, + { + name: "Absent setup key", + existingSetupKeyID: "AAAAAAAA-38F5-4553-B31E-DD66C696CEBB", + expectAddPeerError: true, + expectedErrorMsgSubstring: "failed adding new peer: account not found", + }, + } - require.NoError(t, err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + currentPeer := &nbpeer.Peer{ + ID: xid.New().String(), + AccountID: newPeerTemplate.AccountID, + Key: "newPeerKey_" + xid.New().String(), + UserID: newPeerTemplate.UserID, + IP: newPeerTemplate.IP, + Meta: newPeerTemplate.Meta, + Name: newPeerTemplate.Name, + DNSLabel: newPeerTemplate.DNSLabel, + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()}, + SSHEnabled: newPeerTemplate.SSHEnabled, + ExtraDNSLabels: newPeerTemplate.ExtraDNSLabels, + } - peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, newPeer.Key) - require.NoError(t, err) - assert.Equal(t, peer.AccountID, existingAccountID) + addedPeer, _, _, err := am.AddPeer(context.Background(), tc.existingSetupKeyID, "", currentPeer) - account, err := s.GetAccount(context.Background(), existingAccountID) - require.NoError(t, err) - assert.Contains(t, account.Peers, addedPeer.ID) - assert.Contains(t, account.Groups["cfefqs706sqkneg59g2g"].Peers, addedPeer.ID) - assert.Contains(t, account.Groups["cfefqs706sqkneg59g4g"].Peers, addedPeer.ID) + if tc.expectAddPeerError { + require.Error(t, err, "Expected an error when adding peer with setup key: %s", tc.existingSetupKeyID) + assert.Contains(t, err.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch") + return + } - assert.Equal(t, uint64(1), account.Network.Serial) + require.NoError(t, err, "Expected no error when adding peer with setup key: %s", tc.existingSetupKeyID) + assert.NotNil(t, addedPeer, "addedPeer should not be nil on success") + assert.Equal(t, currentPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch") - lastUsed, err := time.Parse("2006-01-02T15:04:05Z", "0001-01-01T00:00:00Z") - assert.NoError(t, err) + peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, currentPeer.Key) + require.NoError(t, err, "Failed to get peer by pub key: %s", currentPeer.Key) + assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store") + assert.Equal(t, currentPeer.ExtraDNSLabels, peerFromStore.ExtraDNSLabels, "ExtraDNSLabels mismatch for peer from store") + assert.Equal(t, addedPeer.ID, peerFromStore.ID, "Peer ID mismatch between addedPeer and peerFromStore") - hashedKey := sha256.Sum256([]byte(existingSetupKeyID)) - encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:]) - assert.NotEqual(t, lastUsed, account.SetupKeys[encodedHashedKey].LastUsed) - assert.Equal(t, 1, account.SetupKeys[encodedHashedKey].UsedTimes) + account, err := s.GetAccount(context.Background(), existingAccountID) + require.NoError(t, err, "Failed to get account: %s", existingAccountID) + assert.Contains(t, account.Peers, addedPeer.ID, "Peer ID not found in account.Peers") + + for _, groupID := range tc.expectedGroupIDsInAccount { + require.NotNil(t, account.Groups[groupID], "Group %s not found in account", groupID) + assert.Contains(t, account.Groups[groupID].Peers, addedPeer.ID, "Peer ID %s not found in group %s", addedPeer.ID, groupID) + } + + assert.Equal(t, uint64(1), account.Network.Serial, "Network.Serial mismatch; this assumes specific initial state or increment logic.") + + hashedKey := sha256.Sum256([]byte(tc.existingSetupKeyID)) + encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:]) + + setupKeyData, ok := account.SetupKeys[encodedHashedKey] + require.True(t, ok, "Setup key data not found in account.SetupKeys for key ID %s (encoded: %s)", tc.existingSetupKeyID, encodedHashedKey) + + var zeroTime time.Time + assert.NotEqual(t, zeroTime, setupKeyData.LastUsed, "Setup key LastUsed time should have been updated and not be zero.") + + assert.Equal(t, 1, setupKeyData.UsedTimes, "Setup key UsedTimes should be 1 after first use.") + }) + } } @@ -1456,6 +1525,160 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) { assert.Equal(t, 0, account.SetupKeys[encodedHashedKey].UsedTimes) } +func Test_LoginPeer(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "testdata/extended-store.sql", t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + eventStore := &activity.InMemoryEventStore{} + + metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) + assert.NoError(t, err) + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + permissionsManager := permissions.NewManager(s) + + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + assert.NoError(t, err) + + existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + _, err = s.GetAccount(context.Background(), existingAccountID) + require.NoError(t, err, "Failed to get existing account, check testdata/extended-store.sql. Account ID: %s", existingAccountID) + + baseMeta := nbpeer.PeerSystemMeta{ + Hostname: "loginPeerHost", + GoOS: "linux", + } + + newPeerTemplate := &nbpeer.Peer{ + AccountID: existingAccountID, + UserID: "", + IP: net.IP{123, 123, 123, 123}, + Meta: nbpeer.PeerSystemMeta{ + Hostname: "newPeer", + GoOS: "linux", + }, + Name: "newPeerName", + DNSLabel: "newPeer.test", + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()}, + SSHEnabled: false, + ExtraDNSLabels: []string{ + "extraLabel1", + "extraLabel2", + }, + } + + testCases := []struct { + name string + setupKey string + wireGuardPubKey string + expectExtraDNSLabelsMismatch bool + extraDNSLabels []string + expectLoginError bool + expectedErrorMsgSubstring string + }{ + { + name: "Successful login with setup key", + setupKey: "A2C8E62B-38F5-4553-B31E-DD66C696CEBD", + expectLoginError: false, + }, + { + name: "Successful login with setup key with DNS labels mismatch", + setupKey: "A2C8E62B-38F5-4553-B31E-DD66C696CEBD", + expectExtraDNSLabelsMismatch: true, + extraDNSLabels: []string{"anotherLabel1", "anotherLabel2"}, + expectLoginError: false, + }, + { + name: "Failed login with setup key not allowing extra DNS labels", + setupKey: "A2C8E62B-38F5-4553-B31E-DD66C696CEBB", + expectExtraDNSLabelsMismatch: true, + extraDNSLabels: []string{"anotherLabel1", "anotherLabel2"}, + expectLoginError: true, + expectedErrorMsgSubstring: "setup key doesn't allow extra DNS labels", + }, + } + + for _, tc := range testCases { + currentWireGuardPubKey := "testPubKey_" + xid.New().String() + + t.Run(tc.name, func(t *testing.T) { + upperKey := strings.ToUpper(tc.setupKey) + hashedKey := sha256.Sum256([]byte(upperKey)) + encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:]) + sk, err := s.GetSetupKeyBySecret(context.Background(), store.LockingStrengthUpdate, encodedHashedKey) + require.NoError(t, err, "Failed to get setup key %s from storage", tc.setupKey) + + currentPeer := &nbpeer.Peer{ + ID: xid.New().String(), + AccountID: newPeerTemplate.AccountID, + Key: currentWireGuardPubKey, + UserID: newPeerTemplate.UserID, + IP: newPeerTemplate.IP, + Meta: newPeerTemplate.Meta, + Name: newPeerTemplate.Name, + DNSLabel: newPeerTemplate.DNSLabel, + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()}, + SSHEnabled: newPeerTemplate.SSHEnabled, + } + // add peer manually to bypass creation during login stage + if sk.AllowExtraDNSLabels { + currentPeer.ExtraDNSLabels = newPeerTemplate.ExtraDNSLabels + } + _, _, _, err = am.AddPeer(context.Background(), tc.setupKey, "", currentPeer) + require.NoError(t, err, "Expected no error when adding peer with setup key: %s", tc.setupKey) + + loginInput := types.PeerLogin{ + WireGuardPubKey: currentWireGuardPubKey, + SSHKey: "test-ssh-key", + Meta: baseMeta, + UserID: "", + SetupKey: tc.setupKey, + ConnectionIP: net.ParseIP("192.0.2.100"), + } + + if tc.expectExtraDNSLabelsMismatch { + loginInput.ExtraDNSLabels = tc.extraDNSLabels + } + + loggedinPeer, networkMap, postureChecks, loginErr := am.LoginPeer(context.Background(), loginInput) + if tc.expectLoginError { + require.Error(t, loginErr, "Expected an error during LoginPeer with setup key: %s", tc.setupKey) + assert.Contains(t, loginErr.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch") + assert.Nil(t, loggedinPeer, "LoggedinPeer should be nil on error") + assert.Nil(t, networkMap, "NetworkMap should be nil on error") + assert.Nil(t, postureChecks, "PostureChecks should be empty or nil on error") + return + } + + require.NoError(t, loginErr, "Expected no error during LoginPeer with setup key: %s", tc.setupKey) + assert.NotNil(t, loggedinPeer, "loggedinPeer should not be nil on success") + if tc.expectExtraDNSLabelsMismatch { + assert.NotEqual(t, tc.extraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels should not match on loggedinPeer") + assert.Equal(t, currentPeer.ExtraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch on loggedinPeer") + } else { + assert.Equal(t, currentPeer.ExtraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch on loggedinPeer") + } + assert.NotNil(t, networkMap, "networkMap should not be nil on success") + + assert.Equal(t, existingAccountID, loggedinPeer.AccountID, "AccountID mismatch for logged peer") + + peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, loginInput.WireGuardPubKey) + require.NoError(t, err, "Failed to get peer by pub key: %s", loginInput.WireGuardPubKey) + assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store") + assert.Equal(t, loggedinPeer.ID, peerFromStore.ID, "Peer ID mismatch between loggedinPeer and peerFromStore") + }) + } +} + func TestPeerAccountPeersUpdate(t *testing.T) { manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql index 7900dabf5..324bf6293 100644 --- a/management/server/testdata/extended-store.sql +++ b/management/server/testdata/extended-store.sql @@ -1,5 +1,5 @@ CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`)); -CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); +CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,`allow_extra_dns_labels` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)); @@ -26,8 +26,9 @@ CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`accoun CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`); INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:01:38.210000+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); -INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0); -INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0); +INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0,0); +INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBD','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBD','Default key with extra DNS labels','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0,1); +INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0,0); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,''); INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,''); INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00'); From 92c91bbdd82e6cc7365aa7b37ed4b6b8d1387907 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Wed, 14 May 2025 19:52:02 +0300 Subject: [PATCH 010/213] [client] Add FreeBSD desktop client support to OAuth flow (#3822) [client] Add FreeBSD desktop client support to OAuth flow --- client/cmd/login.go | 20 +- client/cmd/up.go | 22 +- client/internal/auth/oauth.go | 9 +- client/proto/daemon.pb.go | 1128 ++++++++++++++++----------------- client/proto/daemon.proto | 2 +- client/server/server.go | 2 +- client/ui/client_ui.go | 16 +- 7 files changed, 599 insertions(+), 600 deletions(-) diff --git a/client/cmd/login.go b/client/cmd/login.go index 84906a7a4..14abcd034 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "runtime" "strings" "time" @@ -98,11 +99,11 @@ var loginCmd = &cobra.Command{ } loginRequest := proto.LoginRequest{ - SetupKey: providedSetupKey, - ManagementUrl: managementURL, - IsLinuxDesktopClient: isLinuxRunningDesktop(), - Hostname: hostName, - DnsLabels: dnsLabelsReq, + SetupKey: providedSetupKey, + ManagementUrl: managementURL, + IsUnixDesktopClient: isUnixRunningDesktop(), + Hostname: hostName, + DnsLabels: dnsLabelsReq, } if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { @@ -195,7 +196,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C } func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) { - oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isLinuxRunningDesktop()) + oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop()) if err != nil { return nil, err } @@ -243,7 +244,10 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro } } -// isLinuxRunningDesktop checks if a Linux OS is running desktop environment -func isLinuxRunningDesktop() bool { +// isUnixRunningDesktop checks if a Linux OS is running desktop environment +func isUnixRunningDesktop() bool { + if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" { + return false + } return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != "" } diff --git a/client/cmd/up.go b/client/cmd/up.go index bfe41628e..8cc272834 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -262,17 +262,17 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { } loginRequest := proto.LoginRequest{ - SetupKey: providedSetupKey, - ManagementUrl: managementURL, - AdminURL: adminURL, - NatExternalIPs: natExternalIPs, - CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0, - CustomDNSAddress: customDNSAddressConverted, - IsLinuxDesktopClient: isLinuxRunningDesktop(), - Hostname: hostName, - ExtraIFaceBlacklist: extraIFaceBlackList, - DnsLabels: dnsLabels, - CleanDNSLabels: dnsLabels != nil && len(dnsLabels) == 0, + SetupKey: providedSetupKey, + ManagementUrl: managementURL, + AdminURL: adminURL, + NatExternalIPs: natExternalIPs, + CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0, + CustomDNSAddress: customDNSAddressConverted, + IsUnixDesktopClient: isUnixRunningDesktop(), + Hostname: hostName, + ExtraIFaceBlacklist: extraIFaceBlackList, + DnsLabels: dnsLabels, + CleanDNSLabels: dnsLabels != nil && len(dnsLabels) == 0, } if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { diff --git a/client/internal/auth/oauth.go b/client/internal/auth/oauth.go index 001609f26..86df58fdb 100644 --- a/client/internal/auth/oauth.go +++ b/client/internal/auth/oauth.go @@ -64,13 +64,8 @@ func (t TokenInfo) GetTokenToUse() string { // and if that also fails, the authentication process is deemed unsuccessful // // On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow -func NewOAuthFlow(ctx context.Context, config *internal.Config, isLinuxDesktopClient bool) (OAuthFlow, error) { - if runtime.GOOS == "linux" && !isLinuxDesktopClient { - return authenticateWithDeviceCodeFlow(ctx, config) - } - - // On FreeBSD we currently do not support desktop environments and offer only Device Code Flow (#2384) - if runtime.GOOS == "freebsd" { +func NewOAuthFlow(ctx context.Context, config *internal.Config, isUnixDesktopClient bool) (OAuthFlow, error) { + if (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient { return authenticateWithDeviceCodeFlow(ctx, config) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 879fb8032..a4e8b3e9f 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -255,7 +255,7 @@ type LoginRequest struct { // omits initialized empty slices due to omitempty tags CleanNATExternalIPs bool `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"` CustomDNSAddress []byte `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"` - IsLinuxDesktopClient bool `protobuf:"varint,8,opt,name=isLinuxDesktopClient,proto3" json:"isLinuxDesktopClient,omitempty"` + IsUnixDesktopClient bool `protobuf:"varint,8,opt,name=isUnixDesktopClient,proto3" json:"isUnixDesktopClient,omitempty"` Hostname string `protobuf:"bytes,9,opt,name=hostname,proto3" json:"hostname,omitempty"` RosenpassEnabled *bool `protobuf:"varint,10,opt,name=rosenpassEnabled,proto3,oneof" json:"rosenpassEnabled,omitempty"` InterfaceName *string `protobuf:"bytes,11,opt,name=interfaceName,proto3,oneof" json:"interfaceName,omitempty"` @@ -362,9 +362,9 @@ func (x *LoginRequest) GetCustomDNSAddress() []byte { return nil } -func (x *LoginRequest) GetIsLinuxDesktopClient() bool { +func (x *LoginRequest) GetIsUnixDesktopClient() bool { if x != nil { - return x.IsLinuxDesktopClient + return x.IsUnixDesktopClient } return false } @@ -3609,7 +3609,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0e, 0x0a, 0x0c, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb0, 0x0c, 0x0a, 0x0c, 0x4c, 0x6f, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xae, 0x0c, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, @@ -3627,572 +3627,572 @@ var file_daemon_proto_rawDesc = []byte{ 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, - 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x44, 0x65, 0x73, - 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, - 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x00, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, - 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88, - 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, - 0x6f, 0x72, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x0d, 0x77, 0x69, 0x72, - 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, - 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, - 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x14, 0x6f, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x78, + 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, + 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, + 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, + 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, + 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0d, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x29, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, + 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, - 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x0e, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x04, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, - 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, - 0x0f, 0x20, 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, - 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x13, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, - 0x69, 0x76, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x13, 0x72, 0x6f, 0x73, - 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, - 0x65, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, 0x6c, 0x61, 0x63, - 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x48, 0x07, 0x52, - 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x88, - 0x01, 0x01, 0x12, 0x4a, 0x0a, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x08, 0x52, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x37, - 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x48, 0x09, 0x52, - 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, - 0x12, 0x24, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18, - 0x16, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x44, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x0c, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, - 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x0d, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19, - 0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, - 0x1d, 0x0a, 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26, - 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, - 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, - 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, - 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, - 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, - 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, - 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, - 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, - 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, - 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, - 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, - 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, - 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, - 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, - 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, - 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, - 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03, - 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, - 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, - 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, - 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, - 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, - 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, - 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, - 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, - 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, - 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, - 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, - 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, - 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, - 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, - 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, - 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, - 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, + 0x4b, 0x65, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x14, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, + 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, + 0x48, 0x04, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0f, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, + 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x13, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, + 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x88, 0x01, + 0x01, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, + 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, + 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, + 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, + 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x48, 0x07, 0x52, 0x0e, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, + 0x12, 0x4a, 0x0a, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x76, 0x61, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x08, 0x52, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x48, 0x09, 0x52, 0x13, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x15, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x24, + 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18, 0x16, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6e, + 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0c, + 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, + 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0d, + 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, + 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, + 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26, 0x0a, 0x0e, + 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1b, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, + 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, + 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, + 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, + 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, + 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, + 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, + 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, + 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, + 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, + 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, + 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, + 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03, 0x0a, 0x11, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, + 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, + 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, + 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, + 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, + 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, + 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, + 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, + 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, + 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, + 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, + 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, + 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, 0x05, 0x0a, + 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, + 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, + 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, + 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, + 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, - 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, - 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, + 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, - 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, - 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, - 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, - 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, - 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, - 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, - 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, - 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, - 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, - 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, - 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, - 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, - 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xb9, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c, - 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, - 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, - 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17, - 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, - 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, - 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, - 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, - 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, - 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, - 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, - 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, - 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, - 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, - 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, - 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x2e, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, - 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x38, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, - 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, - 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, - 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, - 0x65, 0x73, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, - 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, - 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, - 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, - 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, - 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, - 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, - 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, + 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, + 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, + 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, + 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, + 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, + 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, + 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, + 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, + 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, + 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, + 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, + 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, + 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, + 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, + 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xb9, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, + 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, + 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, + 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, + 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, + 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, + 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, + 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, + 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, + 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, + 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, + 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, + 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, + 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, + 0x22, 0x88, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, + 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, + 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, + 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, + 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, 0x44, + 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, + 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, - 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, - 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, - 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, - 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, - 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, - 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, - 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, - 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, - 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, - 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, - 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, - 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, - 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, - 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, - 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, - 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, - 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, - 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, - 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, - 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, - 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, - 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, - 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, - 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, - 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, - 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, - 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, - 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, - 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, - 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, - 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, - 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, - 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, - 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, - 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, - 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, - 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, - 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, - 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, - 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, - 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, - 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, - 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, - 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, - 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, - 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, - 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, - 0xb3, 0x0b, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, - 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, - 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, - 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, - 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, - 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, + 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, + 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, - 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, - 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, + 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, + 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, + 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, + 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, + 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, + 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, + 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, + 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, + 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, + 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, + 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, + 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, + 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, + 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, + 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, + 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, + 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, + 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, + 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, + 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, + 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, + 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, + 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, + 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, + 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, + 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, + 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, + 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, + 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, + 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, + 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, + 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, + 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, + 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, + 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, + 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, + 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, + 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, + 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, + 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, + 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, + 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, + 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, + 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, + 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, + 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, + 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, + 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, + 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, + 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, + 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, + 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 6c63a8f9b..282c307d6 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -94,7 +94,7 @@ message LoginRequest { bytes customDNSAddress = 7; - bool isLinuxDesktopClient = 8; + bool isUnixDesktopClient = 8; string hostname = 9; diff --git a/client/server/server.go b/client/server/server.go index cba09a8b9..5bc008fec 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -446,7 +446,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro state.Set(internal.StatusConnecting) if msg.SetupKey == "" { - oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsLinuxDesktopClient) + oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsUnixDesktopClient) if err != nil { state.Set(internal.StatusLoginFailed) return nil, err diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 2c8023185..03458bc4b 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -382,12 +382,12 @@ func (s *serviceClient) getSettingsForm() *widget.Form { s.adminURL = iAdminURL loginRequest := proto.LoginRequest{ - ManagementUrl: iMngURL, - AdminURL: iAdminURL, - IsLinuxDesktopClient: runtime.GOOS == "linux", - RosenpassPermissive: &s.sRosenpassPermissive.Checked, - InterfaceName: &s.iInterfaceName.Text, - WireguardPort: &port, + ManagementUrl: iMngURL, + AdminURL: iAdminURL, + IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", + RosenpassPermissive: &s.sRosenpassPermissive.Checked, + InterfaceName: &s.iInterfaceName.Text, + WireguardPort: &port, } if s.iPreSharedKey.Text != censoredPreSharedKey { @@ -414,7 +414,7 @@ func (s *serviceClient) login() error { } loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{ - IsLinuxDesktopClient: runtime.GOOS == "linux", + IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", }) if err != nil { log.Errorf("login to management URL with: %v", err) @@ -1020,7 +1020,7 @@ func (s *serviceClient) updateConfig() error { notificationsDisabled := !s.mNotifications.Checked() loginRequest := proto.LoginRequest{ - IsLinuxDesktopClient: runtime.GOOS == "linux", + IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, From e520b64c6df4c81e45d914d9383acf0ca1468840 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 14 May 2025 19:28:51 +0200 Subject: [PATCH 011/213] [signal] remove stream receive server side (#3820) --- go.mod | 2 +- go.sum | 4 ++-- signal/server/signal.go | 42 ++++++++++++++--------------------------- 3 files changed, 17 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index 2b3ef9cd6..0f779e35d 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203 - github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d + github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index a90db83de..226ee94c2 100644 --- a/go.sum +++ b/go.sum @@ -507,8 +507,8 @@ github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713- github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203/go.mod h1:2ZE6/tBBCKHQggPfO2UOQjyjXI7k+JDVl2ymorTOVQs= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= -github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28= -github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= +github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= +github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ= github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= diff --git a/signal/server/signal.go b/signal/server/signal.go index bb11ee347..9dfba7798 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -3,7 +3,6 @@ package server import ( "context" "fmt" - "io" "time" log "github.com/sirupsen/logrus" @@ -29,10 +28,11 @@ const ( labelTypeStream = "stream" labelTypeMessage = "message" - labelError = "error" - labelErrorMissingId = "missing_id" - labelErrorMissingMeta = "missing_meta" - labelErrorFailedHeader = "failed_header" + labelError = "error" + labelErrorMissingId = "missing_id" + labelErrorMissingMeta = "missing_meta" + labelErrorFailedHeader = "failed_header" + labelErrorFailedRegistration = "failed_registration" labelRegistrationStatus = "status" labelRegistrationFound = "found" @@ -99,28 +99,9 @@ func (s *Server) ConnectStream(stream proto.SignalExchange_ConnectStreamServer) log.Debugf("peer connected [%s] [streamID %d] ", p.Id, p.StreamID) - for { - select { - case <-stream.Context().Done(): - log.Debugf("stream closed for peer [%s] [streamID %d] due to context cancellation", p.Id, p.StreamID) - return stream.Context().Err() - default: - // read incoming messages - msg, err := stream.Recv() - if err == io.EOF { - break - } else if err != nil { - return err - } - - log.Tracef("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) - - _, err = s.dispatcher.SendMessage(stream.Context(), msg) - if err != nil { - log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) - } - } - } + <-stream.Context().Done() + log.Debugf("peer stream closing [%s] [streamID %d] ", p.Id, p.StreamID) + return nil } func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) (*peer.Peer, error) { @@ -139,7 +120,12 @@ func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) ( p := peer.NewPeer(id[0], stream) s.registry.Register(p) - s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer) + err := s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer) + if err != nil { + s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorFailedRegistration))) + log.Errorf("error while registering message listener for peer [%s] %v", p.Id, err) + return nil, status.Errorf(codes.Internal, "error while registering message listener") + } return p, nil } From 43ae79d848fd9898c3e78446c95138ef8f0e528f Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 15 May 2025 18:20:29 +0200 Subject: [PATCH 012/213] [management] extend rest client lib (#3830) --- management/client/rest/accounts.go | 6 ++--- management/client/rest/client.go | 30 ++++++++++++++------- management/client/rest/dns.go | 14 +++++----- management/client/rest/events.go | 2 +- management/client/rest/geo.go | 4 +-- management/client/rest/groups.go | 10 +++---- management/client/rest/networks.go | 30 ++++++++++----------- management/client/rest/options.go | 35 +++++++++++++++++++++++++ management/client/rest/peers.go | 10 +++---- management/client/rest/policies.go | 14 ++++++---- management/client/rest/posturechecks.go | 10 +++---- management/client/rest/routes.go | 10 +++---- management/client/rest/setupkeys.go | 12 +++++---- management/client/rest/tokens.go | 8 +++--- management/client/rest/users.go | 12 ++++----- 15 files changed, 129 insertions(+), 78 deletions(-) create mode 100644 management/client/rest/options.go diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go index 29d4ac79d..2530e4f72 100644 --- a/management/client/rest/accounts.go +++ b/management/client/rest/accounts.go @@ -16,7 +16,7 @@ type AccountsAPI struct { // List list all accounts, only returns one account always // See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/accounts", nil) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api. if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api. // Delete delete account // See more: https://docs.netbird.io/api/resources/accounts#delete-an-account func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil) if err != nil { return err } diff --git a/management/client/rest/client.go b/management/client/rest/client.go index 0785d88af..886a59f2c 100644 --- a/management/client/rest/client.go +++ b/management/client/rest/client.go @@ -14,6 +14,7 @@ import ( type Client struct { managementURL string authHeader string + httpClient HttpClient // Accounts NetBird account APIs // see more: https://docs.netbird.io/api/resources/accounts @@ -70,20 +71,29 @@ type Client struct { // New initialize new Client instance using PAT token func New(managementURL, token string) *Client { - client := &Client{ - managementURL: managementURL, - authHeader: "Token " + token, - } - client.initialize() - return client + return NewWithOptions( + WithManagementURL(managementURL), + WithPAT(token), + ) } // NewWithBearerToken initialize new Client instance using Bearer token type func NewWithBearerToken(managementURL, token string) *Client { + return NewWithOptions( + WithManagementURL(managementURL), + WithBearerToken(token), + ) +} + +func NewWithOptions(opts ...option) *Client { client := &Client{ - managementURL: managementURL, - authHeader: "Bearer " + token, + httpClient: http.DefaultClient, } + + for _, option := range opts { + option(client) + } + client.initialize() return client } @@ -104,7 +114,7 @@ func (c *Client) initialize() { c.Events = &EventsAPI{c} } -func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { +func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body) if err != nil { return nil, err @@ -116,7 +126,7 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body io.Re req.Header.Add("Content-Type", "application/json") } - resp, err := http.DefaultClient.Do(req) + resp, err := c.httpClient.Do(req) if err != nil { return nil, err } diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go index 0e2d15842..1e35c0226 100644 --- a/management/client/rest/dns.go +++ b/management/client/rest/dns.go @@ -16,7 +16,7 @@ type DNSAPI struct { // ListNameserverGroups list all nameserver groups // See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGrou // GetNameserverGroup get nameserver group info // See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiD if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st // DeleteNameserverGroup delete nameserver group // See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil) if err != nil { return err } @@ -94,7 +94,7 @@ func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID st // GetSettings get DNS settings // See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/dns/settings", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/settings", nil) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettin if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes)) if err != nil { return nil, err } diff --git a/management/client/rest/events.go b/management/client/rest/events.go index ed74fae39..cae813e86 100644 --- a/management/client/rest/events.go +++ b/management/client/rest/events.go @@ -14,7 +14,7 @@ type EventsAPI struct { // List list all events // See more: https://docs.netbird.io/api/resources/events#list-all-events func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/events", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/events", nil) if err != nil { return nil, err } diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go index 0bdcc0a22..d06d65d80 100644 --- a/management/client/rest/geo.go +++ b/management/client/rest/geo.go @@ -14,7 +14,7 @@ type GeoLocationAPI struct { // ListCountries list all country codes // See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries", nil) if err != nil { return nil, err } @@ -28,7 +28,7 @@ func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, erro // ListCountryCities Get a list of all English city names for a given country code // See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil) if err != nil { return nil, err } diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go index aac453b93..7612b7188 100644 --- a/management/client/rest/groups.go +++ b/management/client/rest/groups.go @@ -16,7 +16,7 @@ type GroupsAPI struct { // List list all groups // See more: https://docs.netbird.io/api/resources/groups#list-all-groups func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/groups", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/groups", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { // Get get group info // See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/groups/"+groupID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/groups/"+groupID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONReq if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA // Delete delete group // See more: https://docs.netbird.io/api/resources/groups#delete-a-group func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/groups/"+groupID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/groups/"+groupID, nil) if err != nil { return err } diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go index b211312c9..b744e3fe7 100644 --- a/management/client/rest/networks.go +++ b/management/client/rest/networks.go @@ -16,7 +16,7 @@ type NetworksAPI struct { // List list all networks // See more: https://docs.netbird.io/api/resources/networks#list-all-networks func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/networks", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { // Get get network info // See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+networkID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+networkID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSO if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api. if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api. // Delete delete network // See more: https://docs.netbird.io/api/resources/networks#delete-a-network func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+networkID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+networkID, nil) if err != nil { return err } @@ -108,7 +108,7 @@ func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI { // List list all resources in networks // See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil) if err != nil { return nil, err } @@ -122,7 +122,7 @@ func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, // Get get network resource info // See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) if err != nil { return nil, err } @@ -140,7 +140,7 @@ func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNet if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -172,7 +172,7 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri // Delete delete network resource // See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) if err != nil { return err } @@ -200,7 +200,7 @@ func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI { // List list all routers in networks // See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil) if err != nil { return nil, err } @@ -214,7 +214,7 @@ func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, erro // Get get network router info // See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) if err != nil { return nil, err } @@ -232,7 +232,7 @@ func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetwo if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -250,7 +250,7 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -264,7 +264,7 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, // Delete delete network router // See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) if err != nil { return err } diff --git a/management/client/rest/options.go b/management/client/rest/options.go new file mode 100644 index 000000000..5aad7dd7e --- /dev/null +++ b/management/client/rest/options.go @@ -0,0 +1,35 @@ +package rest + +import "net/http" + +type option func(*Client) + +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +func WithHttpClient(client HttpClient) option { + return func(c *Client) { + c.httpClient = client + } +} + +func WithBearerToken(token string) option { + return WithAuthHeader("Bearer " + token) +} + +func WithPAT(token string) option { + return WithAuthHeader("Token " + token) +} + +func WithManagementURL(url string) option { + return func(c *Client) { + c.managementURL = url + } +} + +func WithAuthHeader(value string) option { + return func(c *Client) { + c.authHeader = value + } +} diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go index 2b1a65b4c..37679fdb9 100644 --- a/management/client/rest/peers.go +++ b/management/client/rest/peers.go @@ -16,7 +16,7 @@ type PeersAPI struct { // List list all peers // See more: https://docs.netbird.io/api/resources/peers#list-all-peers func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/peers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { // Get retrieve a peer // See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi // Delete delete a peer // See more: https://docs.netbird.io/api/resources/peers#delete-a-peer func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/peers/"+peerID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+peerID, nil) if err != nil { return err } @@ -76,7 +76,7 @@ func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { // ListAccessiblePeers list all peers that the specified peer can connect to within the network // See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil) if err != nil { return nil, err } diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go index 975a95440..2f2df4a78 100644 --- a/management/client/rest/policies.go +++ b/management/client/rest/policies.go @@ -16,7 +16,9 @@ type PoliciesAPI struct { // List list all policies // See more: https://docs.netbird.io/api/resources/policies#list-all-policies func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/policies", nil) + path := "/api/policies" + + resp, err := a.c.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, err } @@ -30,7 +32,7 @@ func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { // Get get policy info // See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/policies/"+policyID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/policies/"+policyID, nil) if err != nil { return nil, err } @@ -48,7 +50,7 @@ func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSO if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -62,11 +64,13 @@ func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSO // Update update policy info // See more: https://docs.netbird.io/api/resources/policies#update-a-policy func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.PutApiPoliciesPolicyIdJSONRequestBody) (*api.Policy, error) { + path := "/api/policies/" + policyID + requestBytes, err := json.Marshal(request) if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/policies/"+policyID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", path, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +84,7 @@ func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.P // Delete delete policy // See more: https://docs.netbird.io/api/resources/policies#delete-a-policy func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/policies/"+policyID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/policies/"+policyID, nil) if err != nil { return err } diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go index 7343957a5..622eeeb64 100644 --- a/management/client/rest/posturechecks.go +++ b/management/client/rest/posturechecks.go @@ -16,7 +16,7 @@ type PostureChecksAPI struct { // List list all posture checks // See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) // Get get posture check info // See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostur if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re // Delete delete posture check // See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil) if err != nil { return err } diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go index 6ca4be2c5..671c3bfc9 100644 --- a/management/client/rest/routes.go +++ b/management/client/rest/routes.go @@ -16,7 +16,7 @@ type RoutesAPI struct { // List list all routes // See more: https://docs.netbird.io/api/resources/routes#list-all-routes func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/routes", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/routes", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { // Get get route info // See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/routes/"+routeID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/routes/"+routeID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONReq if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA // Delete delete route // See more: https://docs.netbird.io/api/resources/routes#delete-a-route func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/routes/"+routeID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/routes/"+routeID, nil) if err != nil { return err } diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go index 91f370663..5625b6acc 100644 --- a/management/client/rest/setupkeys.go +++ b/management/client/rest/setupkeys.go @@ -16,7 +16,7 @@ type SetupKeysAPI struct { // List list all setup keys // See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { // Get get setup key info // See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil) if err != nil { return nil, err } @@ -44,11 +44,13 @@ func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKe // Create generate new Setup Key // See more: https://docs.netbird.io/api/resources/setup-keys#create-a-setup-key func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJSONRequestBody) (*api.SetupKeyClear, error) { + path := "/api/setup-keys" + requestBytes, err := json.Marshal(request) if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/setup-keys", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", path, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +68,7 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -80,7 +82,7 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap // Delete delete setup key // See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil) if err != nil { return err } diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go index 7e5004147..278a0d159 100644 --- a/management/client/rest/tokens.go +++ b/management/client/rest/tokens.go @@ -16,7 +16,7 @@ type TokensAPI struct { // List list user tokens // See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAcce // Get get user token info // See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA // Delete delete user token // See more: https://docs.netbird.io/api/resources/tokens#delete-a-token func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil) if err != nil { return err } diff --git a/management/client/rest/users.go b/management/client/rest/users.go index bb81796c0..107b0581e 100644 --- a/management/client/rest/users.go +++ b/management/client/rest/users.go @@ -16,7 +16,7 @@ type UsersAPI struct { // List list all users, only returns one user always // See more: https://docs.netbird.io/api/resources/users#list-all-users func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/users", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users", nil) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONReque if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi if err != nil { return nil, err } - resp, err := a.c.newRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes)) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi // Delete delete user // See more: https://docs.netbird.io/api/resources/users#delete-a-user func (a *UsersAPI) Delete(ctx context.Context, userID string) error { - resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID, nil) if err != nil { return err } @@ -80,7 +80,7 @@ func (a *UsersAPI) Delete(ctx context.Context, userID string) error { // ResendInvitation resend user invitation // See more: https://docs.netbird.io/api/resources/users#resend-user-invitation func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { - resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil) + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil) if err != nil { return err } @@ -94,7 +94,7 @@ func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { // Current gets the current user info // See more: https://docs.netbird.io/api/resources/users#retrieve-current-user func (a *UsersAPI) Current(ctx context.Context) (*api.User, error) { - resp, err := a.c.newRequest(ctx, "GET", "/api/users/current", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/current", nil) if err != nil { return nil, err } From b2a10780af2269c38c60b51db9dc77704e7a7922 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 16 May 2025 09:43:13 +0200 Subject: [PATCH 013/213] [client] Disable dnssec for systemd explicitly (#3831) --- client/internal/dns/systemd_linux.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index 53c5c58a0..9040ed787 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -30,9 +30,12 @@ const ( systemdDbusSetDNSMethodSuffix = systemdDbusLinkInterface + ".SetDNS" systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute" systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains" + systemdDbusSetDNSSECMethodSuffix = systemdDbusLinkInterface + ".SetDNSSEC" systemdDbusResolvConfModeForeign = "foreign" dbusErrorUnknownObject = "org.freedesktop.DBus.Error.UnknownObject" + + dnsSecDisabled = "no" ) type systemdDbusConfigurator struct { @@ -95,9 +98,13 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana Family: unix.AF_INET, Address: ipAs4[:], } - err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}) - if err != nil { - return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %w", config.ServerIP, config.ServerPort, err) + if err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil { + return fmt.Errorf("set interface DNS server %s:%d: %w", config.ServerIP, config.ServerPort, err) + } + + // We don't support dnssec. On some machines this is default on so we explicitly set it to off + if err = s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil { + log.Warnf("failed to set DNSSEC to 'no': %v", err) } var ( From f1de8e6eb02a4ef2a5acc0ebed8b0312c25507d7 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 16 May 2025 13:16:51 +0200 Subject: [PATCH 014/213] [management] Make startup period configurable (#3767) --- management/server/account.go | 42 +++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 5c474a343..a7b3e628b 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -196,21 +196,7 @@ func BuildManager( permissionsManager: permissionsManager, } - var initialInterval int64 - intervalStr := os.Getenv("PEER_UPDATE_INTERVAL_MS") - interval, err := strconv.Atoi(intervalStr) - if err != nil { - initialInterval = 1 - } else { - initialInterval = int64(interval) * 10 - go func() { - time.Sleep(30 * time.Second) - am.updateAccountPeersBufferInterval.Store(int64(time.Duration(interval) * time.Millisecond)) - log.WithContext(ctx).Infof("set peer update buffer interval to %dms", interval) - }() - } - am.updateAccountPeersBufferInterval.Store(initialInterval) - log.WithContext(ctx).Infof("set peer update buffer interval to %dms", initialInterval) + am.startWarmup(ctx) accountsCounter, err := store.GetAccountsCounter(ctx) if err != nil { @@ -254,6 +240,32 @@ func BuildManager( return am, nil } +func (am *DefaultAccountManager) startWarmup(ctx context.Context) { + var initialInterval int64 + intervalStr := os.Getenv("NB_PEER_UPDATE_INTERVAL_MS") + interval, err := strconv.Atoi(intervalStr) + if err != nil { + initialInterval = 1 + log.WithContext(ctx).Warnf("failed to parse peer update interval, using default value %dms: %v", initialInterval, err) + } else { + initialInterval = int64(interval) * 10 + go func() { + startupPeriodStr := os.Getenv("NB_PEER_UPDATE_STARTUP_PERIOD_S") + startupPeriod, err := strconv.Atoi(startupPeriodStr) + if err != nil { + startupPeriod = 1 + log.WithContext(ctx).Warnf("failed to parse peer update startup period, using default value %ds: %v", startupPeriod, err) + } + time.Sleep(time.Duration(startupPeriod) * time.Second) + am.updateAccountPeersBufferInterval.Store(int64(time.Duration(interval) * time.Millisecond)) + log.WithContext(ctx).Infof("set peer update buffer interval to %dms", interval) + }() + } + am.updateAccountPeersBufferInterval.Store(initialInterval) + log.WithContext(ctx).Infof("set peer update buffer interval to %dms", initialInterval) + +} + func (am *DefaultAccountManager) GetExternalCacheManager() account.ExternalCacheManager { return am.externalCacheManager } From 99f8dc7748c30049456ae5f16dc44a415751f93d Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 16 May 2025 17:39:30 +0200 Subject: [PATCH 015/213] [client] Offer to remove netbird data in windows uninstall (#3766) --- client/installer.nsis | 83 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/client/installer.nsis b/client/installer.nsis index 5219058a8..fabd727fc 100644 --- a/client/installer.nsis +++ b/client/installer.nsis @@ -24,6 +24,8 @@ !define AUTOSTART_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Run" +!define NETBIRD_DATA_DIR "$COMMONPROGRAMDATA\Netbird" + Unicode True ###################################################################### @@ -49,6 +51,10 @@ ShowInstDetails Show ###################################################################### +!include "MUI2.nsh" +!include LogicLib.nsh +!include "nsDialogs.nsh" + !define MUI_ICON "${ICON}" !define MUI_UNICON "${ICON}" !define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}" @@ -58,9 +64,6 @@ ShowInstDetails Show !define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink" ###################################################################### -!include "MUI2.nsh" -!include LogicLib.nsh - !define MUI_ABORTWARNING !define MUI_UNABORTWARNING @@ -70,13 +73,16 @@ ShowInstDetails Show !insertmacro MUI_PAGE_DIRECTORY -; Custom page for autostart checkbox Page custom AutostartPage AutostartPageLeave !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_WELCOME + +UninstPage custom un.DeleteDataPage un.DeleteDataPageLeave + !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES @@ -89,6 +95,10 @@ Page custom AutostartPage AutostartPageLeave Var AutostartCheckbox Var AutostartEnabled +; Variables for uninstall data deletion option +Var DeleteDataCheckbox +Var DeleteDataEnabled + ###################################################################### ; Function to create the autostart options page @@ -104,8 +114,8 @@ Function AutostartPage ${NSD_CreateCheckbox} 0 20u 100% 10u "Start ${APP_NAME} UI automatically when Windows starts" Pop $AutostartCheckbox - ${NSD_Check} $AutostartCheckbox ; Default to checked - StrCpy $AutostartEnabled "1" ; Default to enabled + ${NSD_Check} $AutostartCheckbox + StrCpy $AutostartEnabled "1" nsDialogs::Show FunctionEnd @@ -115,6 +125,30 @@ Function AutostartPageLeave ${NSD_GetState} $AutostartCheckbox $AutostartEnabled FunctionEnd +; Function to create the uninstall data deletion page +Function un.DeleteDataPage + !insertmacro MUI_HEADER_TEXT "Uninstall Options" "Choose whether to delete ${APP_NAME} data." + + nsDialogs::Create 1018 + Pop $0 + + ${If} $0 == error + Abort + ${EndIf} + + ${NSD_CreateCheckbox} 0 20u 100% 10u "Delete all ${APP_NAME} configuration and state data (${NETBIRD_DATA_DIR})" + Pop $DeleteDataCheckbox + ${NSD_Uncheck} $DeleteDataCheckbox + StrCpy $DeleteDataEnabled "0" + + nsDialogs::Show +FunctionEnd + +; Function to handle leaving the data deletion page +Function un.DeleteDataPageLeave + ${NSD_GetState} $DeleteDataCheckbox $DeleteDataEnabled +FunctionEnd + Function GetAppFromCommand Exch $1 Push $2 @@ -176,10 +210,10 @@ ${EndIf} FunctionEnd ###################################################################### Section -MainProgram - ${INSTALL_TYPE} - # SetOverwrite ifnewer - SetOutPath "$INSTDIR" - File /r "..\\dist\\netbird_windows_amd64\\" + ${INSTALL_TYPE} + # SetOverwrite ifnewer + SetOutPath "$INSTDIR" + File /r "..\\dist\\netbird_windows_amd64\\" SectionEnd ###################################################################### @@ -225,31 +259,58 @@ SectionEnd Section Uninstall ${INSTALL_TYPE} +DetailPrint "Stopping Netbird service..." ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop' +DetailPrint "Uninstalling Netbird service..." ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall' -# kill ui client +DetailPrint "Terminating Netbird UI process..." ExecWait `taskkill /im ${UI_APP_EXE}.exe /f` ; Remove autostart registry entry +DetailPrint "Removing autostart registry entry if exists..." DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}" +; Handle data deletion based on checkbox +DetailPrint "Checking if user requested data deletion..." +${If} $DeleteDataEnabled == "1" + DetailPrint "User opted to delete Netbird data. Removing ${NETBIRD_DATA_DIR}..." + ClearErrors + RMDir /r "${NETBIRD_DATA_DIR}" + IfErrors 0 +2 ; If no errors, jump over the message + DetailPrint "Error deleting Netbird data directory. It might be in use or already removed." + DetailPrint "Netbird data directory removal complete." +${Else} + DetailPrint "User did not opt to delete Netbird data." +${EndIf} + # wait the service uninstall take unblock the executable +DetailPrint "Waiting for service handle to be released..." Sleep 3000 + +DetailPrint "Deleting application files..." Delete "$INSTDIR\${UI_APP_EXE}" Delete "$INSTDIR\${MAIN_APP_EXE}" Delete "$INSTDIR\wintun.dll" Delete "$INSTDIR\opengl32.dll" +DetailPrint "Removing application directory..." RmDir /r "$INSTDIR" +DetailPrint "Removing shortcuts..." SetShellVarContext all Delete "$DESKTOP\${APP_NAME}.lnk" Delete "$SMPROGRAMS\${APP_NAME}.lnk" +DetailPrint "Removing registry keys..." DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" +DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}" + +DetailPrint "Removing application directory from PATH..." EnVar::SetHKLM EnVar::DeleteValue "path" "$INSTDIR" + +DetailPrint "Uninstallation finished." SectionEnd From 701c13807acc74115db090fa9f3287a7d54f0c07 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 19 May 2025 13:36:24 +0200 Subject: [PATCH 016/213] [management] add flag to disable auto-migration (#3840) --- management/cmd/management.go | 2 +- management/server/store/sql_store.go | 35 +++++++++++++++------------ management/server/store/store.go | 12 ++++----- management/server/store/store_test.go | 2 +- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/management/cmd/management.go b/management/cmd/management.go index d6735f955..5fb07890f 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -159,7 +159,7 @@ var ( if err != nil { return err } - store, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, appMetrics) + store, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, appMetrics, false) if err != nil { return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err) } diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index d568460f9..878f09c7e 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -66,7 +66,7 @@ type installation struct { type migrationFunc func(*gorm.DB) error // NewSqlStore creates a new SqlStore instance. -func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics) (*SqlStore, error) { +func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) { sql, err := db.DB() if err != nil { return nil, err @@ -88,6 +88,11 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met log.WithContext(ctx).Infof("Set max open db connections to %d", conns) + if skipMigration { + log.WithContext(ctx).Infof("skipping migration") + return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1}, nil + } + if err := migrate(ctx, db); err != nil { return nil, fmt.Errorf("migrate: %w", err) } @@ -1016,7 +1021,7 @@ func (s *SqlStore) GetStoreEngine() types.Engine { } // NewSqliteStore creates a new SQLite store. -func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) { +func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) { storeStr := fmt.Sprintf("%s?cache=shared", storeSqliteFileName) if runtime.GOOS == "windows" { // Vo avoid `The process cannot access the file because it is being used by another process` on Windows @@ -1029,27 +1034,27 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe return nil, err } - return NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics) + return NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration) } // NewPostgresqlStore creates a new Postgres store. -func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { +func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) { db, err := gorm.Open(postgres.Open(dsn), getGormConfig()) if err != nil { return nil, err } - return NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics) + return NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration) } // NewMysqlStore creates a new MySQL store. -func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { +func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) { db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) if err != nil { return nil, err } - return NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics) + return NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics, skipMigration) } func getGormConfig() *gorm.Config { @@ -1060,26 +1065,26 @@ func getGormConfig() *gorm.Config { } // newPostgresStore initializes a new Postgres store. -func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics) (Store, error) { +func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) { dsn, ok := os.LookupEnv(postgresDsnEnv) if !ok { return nil, fmt.Errorf("%s is not set", postgresDsnEnv) } - return NewPostgresqlStore(ctx, dsn, metrics) + return NewPostgresqlStore(ctx, dsn, metrics, skipMigration) } // newMysqlStore initializes a new MySQL store. -func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics) (Store, error) { +func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) { dsn, ok := os.LookupEnv(mysqlDsnEnv) if !ok { return nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } - return NewMysqlStore(ctx, dsn, metrics) + return NewMysqlStore(ctx, dsn, metrics, skipMigration) } // NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir. -func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) { - store, err := NewSqliteStore(ctx, dataDir, metrics) +func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) { + store, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration) if err != nil { return nil, err } @@ -1108,7 +1113,7 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data // NewPostgresqlStoreFromSqlStore restores a store from SqlStore and stores Postgres DB. func NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - store, err := NewPostgresqlStore(ctx, dsn, metrics) + store, err := NewPostgresqlStore(ctx, dsn, metrics, false) if err != nil { return nil, err } @@ -1130,7 +1135,7 @@ func NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, // NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB. func NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - store, err := NewMysqlStore(ctx, dsn, metrics) + store, err := NewMysqlStore(ctx, dsn, metrics, false) if err != nil { return nil, err } diff --git a/management/server/store/store.go b/management/server/store/store.go index 6da623956..d64596269 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -243,7 +243,7 @@ func getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) type } // NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics -func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics) (Store, error) { +func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) { kind = getStoreEngine(ctx, dataDir, kind) if err := checkFileStoreEngine(kind, dataDir); err != nil { @@ -253,13 +253,13 @@ func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics te switch kind { case types.SqliteStoreEngine: log.WithContext(ctx).Info("using SQLite store engine") - return NewSqliteStore(ctx, dataDir, metrics) + return NewSqliteStore(ctx, dataDir, metrics, skipMigration) case types.PostgresStoreEngine: log.WithContext(ctx).Info("using Postgres store engine") - return newPostgresStore(ctx, metrics) + return newPostgresStore(ctx, metrics, skipMigration) case types.MysqlStoreEngine: log.WithContext(ctx).Info("using MySQL store engine") - return newMysqlStore(ctx, metrics) + return newMysqlStore(ctx, metrics, skipMigration) default: return nil, fmt.Errorf("unsupported kind of store: %s", kind) } @@ -354,7 +354,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } } - store, err := NewSqlStore(ctx, db, types.SqliteStoreEngine, nil) + store, err := NewSqlStore(ctx, db, types.SqliteStoreEngine, nil, false) if err != nil { return nil, nil, fmt.Errorf("failed to create test store: %v", err) } @@ -563,7 +563,7 @@ func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error { log.WithContext(ctx).Infof("%d account will be migrated from file store %s to sqlite store %s", fsStoreAccounts, fileStorePath, sqlStorePath) - store, err := NewSqliteStoreFromFileStore(ctx, fstore, dataDir, nil) + store, err := NewSqliteStoreFromFileStore(ctx, fstore, dataDir, nil, true) if err != nil { return fmt.Errorf("failed creating file store: %s: %v", dataDir, err) } diff --git a/management/server/store/store_test.go b/management/server/store/store_test.go index 1d0026e3d..19fce2195 100644 --- a/management/server/store/store_test.go +++ b/management/server/store/store_test.go @@ -16,7 +16,7 @@ type benchCase struct { var newSqlite = func(b *testing.B) Store { b.Helper() - store, _ := NewSqliteStore(context.Background(), b.TempDir(), nil) + store, _ := NewSqliteStore(context.Background(), b.TempDir(), nil, false) return store } From cbcdad78149c7c9d3273a818cb83db5695068b8e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 19 May 2025 15:41:24 +0200 Subject: [PATCH 017/213] [misc] Update issue template (#3842) --- .github/ISSUE_TEMPLATE/bug-issue-report.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue-report.md b/.github/ISSUE_TEMPLATE/bug-issue-report.md index 3633cca4f..df670db06 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue-report.md +++ b/.github/ISSUE_TEMPLATE/bug-issue-report.md @@ -37,16 +37,21 @@ If yes, which one? **Debug output** -To help us resolve the problem, please attach the following debug output +To help us resolve the problem, please attach the following anonymized status output netbird status -dA -As well as the file created by +Create and upload a debug bundle, and share the returned file key: + + netbird debug for 1m -AS -U + +*Uploaded files are automatically deleted after 30 days.* + + +Alternatively, create the file only and attach it here manually: netbird debug for 1m -AS - -We advise reviewing the anonymized output for any remaining personal information. **Screenshots** @@ -57,8 +62,10 @@ If applicable, add screenshots to help explain your problem. Add any other context about the problem here. **Have you tried these troubleshooting steps?** +- [ ] Reviewed [client troubleshooting](https://docs.netbird.io/how-to/troubleshooting-client) (if applicable) - [ ] Checked for newer NetBird versions - [ ] Searched for similar issues on GitHub (including closed ones) - [ ] Restarted the NetBird client - [ ] Disabled other VPN software - [ ] Checked firewall settings + From 207fa059d22b566e2887fbffbd20b6791d73c464 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 19 May 2025 16:42:47 +0200 Subject: [PATCH 018/213] [management] make locking strength clause optional (#3844) --- management/server/store/sql_store.go | 379 +++++++++++++++++++++++---- management/server/store/store.go | 1 + 2 files changed, 326 insertions(+), 54 deletions(-) diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 878f09c7e..eb194ca9b 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -481,8 +481,13 @@ func (s *SqlStore) GetAccountByPrivateDomain(ctx context.Context, domain string) } func (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountID string - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).Select("id"). + result := tx.Model(&types.Account{}).Select("id"). Where("domain = ? and is_domain_primary_account = ? and domain_category = ?", strings.ToLower(domain), true, types.PrivateCategory, ).First(&accountID) @@ -530,8 +535,13 @@ func (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken stri } func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var user types.User - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Joins("JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id"). Where("personal_access_tokens.id = ?", patID).First(&user) if result.Error != nil { @@ -546,8 +556,13 @@ func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStren } func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var user types.User - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&user, idQueryCondition, userID) + result := tx.First(&user, idQueryCondition, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewUserNotFoundError(userID) @@ -578,8 +593,13 @@ func (s *SqlStore) DeleteUser(ctx context.Context, lockStrength LockingStrength, } func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var users []*types.User - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&users, accountIDCondition, accountID) + result := tx.Find(&users, accountIDCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "accountID not found: index lookup failed") @@ -592,8 +612,13 @@ func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStre } func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.User, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var user types.User - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&user, "account_id = ? AND role = ?", accountID, types.UserRoleOwner) + result := tx.First(&user, "account_id = ? AND role = ?", accountID, types.UserRoleOwner) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "account owner not found: index lookup failed") @@ -605,8 +630,13 @@ func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStre } func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var groups []*types.Group - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&groups, accountIDCondition, accountID) + result := tx.Find(&groups, accountIDCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "accountID not found: index lookup failed") @@ -619,11 +649,16 @@ func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStr } func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var groups []*types.Group likePattern := `%"ID":"` + resourceID + `"%` - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Where("resources LIKE ?", likePattern). Find(&groups) @@ -664,8 +699,13 @@ func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) { } func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountMeta types.AccountMeta - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + result := tx.Model(&types.Account{}). First(&accountMeta, idQueryCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("error when getting account meta %s from the store: %s", accountID, result.Error) @@ -833,8 +873,13 @@ func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) } func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountID string - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.User{}). + result := tx.Model(&types.User{}). Select("account_id").Where(idQueryCondition, userID).First(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -847,8 +892,13 @@ func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength Lockin } func (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountID string - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). + result := tx.Model(&nbpeer.Peer{}). Select("account_id").Where(idQueryCondition, peerID).First(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -879,10 +929,15 @@ func (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) } func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]net.IP, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var ipJSONStrings []string // Fetch the IP addresses as JSON strings - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). + result := tx.Model(&nbpeer.Peer{}). Where("account_id = ?", accountID). Pluck("ip", &ipJSONStrings) if result.Error != nil { @@ -906,8 +961,13 @@ func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength } func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string) ([]string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var labels []string - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). + result := tx.Model(&nbpeer.Peer{}). Where("account_id = ?", accountID). Pluck("dns_label", &labels) @@ -923,8 +983,13 @@ func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength Lock } func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Network, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountNetwork types.AccountNetwork - if err := s.db.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountNetwork).Error; err != nil { + if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountNetwork).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewAccountNotFoundError(accountID) } @@ -934,8 +999,13 @@ func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingSt } func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var peer nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&peer, GetKeyQueryCondition(s), peerKey) + result := tx.First(&peer, GetKeyQueryCondition(s), peerKey) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -948,8 +1018,13 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking } func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountSettings types.AccountSettings - if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountSettings).Error; err != nil { + if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountSettings).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "settings not found") } @@ -959,8 +1034,13 @@ func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingS } func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var createdBy string - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + result := tx.Model(&types.Account{}). Select("created_by").First(&createdBy, idQueryCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1156,8 +1236,13 @@ func NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn s } func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var setupKey types.SetupKey - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&setupKey, GetKeyQueryCondition(s), key) if result.Error != nil { @@ -1299,8 +1384,13 @@ func (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string // GetPeerGroups retrieves all groups assigned to a specific peer in a given account. func (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var groups []*types.Group - query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + query := tx. Find(&groups, "account_id = ? AND peers LIKE ?", accountId, fmt.Sprintf(`%%"%s"%%`, peerId)) if query.Error != nil { @@ -1332,6 +1422,11 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre // GetUserPeers retrieves peers for a user. func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var peers []*nbpeer.Peer // Exclude peers added via setup keys, as they are not user-specific and have an empty user_id. @@ -1339,7 +1434,7 @@ func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrengt return peers, nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Find(&peers, "account_id = ? AND user_id = ?", accountID, userID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err) @@ -1359,8 +1454,13 @@ func (s *SqlStore) AddPeerToAccount(ctx context.Context, lockStrength LockingStr // GetPeerByID retrieves a peer by its ID and account ID. func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var peer *nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&peer, accountAndIDQueryCondition, accountID, peerID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1374,8 +1474,13 @@ func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength // GetPeersByIDs retrieves peers by their IDs and account ID. func (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var peers []*nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs) + result := tx.Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get peers by ID's from the store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get peers by ID's from the store") @@ -1391,8 +1496,13 @@ func (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStreng // GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user. func (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var peers []*nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Where("login_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true). Find(&peers, accountIDCondition, accountID) if err := result.Error; err != nil { @@ -1405,8 +1515,13 @@ func (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStreng // GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user. func (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var peers []*nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Where("inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true). Find(&peers, accountIDCondition, accountID) if err := result.Error; err != nil { @@ -1419,8 +1534,13 @@ func (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStreng // GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing. func (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var allEphemeralPeers, batchPeers []*nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Where("ephemeral = ?", true). FindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error { allEphemeralPeers = append(allEphemeralPeers, batchPeers...) @@ -1496,8 +1616,13 @@ func (s *SqlStore) GetDB() *gorm.DB { } func (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountDNSSettings types.AccountDNSSettings - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + result := tx.Model(&types.Account{}). First(&accountDNSSettings, idQueryCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1511,8 +1636,13 @@ func (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength Locki // AccountExists checks whether an account exists by the given ID. func (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var accountID string - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + result := tx.Model(&types.Account{}). Select("id").First(&accountID, idQueryCondition, id) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1526,8 +1656,13 @@ func (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStreng // GetAccountDomainAndCategory retrieves the Domain and DomainCategory fields for an account based on the given accountID. func (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var account types.Account - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).Select("domain", "domain_category"). + result := tx.Model(&types.Account{}).Select("domain", "domain_category"). Where(idQueryCondition, accountID).First(&account) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1541,8 +1676,13 @@ func (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength // GetGroupByID retrieves a group by ID and account ID. func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var group *types.Group - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&group, accountAndIDQueryCondition, accountID, groupID) + result := tx.First(&group, accountAndIDQueryCondition, accountID, groupID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewGroupNotFoundError(groupID) @@ -1556,11 +1696,16 @@ func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrengt // GetGroupByName retrieves a group by name and account ID. func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var group types.Group // TODO: This fix is accepted for now, but if we need to handle this more frequently // we may need to reconsider changing the types. - query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Preload(clause.Associations) + query := tx.Preload(clause.Associations) switch s.storeEngine { case types.PostgresStoreEngine: @@ -1584,8 +1729,13 @@ func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStren // GetGroupsByIDs retrieves groups by their IDs and account ID. func (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var groups []*types.Group - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs) + result := tx.Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get groups by ID's from store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get groups by ID's from store") @@ -1639,8 +1789,13 @@ func (s *SqlStore) DeleteGroups(ctx context.Context, strength LockingStrength, a // GetAccountPolicies retrieves policies for an account. func (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var policies []*types.Policy - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Preload(clause.Associations).Find(&policies, accountIDCondition, accountID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get policies from the store: %s", result.Error) @@ -1652,8 +1807,14 @@ func (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingS // GetPolicyByID retrieves a policy by its ID and account ID. func (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var policy *types.Policy - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Preload(clause.Associations). + + result := tx.Preload(clause.Associations). First(&policy, accountAndIDQueryCondition, accountID, policyID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -1712,8 +1873,13 @@ func (s *SqlStore) DeletePolicy(ctx context.Context, lockStrength LockingStrengt // GetAccountPostureChecks retrieves posture checks for an account. func (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*posture.Checks, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var postureChecks []*posture.Checks - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&postureChecks, accountIDCondition, accountID) + result := tx.Find(&postureChecks, accountIDCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get posture checks from store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get posture checks from store") @@ -1724,8 +1890,13 @@ func (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength Loc // GetPostureChecksByID retrieves posture checks by their ID and account ID. func (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) (*posture.Checks, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var postureCheck *posture.Checks - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1740,8 +1911,13 @@ func (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength Lockin // GetPostureChecksByIDs retrieves posture checks by their IDs and account ID. func (s *SqlStore) GetPostureChecksByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, postureChecksIDs []string) (map[string]*posture.Checks, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var postureChecks []*posture.Checks - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs) + result := tx.Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get posture checks by ID's from store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get posture checks by ID's from store") @@ -1794,8 +1970,13 @@ func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrengt // GetAccountSetupKeys retrieves setup keys for an account. func (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.SetupKey, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var setupKeys []*types.SetupKey - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Find(&setupKeys, accountIDCondition, accountID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get setup keys from the store: %s", err) @@ -1807,8 +1988,13 @@ func (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength Locking // GetSetupKeyByID retrieves a setup key by its ID and account ID. func (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStrength, accountID, setupKeyID string) (*types.SetupKey, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var setupKey *types.SetupKey - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). First(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -1849,8 +2035,13 @@ func (s *SqlStore) DeleteSetupKey(ctx context.Context, lockStrength LockingStren // GetAccountNameServerGroups retrieves name server groups for an account. func (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbdns.NameServerGroup, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var nsGroups []*nbdns.NameServerGroup - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&nsGroups, accountIDCondition, accountID) + result := tx.Find(&nsGroups, accountIDCondition, accountID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get name server groups from the store: %s", err) return nil, status.Errorf(status.Internal, "failed to get name server groups from store") @@ -1861,8 +2052,13 @@ func (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength // GetNameServerGroupByID retrieves a name server group by its ID and account ID. func (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) (*nbdns.NameServerGroup, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var nsGroup *nbdns.NameServerGroup - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -1902,9 +2098,14 @@ func (s *SqlStore) DeleteNameServerGroup(ctx context.Context, lockStrength Locki // getRecords retrieves records from the database based on the account ID. func getRecords[T any](db *gorm.DB, lockStrength LockingStrength, accountID string) ([]T, error) { + tx := db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var record []T - result := db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&record, accountIDCondition, accountID) + result := tx.Find(&record, accountIDCondition, accountID) if err := result.Error; err != nil { parts := strings.Split(fmt.Sprintf("%T", record), ".") recordType := parts[len(parts)-1] @@ -1917,9 +2118,14 @@ func getRecords[T any](db *gorm.DB, lockStrength LockingStrength, accountID stri // getRecordByID retrieves a record by its ID and account ID from the database. func getRecordByID[T any](db *gorm.DB, lockStrength LockingStrength, recordID, accountID string) (*T, error) { + tx := db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var record T - result := db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). First(&record, accountAndIDQueryCondition, accountID, recordID) if err := result.Error; err != nil { parts := strings.Split(fmt.Sprintf("%T", record), ".") @@ -1950,8 +2156,13 @@ func (s *SqlStore) SaveDNSSettings(ctx context.Context, lockStrength LockingStre } func (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var networks []*networkTypes.Network - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&networks, accountIDCondition, accountID) + result := tx.Find(&networks, accountIDCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get networks from the store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get networks from store") @@ -1961,8 +2172,13 @@ func (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingS } func (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) (*networkTypes.Network, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var network *networkTypes.Network - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&network, accountAndIDQueryCondition, accountID, networkID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -2002,8 +2218,13 @@ func (s *SqlStore) DeleteNetwork(ctx context.Context, lockStrength LockingStreng } func (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*routerTypes.NetworkRouter, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netRouters []*routerTypes.NetworkRouter - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Find(&netRouters, "account_id = ? AND network_id = ?", accountID, netID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get network routers from store: %v", result.Error) @@ -2014,8 +2235,13 @@ func (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength Lo } func (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*routerTypes.NetworkRouter, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netRouters []*routerTypes.NetworkRouter - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Find(&netRouters, accountIDCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get network routers from store: %v", result.Error) @@ -2026,8 +2252,13 @@ func (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrengt } func (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) (*routerTypes.NetworkRouter, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netRouter *routerTypes.NetworkRouter - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&netRouter, accountAndIDQueryCondition, accountID, routerID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -2066,8 +2297,13 @@ func (s *SqlStore) DeleteNetworkRouter(ctx context.Context, lockStrength Locking } func (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) ([]*resourceTypes.NetworkResource, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netResources []*resourceTypes.NetworkResource - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Find(&netResources, "account_id = ? AND network_id = ?", accountID, networkID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get network resources from store: %v", result.Error) @@ -2078,8 +2314,13 @@ func (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength } func (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netResources []*resourceTypes.NetworkResource - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Find(&netResources, accountIDCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get network resources from store: %v", result.Error) @@ -2090,8 +2331,13 @@ func (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStren } func (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netResources *resourceTypes.NetworkResource - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&netResources, accountAndIDQueryCondition, accountID, resourceID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -2105,8 +2351,13 @@ func (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength Lock } func (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var netResources *resourceTypes.NetworkResource - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&netResources, "account_id = ? AND name = ?", accountID, resourceName) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -2146,8 +2397,13 @@ func (s *SqlStore) DeleteNetworkResource(ctx context.Context, lockStrength Locki // GetPATByHashedToken returns a PersonalAccessToken by its hashed token. func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var pat types.PersonalAccessToken - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&pat, "hashed_token = ?", hashedToken) + result := tx.First(&pat, "hashed_token = ?", hashedToken) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPATNotFoundError(hashedToken) @@ -2161,8 +2417,13 @@ func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength Locking // GetPATByID retrieves a personal access token by its ID and user ID. func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var pat types.PersonalAccessToken - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&pat, "id = ? AND user_id = ?", patID, userID) if err := result.Error; err != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -2177,8 +2438,13 @@ func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, // GetUserPATs retrieves personal access tokens for a user. func (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var pats []*types.PersonalAccessToken - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&pats, "user_id = ?", userID) + result := tx.Find(&pats, "user_id = ?", userID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get user pat's from the store: %s", err) return nil, status.Errorf(status.Internal, "failed to get user pat's from store") @@ -2236,10 +2502,15 @@ func (s *SqlStore) DeletePAT(ctx context.Context, lockStrength LockingStrength, } func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + jsonValue := fmt.Sprintf(`"%s"`, ip.String()) var peer nbpeer.Peer - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. First(&peer, "account_id = ? AND ip = ?", accountID, jsonValue) if result.Error != nil { // no logging here diff --git a/management/server/store/store.go b/management/server/store/store.go index d64596269..3d529ceb5 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -44,6 +44,7 @@ const ( LockingStrengthShare LockingStrength = "SHARE" // Allows reading but prevents changes by other transactions. LockingStrengthNoKeyUpdate LockingStrength = "NO KEY UPDATE" // Similar to UPDATE but allows changes to related rows. LockingStrengthKeyShare LockingStrength = "KEY SHARE" // Protects against changes to primary/unique keys but allows other updates. + LockingStrengthNone LockingStrength = "NONE" // No locking, allowing all transactions to proceed without restrictions. ) type Store interface { From 1d4cfb83e70112eb34f541263c4652c83167fa8e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 20 May 2025 10:39:17 +0200 Subject: [PATCH 019/213] [client] Fix UI new version notifier (#3845) --- client/ui/client_ui.go | 2 ++ version/update.go | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 03458bc4b..b9fefe518 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -62,6 +62,8 @@ func main() { return } logFile = file + } else { + _ = util.InitLog("trace", "console") } // Create the Fyne application. diff --git a/version/update.go b/version/update.go index 1de60ea9a..3f548ea85 100644 --- a/version/update.go +++ b/version/update.go @@ -93,23 +93,25 @@ func (u *Update) SetOnUpdateListener(updateFn func()) { } func (u *Update) startFetcher() { - changed := u.fetchVersion() - if changed { + if changed := u.fetchVersion(); changed { u.checkUpdate() } - select { - case <-u.fetchDone: - return - case <-u.fetchTicker.C: - changed := u.fetchVersion() - if changed { - u.checkUpdate() + for { + select { + case <-u.fetchDone: + return + case <-u.fetchTicker.C: + if changed := u.fetchVersion(); changed { + u.checkUpdate() + } } } } func (u *Update) fetchVersion() bool { + log.Debugf("fetching version info from %s", versionURL) + resp, err := http.Get(versionURL) if err != nil { log.Errorf("failed to fetch version info: %s", err) From 4785f23fc47f5a002593c42252931e01e0d57dc4 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 20 May 2025 17:00:37 +0300 Subject: [PATCH 020/213] [management] Migrate events sqlite store to gorm (#3837) --- go.mod | 2 +- management/server/activity/event.go | 17 +- .../server/activity/sqlite/migration.go | 252 +++++++------ .../server/activity/sqlite/migration_test.go | 148 +++++--- management/server/activity/sqlite/sqlite.go | 343 ++++++------------ management/server/event.go | 2 +- 6 files changed, 365 insertions(+), 399 deletions(-) diff --git a/go.mod b/go.mod index 0f779e35d..c86acdf26 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,6 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/libdns/route53 v1.5.0 github.com/libp2p/go-netroute v0.2.1 - github.com/mattn/go-sqlite3 v1.14.22 github.com/mdlayher/socket v0.5.1 github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 @@ -195,6 +194,7 @@ require ( github.com/libdns/libdns v0.2.2 // indirect github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mholt/acmez/v2 v2.0.1 // indirect diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 0e819c3a7..5bcc67fbc 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -19,22 +19,22 @@ type Event struct { // Timestamp of the event Timestamp time.Time // Activity that was performed during the event - Activity ActivityDescriber + Activity Activity `gorm:"type:integer"` // ID of the event (can be empty, meaning that it wasn't yet generated) - ID uint64 + ID uint64 `gorm:"primaryKey;autoIncrement"` // InitiatorID is the ID of an object that initiated the event (e.g., a user) InitiatorID string // InitiatorName is the name of an object that initiated the event. - InitiatorName string + InitiatorName string `gorm:"-"` // InitiatorEmail is the email address of an object that initiated the event. - InitiatorEmail string + InitiatorEmail string `gorm:"-"` // TargetID is the ID of an object that was effected by the event (e.g., a peer) TargetID string // AccountID is the ID of an account where the event happened AccountID string // Meta of the event, e.g. deleted peer information like name, IP, etc - Meta map[string]any + Meta map[string]any `gorm:"serializer:json"` } // Copy the event @@ -57,3 +57,10 @@ func (e *Event) Copy() *Event { Meta: meta, } } + +type DeletedUser struct { + ID string `gorm:"primaryKey"` + Email string `gorm:"not null"` + Name string + EncAlgo string `gorm:"not null"` +} diff --git a/management/server/activity/sqlite/migration.go b/management/server/activity/sqlite/migration.go index 28c5b3020..6da7893a0 100644 --- a/management/server/activity/sqlite/migration.go +++ b/management/server/activity/sqlite/migration.go @@ -2,156 +2,180 @@ package sqlite import ( "context" - "database/sql" "fmt" log "github.com/sirupsen/logrus" + "gorm.io/gorm" + + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/migration" ) -func migrate(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error { - if _, err := db.Exec(createTableQuery); err != nil { - return err - } +func migrate(ctx context.Context, crypt *FieldEncrypt, db *gorm.DB) error { + migrations := getMigrations(ctx, crypt) - if _, err := db.Exec(creatTableDeletedUsersQuery); err != nil { - return err - } - - if err := updateDeletedUsersTable(ctx, db); err != nil { - return fmt.Errorf("failed to update deleted_users table: %v", err) - } - - return migrateLegacyEncryptedUsersToGCM(ctx, crypt, db) -} - -// updateDeletedUsersTable checks and updates the deleted_users table schema to ensure required columns exist. -func updateDeletedUsersTable(ctx context.Context, db *sql.DB) error { - exists, err := checkColumnExists(db, "deleted_users", "name") - if err != nil { - return err - } - - if !exists { - log.WithContext(ctx).Debug("Adding name column to the deleted_users table") - - _, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN name TEXT;`) - if err != nil { + for _, m := range migrations { + if err := m(db); err != nil { return err } - - log.WithContext(ctx).Debug("Successfully added name column to the deleted_users table") - } - - exists, err = checkColumnExists(db, "deleted_users", "enc_algo") - if err != nil { - return err - } - - if !exists { - log.WithContext(ctx).Debug("Adding enc_algo column to the deleted_users table") - - _, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN enc_algo TEXT;`) - if err != nil { - return err - } - - log.WithContext(ctx).Debug("Successfully added enc_algo column to the deleted_users table") } return nil } -// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using, +type migrationFunc func(*gorm.DB) error + +func getMigrations(ctx context.Context, crypt *FieldEncrypt) []migrationFunc { + return []migrationFunc{ + func(db *gorm.DB) error { + return migration.MigrateNewField[activity.DeletedUser](ctx, db, "name", "") + }, + func(db *gorm.DB) error { + return migration.MigrateNewField[activity.DeletedUser](ctx, db, "enc_algo", "") + }, + func(db *gorm.DB) error { + return migrateLegacyEncryptedUsersToGCM(ctx, db, crypt) + }, + func(db *gorm.DB) error { + return migrateDuplicateDeletedUsers(ctx, db) + }, + } +} + +// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using // legacy CBC encryption with a static IV to the new GCM encryption method. -func migrateLegacyEncryptedUsersToGCM(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error { - log.WithContext(ctx).Debug("Migrating CBC encrypted deleted users to GCM") +func migrateLegacyEncryptedUsersToGCM(ctx context.Context, db *gorm.DB, crypt *FieldEncrypt) error { + model := &activity.DeletedUser{} - tx, err := db.Begin() - if err != nil { - return fmt.Errorf("failed to begin transaction: %v", err) + if !db.Migrator().HasTable(model) { + log.WithContext(ctx).Debugf("Table for %T does not exist, no CBC to GCM migration needed", model) + return nil } - defer func() { - _ = tx.Rollback() - }() - rows, err := tx.Query(fmt.Sprintf(`SELECT id, email, name FROM deleted_users where enc_algo IS NULL OR enc_algo != '%s'`, gcmEncAlgo)) + var deletedUsers []activity.DeletedUser + err := db.Model(model).Find(&deletedUsers, "enc_algo IS NULL OR enc_algo != ?", gcmEncAlgo).Error if err != nil { - return fmt.Errorf("failed to execute select query: %v", err) + return fmt.Errorf("failed to query deleted_users: %w", err) } - defer rows.Close() - updateStmt, err := tx.Prepare(`UPDATE deleted_users SET email = ?, name = ?, enc_algo = ? WHERE id = ?`) - if err != nil { - return fmt.Errorf("failed to prepare update statement: %v", err) + if len(deletedUsers) == 0 { + log.WithContext(ctx).Debug("No CBC encrypted deleted users to migrate") + return nil } - defer updateStmt.Close() - if err = processUserRows(ctx, crypt, rows, updateStmt); err != nil { + if err = db.Transaction(func(tx *gorm.DB) error { + for _, user := range deletedUsers { + if err = updateDeletedUserData(tx, user, crypt); err != nil { + return fmt.Errorf("failed to migrate deleted user %s: %w", user.ID, err) + } + } + return nil + }); err != nil { return err } - if err = tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %v", err) - } - log.WithContext(ctx).Debug("Successfully migrated CBC encrypted deleted users to GCM") + return nil } -// processUserRows processes database rows of user data, decrypts legacy encryption fields, and re-encrypts them using GCM. -func processUserRows(ctx context.Context, crypt *FieldEncrypt, rows *sql.Rows, updateStmt *sql.Stmt) error { - for rows.Next() { - var ( - id, decryptedEmail, decryptedName string - email, name *string - ) +func updateDeletedUserData(transaction *gorm.DB, user activity.DeletedUser, crypt *FieldEncrypt) error { + var err error + var decryptedEmail, decryptedName string - err := rows.Scan(&id, &email, &name) + if user.Email != "" { + decryptedEmail, err = crypt.LegacyDecrypt(user.Email) if err != nil { - return err - } - - if email != nil { - decryptedEmail, err = crypt.LegacyDecrypt(*email) - if err != nil { - log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v", - id, - fmt.Errorf("failed to decrypt email: %w", err), - ) - continue - } - } - - if name != nil { - decryptedName, err = crypt.LegacyDecrypt(*name) - if err != nil { - log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v", - id, - fmt.Errorf("failed to decrypt name: %w", err), - ) - continue - } - } - - encryptedEmail, err := crypt.Encrypt(decryptedEmail) - if err != nil { - return fmt.Errorf("failed to encrypt email: %w", err) - } - - encryptedName, err := crypt.Encrypt(decryptedName) - if err != nil { - return fmt.Errorf("failed to encrypt name: %w", err) - } - - _, err = updateStmt.Exec(encryptedEmail, encryptedName, gcmEncAlgo, id) - if err != nil { - return err + return fmt.Errorf("failed to decrypt email: %w", err) } } - if err := rows.Err(); err != nil { + if user.Name != "" { + decryptedName, err = crypt.LegacyDecrypt(user.Name) + if err != nil { + return fmt.Errorf("failed to decrypt name: %w", err) + } + } + + updatedUser := user + updatedUser.EncAlgo = gcmEncAlgo + + updatedUser.Email, err = crypt.Encrypt(decryptedEmail) + if err != nil { + return fmt.Errorf("failed to encrypt email: %w", err) + } + + updatedUser.Name, err = crypt.Encrypt(decryptedName) + if err != nil { + return fmt.Errorf("failed to encrypt name: %w", err) + } + + return transaction.Model(&updatedUser).Omit("id").Updates(updatedUser).Error +} + +// MigrateDuplicateDeletedUsers removes duplicates and ensures the id column is marked as the primary key +func migrateDuplicateDeletedUsers(ctx context.Context, db *gorm.DB) error { + model := &activity.DeletedUser{} + if !db.Migrator().HasTable(model) { + log.WithContext(ctx).Debugf("Table for %T does not exist, no duplicate migration needed", model) + return nil + } + + isPrimaryKey, err := isColumnPrimaryKey[activity.DeletedUser](db, "id") + if err != nil { return err } + if isPrimaryKey { + log.WithContext(ctx).Debug("No duplicate deleted users to migrate") + return nil + } + + if err = db.Transaction(func(tx *gorm.DB) error { + groupById := tx.Model(model).Select("MAX(rowid)").Group("id") + if err = tx.Delete(model, "rowid NOT IN (?)", groupById).Error; err != nil { + return err + } + + if err = tx.Migrator().RenameTable("deleted_users", "deleted_users_old"); err != nil { + return err + } + + if err = tx.Migrator().CreateTable(model); err != nil { + return err + } + + if err = tx.Exec(` + INSERT INTO deleted_users (id, email, name, enc_algo) SELECT id, email, name, enc_algo + FROM deleted_users_old;`).Error; err != nil { + return err + } + + return tx.Migrator().DropTable("deleted_users_old") + }); err != nil { + return err + } + + log.WithContext(ctx).Debug("Successfully migrated duplicate deleted users") + return nil } + +// isColumnPrimaryKey checks if a column is a primary key in the given model +func isColumnPrimaryKey[T any](db *gorm.DB, columnName string) (bool, error) { + var model T + + cols, err := db.Migrator().ColumnTypes(&model) + if err != nil { + return false, err + } + + for _, col := range cols { + if col.Name() == columnName { + isPrimaryKey, _ := col.PrimaryKey() + return isPrimaryKey, nil + } + } + + return false, nil +} diff --git a/management/server/activity/sqlite/migration_test.go b/management/server/activity/sqlite/migration_test.go index a03774fa8..498c976d9 100644 --- a/management/server/activity/sqlite/migration_test.go +++ b/management/server/activity/sqlite/migration_test.go @@ -2,38 +2,39 @@ package sqlite import ( "context" - "database/sql" "path/filepath" "testing" - "time" - - _ "github.com/mattn/go-sqlite3" - "github.com/netbirdio/netbird/management/server/activity" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/migration" ) -func setupDatabase(t *testing.T) *sql.DB { +const ( + insertDeletedUserQuery = `INSERT INTO deleted_users (id, email, name, enc_algo) VALUES (?, ?, ?, ?)` +) + +func setupDatabase(t *testing.T) *gorm.DB { t.Helper() dbFile := filepath.Join(t.TempDir(), eventSinkDB) - db, err := sql.Open("sqlite3", dbFile) - require.NoError(t, err, "Failed to open database") + db, err := gorm.Open(sqlite.Open(dbFile)) + require.NoError(t, err) + sql, err := db.DB() + require.NoError(t, err) t.Cleanup(func() { - _ = db.Close() + _ = sql.Close() }) - _, err = db.Exec(createTableQuery) - require.NoError(t, err, "Failed to create events table") - - _, err = db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`) - require.NoError(t, err, "Failed to create deleted_users table") - return db } -func TestMigrate(t *testing.T) { +func TestMigrateLegacyEncryptedUsersToGCM(t *testing.T) { db := setupDatabase(t) key, err := GenerateKey() @@ -42,43 +43,98 @@ func TestMigrate(t *testing.T) { crypt, err := NewFieldEncrypt(key) require.NoError(t, err, "Failed to initialize FieldEncrypt") - legacyEmail := crypt.LegacyEncrypt("testaccount@test.com") - legacyName := crypt.LegacyEncrypt("Test Account") + t.Run("empty table, no migration required", func(t *testing.T) { + require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt)) + assert.False(t, db.Migrator().HasTable("deleted_users")) + }) - _, err = db.Exec(`INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)`, - activity.UserDeleted, time.Now(), "initiatorID", "targetID", "accountID", "") - require.NoError(t, err, "Failed to insert event") + require.NoError(t, db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`).Error) + assert.True(t, db.Migrator().HasTable("deleted_users")) + assert.False(t, db.Migrator().HasColumn("deleted_users", "enc_algo")) - _, err = db.Exec(`INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)`, "targetID", legacyEmail, legacyName) - require.NoError(t, err, "Failed to insert legacy encrypted data") + require.NoError(t, migration.MigrateNewField[activity.DeletedUser](context.Background(), db, "enc_algo", "")) + assert.True(t, db.Migrator().HasColumn("deleted_users", "enc_algo")) - colExists, err := checkColumnExists(db, "deleted_users", "enc_algo") - require.NoError(t, err, "Failed to check if enc_algo column exists") - require.False(t, colExists, "enc_algo column should not exist before migration") + t.Run("legacy users migration", func(t *testing.T) { + legacyEmail := crypt.LegacyEncrypt("test.user@test.com") + legacyName := crypt.LegacyEncrypt("Test User") - err = migrate(context.Background(), crypt, db) - require.NoError(t, err, "Migration failed") + require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", legacyEmail, legacyName, "").Error) + require.NoError(t, db.Exec(insertDeletedUserQuery, "user2", legacyEmail, legacyName, "legacy").Error) - colExists, err = checkColumnExists(db, "deleted_users", "enc_algo") - require.NoError(t, err, "Failed to check if enc_algo column exists after migration") - require.True(t, colExists, "enc_algo column should exist after migration") + require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt)) - var encAlgo string - err = db.QueryRow(`SELECT enc_algo FROM deleted_users LIMIT 1`, "").Scan(&encAlgo) - require.NoError(t, err, "Failed to select updated data") - require.Equal(t, gcmEncAlgo, encAlgo, "enc_algo should be set to 'GCM' after migration") + var users []activity.DeletedUser + require.NoError(t, db.Find(&users).Error) + assert.Len(t, users, 2) - store, err := createStore(crypt, db) - require.NoError(t, err, "Failed to create store") + for _, user := range users { + assert.Equal(t, gcmEncAlgo, user.EncAlgo) - events, err := store.Get(context.Background(), "accountID", 0, 1, false) - require.NoError(t, err, "Failed to get events") + decryptedEmail, err := crypt.Decrypt(user.Email) + require.NoError(t, err) + assert.Equal(t, "test.user@test.com", decryptedEmail) - require.Len(t, events, 1, "Should have one event") - require.Equal(t, activity.UserDeleted, events[0].Activity, "activity should match") - require.Equal(t, "initiatorID", events[0].InitiatorID, "initiator id should match") - require.Equal(t, "targetID", events[0].TargetID, "target id should match") - require.Equal(t, "accountID", events[0].AccountID, "account id should match") - require.Equal(t, "testaccount@test.com", events[0].Meta["email"], "email should match") - require.Equal(t, "Test Account", events[0].Meta["username"], "username should match") + decryptedName, err := crypt.Decrypt(user.Name) + require.NoError(t, err) + require.Equal(t, "Test User", decryptedName) + } + }) + + t.Run("users already migrated, no migration", func(t *testing.T) { + encryptedEmail, err := crypt.Encrypt("test.user@test.com") + require.NoError(t, err) + + encryptedName, err := crypt.Encrypt("Test User") + require.NoError(t, err) + + require.NoError(t, db.Exec(insertDeletedUserQuery, "user3", encryptedEmail, encryptedName, gcmEncAlgo).Error) + require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt)) + + var users []activity.DeletedUser + require.NoError(t, db.Find(&users).Error) + assert.Len(t, users, 3) + + for _, user := range users { + assert.Equal(t, gcmEncAlgo, user.EncAlgo) + + decryptedEmail, err := crypt.Decrypt(user.Email) + require.NoError(t, err) + assert.Equal(t, "test.user@test.com", decryptedEmail) + + decryptedName, err := crypt.Decrypt(user.Name) + require.NoError(t, err) + require.Equal(t, "Test User", decryptedName) + } + }) +} + +func TestMigrateDuplicateDeletedUsers(t *testing.T) { + db := setupDatabase(t) + + require.NoError(t, migrateDuplicateDeletedUsers(context.Background(), db)) + assert.False(t, db.Migrator().HasTable("deleted_users")) + + require.NoError(t, db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT, enc_algo TEXT NOT NULL);`).Error) + assert.True(t, db.Migrator().HasTable("deleted_users")) + + isPrimaryKey, err := isColumnPrimaryKey[activity.DeletedUser](db, "id") + require.NoError(t, err) + assert.False(t, isPrimaryKey) + + require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", "email1", "name1", "GCM").Error) + require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", "email2", "name2", "GCM").Error) + require.NoError(t, migrateDuplicateDeletedUsers(context.Background(), db)) + + isPrimaryKey, err = isColumnPrimaryKey[activity.DeletedUser](db, "id") + require.NoError(t, err) + assert.True(t, isPrimaryKey) + + var users []activity.DeletedUser + require.NoError(t, db.Find(&users).Error) + assert.Len(t, users, 1) + assert.Equal(t, "user1", users[0].ID) + assert.Equal(t, "email2", users[0].Email) + assert.Equal(t, "name2", users[0].Name) + assert.Equal(t, "GCM", users[0].EncAlgo) } diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index ffb863de9..6d198fca9 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -2,75 +2,21 @@ package sqlite import ( "context" - "database/sql" - "encoding/json" "fmt" "path/filepath" - "runtime" - "time" - _ "github.com/mattn/go-sqlite3" log "github.com/sirupsen/logrus" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/logger" "github.com/netbirdio/netbird/management/server/activity" ) const ( // eventSinkDB is the default name of the events database - eventSinkDB = "events.db" - createTableQuery = "CREATE TABLE IF NOT EXISTS events " + - "(id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "activity INTEGER, " + - "timestamp DATETIME, " + - "initiator_id TEXT," + - "account_id TEXT," + - "meta TEXT," + - " target_id TEXT);" - - creatTableDeletedUsersQuery = `CREATE TABLE IF NOT EXISTS deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT, enc_algo TEXT NOT NULL);` - - selectDescQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta - FROM events - LEFT JOIN ( - SELECT id, MAX(name) as name, MAX(email) as email - FROM deleted_users - GROUP BY id - ) i ON events.initiator_id = i.id - LEFT JOIN ( - SELECT id, MAX(name) as name, MAX(email) as email - FROM deleted_users - GROUP BY id - ) t ON events.target_id = t.id - WHERE account_id = ? - ORDER BY timestamp DESC LIMIT ? OFFSET ?;` - - selectAscQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta - FROM events - LEFT JOIN ( - SELECT id, MAX(name) as name, MAX(email) as email - FROM deleted_users - GROUP BY id - ) i ON events.initiator_id = i.id - LEFT JOIN ( - SELECT id, MAX(name) as name, MAX(email) as email - FROM deleted_users - GROUP BY id - ) t ON events.target_id = t.id - WHERE account_id = ? - ORDER BY timestamp ASC LIMIT ? OFFSET ?;` - - insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " + - "VALUES(?, ?, ?, ?, ?, ?)" - - /* - TODO: - The insert should avoid duplicated IDs in the table. So the query should be changes to something like: - `INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, name = EXCLUDED.name;` - For this to work we have to set the id column as primary key. But this is not possible because the id column is not unique - and some selfhosted deployments might have duplicates already so we need to clean the table first. - */ - - insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name, enc_algo) VALUES(?, ?, ?, ?)` + eventSinkDB = "events.db" fallbackName = "unknown" fallbackEmail = "unknown@unknown.com" @@ -78,172 +24,158 @@ const ( gcmEncAlgo = "GCM" ) +type eventWithNames struct { + activity.Event + InitiatorName string + InitiatorEmail string + TargetName string + TargetEmail string +} + // Store is the implementation of the activity.Store interface backed by SQLite type Store struct { - db *sql.DB + db *gorm.DB fieldEncrypt *FieldEncrypt - - insertStatement *sql.Stmt - selectAscStatement *sql.Stmt - selectDescStatement *sql.Stmt - deleteUserStmt *sql.Stmt } // NewSQLiteStore creates a new Store with an event table if not exists. func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) { - dbFile := filepath.Join(dataDir, eventSinkDB) - db, err := sql.Open("sqlite3", dbFile) - if err != nil { - return nil, err - } - db.SetMaxOpenConns(runtime.NumCPU()) - crypt, err := NewFieldEncrypt(encryptionKey) if err != nil { - _ = db.Close() + return nil, err } + dbFile := filepath.Join(dataDir, eventSinkDB) + db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + return nil, err + } + + sql, err := db.DB() + if err != nil { + return nil, err + } + sql.SetMaxOpenConns(1) + if err = migrate(ctx, crypt, db); err != nil { - _ = db.Close() return nil, fmt.Errorf("events database migration: %w", err) } - return createStore(crypt, db) + err = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{}) + if err != nil { + return nil, fmt.Errorf("events auto migrate: %w", err) + } + + return &Store{ + db: db, + fieldEncrypt: crypt, + }, nil } -func (store *Store) processResult(ctx context.Context, result *sql.Rows) ([]*activity.Event, error) { - events := make([]*activity.Event, 0) +func (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) { + activityEvents := make([]*activity.Event, 0) var cryptErr error - for result.Next() { - var id int64 - var operation activity.Activity - var timestamp time.Time - var initiator string - var initiatorName *string - var initiatorEmail *string - var target string - var targetUserName *string - var targetEmail *string - var account string - var jsonMeta string - err := result.Scan(&id, &operation, ×tamp, &initiator, &initiatorName, &initiatorEmail, &target, &targetUserName, &targetEmail, &account, &jsonMeta) - if err != nil { - return nil, err + + for _, event := range events { + e := event.Event + if e.Meta == nil { + e.Meta = make(map[string]any) } - meta := make(map[string]any) - if jsonMeta != "" { - err = json.Unmarshal([]byte(jsonMeta), &meta) + if event.TargetName != "" { + name, err := store.fieldEncrypt.Decrypt(event.TargetName) if err != nil { - return nil, err - } - } - - if targetUserName != nil { - name, err := store.fieldEncrypt.Decrypt(*targetUserName) - if err != nil { - cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", target) - meta["username"] = fallbackName + cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", event.TargetName) + e.Meta["username"] = fallbackName } else { - meta["username"] = name + e.Meta["username"] = name } } - if targetEmail != nil { - email, err := store.fieldEncrypt.Decrypt(*targetEmail) + if event.TargetEmail != "" { + email, err := store.fieldEncrypt.Decrypt(event.TargetEmail) if err != nil { - cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", target) - meta["email"] = fallbackEmail + cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", event.TargetEmail) + e.Meta["email"] = fallbackEmail } else { - meta["email"] = email + e.Meta["email"] = email } } - event := &activity.Event{ - Timestamp: timestamp, - Activity: operation, - ID: uint64(id), - InitiatorID: initiator, - TargetID: target, - AccountID: account, - Meta: meta, - } - - if initiatorName != nil { - name, err := store.fieldEncrypt.Decrypt(*initiatorName) + if event.InitiatorName != "" { + name, err := store.fieldEncrypt.Decrypt(event.InitiatorName) if err != nil { - cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", initiator) - event.InitiatorName = fallbackName + cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", event.InitiatorName) + e.InitiatorName = fallbackName } else { - event.InitiatorName = name + e.InitiatorName = name } } - if initiatorEmail != nil { - email, err := store.fieldEncrypt.Decrypt(*initiatorEmail) + if event.InitiatorEmail != "" { + email, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail) if err != nil { - cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", initiator) - event.InitiatorEmail = fallbackEmail + cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", event.InitiatorEmail) + e.InitiatorEmail = fallbackEmail } else { - event.InitiatorEmail = email + e.InitiatorEmail = email } } - events = append(events, event) + activityEvents = append(activityEvents, &e) } if cryptErr != nil { log.WithContext(ctx).Warnf("%s", cryptErr) } - return events, nil + return activityEvents, nil } // Get returns "limit" number of events from index ordered descending or ascending by a timestamp func (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) { - stmt := store.selectDescStatement + baseQuery := store.db.Model(&activity.Event{}). + Select(` + events.*, + u.name AS initiator_name, + u.email AS initiator_email, + t.name AS target_name, + t.email AS target_email + `). + Joins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`). + Joins(`LEFT JOIN deleted_users t ON t.id = events.target_id`) + + orderDir := "DESC" if !descending { - stmt = store.selectAscStatement + orderDir = "ASC" } - result, err := stmt.Query(accountID, limit, offset) + var events []*eventWithNames + err := baseQuery.Order("events.timestamp "+orderDir).Offset(offset).Limit(limit). + Find(&events, "account_id = ?", accountID).Error if err != nil { return nil, err } - defer result.Close() //nolint - return store.processResult(ctx, result) + return store.processResult(ctx, events) } // Save an event in the SQLite events table end encrypt the "email" element in meta map func (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) { - var jsonMeta string - meta, err := store.saveDeletedUserEmailAndNameInEncrypted(event) - if err != nil { - return nil, err - } - - if meta != nil { - metaBytes, err := json.Marshal(event.Meta) - if err != nil { - return nil, err - } - jsonMeta = string(metaBytes) - } - - result, err := store.insertStatement.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID, jsonMeta) - if err != nil { - return nil, err - } - - id, err := result.LastInsertId() - if err != nil { - return nil, err - } - eventCopy := event.Copy() - eventCopy.ID = uint64(id) + meta, err := store.saveDeletedUserEmailAndNameInEncrypted(eventCopy) + if err != nil { + return nil, err + } + eventCopy.Meta = meta + + if err = store.db.Create(eventCopy).Error; err != nil { + return nil, err + } + return eventCopy, nil } @@ -260,16 +192,27 @@ func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event return event.Meta, nil } + deletedUser := activity.DeletedUser{ + ID: event.TargetID, + EncAlgo: gcmEncAlgo, + } + encryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", email)) if err != nil { return nil, err } + deletedUser.Email = encryptedEmail + encryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", name)) if err != nil { return nil, err } + deletedUser.Name = encryptedName - _, err = store.deleteUserStmt.Exec(event.TargetID, encryptedEmail, encryptedName, gcmEncAlgo) + err = store.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"email", "name"}), + }).Create(deletedUser).Error if err != nil { return nil, err } @@ -285,75 +228,11 @@ func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event // Close the Store func (store *Store) Close(_ context.Context) error { if store.db != nil { - return store.db.Close() + sql, err := store.db.DB() + if err != nil { + return err + } + return sql.Close() } return nil } - -// createStore initializes and returns a new Store instance with prepared SQL statements. -func createStore(crypt *FieldEncrypt, db *sql.DB) (*Store, error) { - insertStmt, err := db.Prepare(insertQuery) - if err != nil { - _ = db.Close() - return nil, err - } - - selectDescStmt, err := db.Prepare(selectDescQuery) - if err != nil { - _ = db.Close() - return nil, err - } - - selectAscStmt, err := db.Prepare(selectAscQuery) - if err != nil { - _ = db.Close() - return nil, err - } - - deleteUserStmt, err := db.Prepare(insertDeleteUserQuery) - if err != nil { - _ = db.Close() - return nil, err - } - - return &Store{ - db: db, - fieldEncrypt: crypt, - insertStatement: insertStmt, - selectDescStatement: selectDescStmt, - selectAscStatement: selectAscStmt, - deleteUserStmt: deleteUserStmt, - }, nil -} - -// checkColumnExists checks if a column exists in a specified table -func checkColumnExists(db *sql.DB, tableName, columnName string) (bool, error) { - query := fmt.Sprintf("PRAGMA table_info(%s);", tableName) - rows, err := db.Query(query) - if err != nil { - return false, fmt.Errorf("failed to query table info: %w", err) - } - defer rows.Close() - - for rows.Next() { - var cid int - var name, ctype string - var notnull, pk int - var dfltValue sql.NullString - - err = rows.Scan(&cid, &name, &ctype, ¬null, &dfltValue, &pk) - if err != nil { - return false, fmt.Errorf("failed to scan row: %w", err) - } - - if name == columnName { - return true, nil - } - } - - if err = rows.Err(); err != nil { - return false, err - } - - return false, nil -} diff --git a/management/server/event.go b/management/server/event.go index 6342bfedb..2952edc8c 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -66,7 +66,7 @@ func (am *DefaultAccountManager) StoreEvent(ctx context.Context, initiatorID, ta go func() { _, err := am.eventStore.Save(ctx, &activity.Event{ Timestamp: time.Now().UTC(), - Activity: activityID, + Activity: activityID.(activity.Activity), InitiatorID: initiatorID, TargetID: targetID, AccountID: accountID, From daa8380df9aca630543613669f2f81e380b49082 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 21 May 2025 11:12:28 +0200 Subject: [PATCH 021/213] [client] Feature/lazy connection (#3379) With the lazy connection feature, the peer will connect to target peers on-demand. The trigger can be any IP traffic. This feature can be enabled with the NB_ENABLE_EXPERIMENTAL_LAZY_CONN environment variable. When the engine receives a network map, it binds a free UDP port for every remote peer, and the system configures WireGuard endpoints for these ports. When traffic appears on a UDP socket, the system removes this listener and starts the peer connection procedure immediately. Key changes Fix slow netbird status -d command Move from engine.go file to conn_mgr.go the peer connection related code Refactor the iface interface usage and moved interface file next to the engine code Add new command line flag and UI option to enable feature The peer.Conn struct is reusable after it has been closed. Change connection states Connection states Idle: The peer is not attempting to establish a connection. This typically means it's in a lazy state or the remote peer is expired. Connecting: The peer is actively trying to establish a connection. This occurs when the peer has entered an active state and is continuously attempting to reach the remote peer. Connected: A successful peer-to-peer connection has been established and communication is active. --- client/cmd/root.go | 35 +- client/cmd/status.go | 6 +- client/cmd/up.go | 8 + client/iface/configurer/kernel_unix.go | 32 +- client/iface/configurer/usp.go | 144 +-- client/iface/configurer/usp_test.go | 79 +- client/iface/device/interface.go | 2 +- client/iface/iface.go | 6 +- client/internal/config.go | 10 + client/internal/conn_mgr.go | 303 +++++ client/internal/connect.go | 5 +- client/internal/debug/debug.go | 1 + client/internal/dns/wgiface.go | 2 - client/internal/dns/wgiface_windows.go | 2 - client/internal/engine.go | 200 ++-- client/internal/engine_test.go | 21 +- client/internal/iface_common.go | 2 +- .../internal/lazyconn/activity/listen_ip.go | 9 + .../lazyconn/activity/listen_ip_linux.go | 10 + client/internal/lazyconn/activity/listener.go | 106 ++ .../lazyconn/activity/listener_test.go | 41 + client/internal/lazyconn/activity/manager.go | 95 ++ .../lazyconn/activity/manager_test.go | 162 +++ client/internal/lazyconn/doc.go | 32 + client/internal/lazyconn/env.go | 26 + .../lazyconn/inactivity/inactivity.go | 70 ++ .../lazyconn/inactivity/inactivity_test.go | 156 +++ client/internal/lazyconn/manager/manager.go | 404 +++++++ client/internal/lazyconn/peercfg.go | 16 + client/internal/lazyconn/support.go | 41 + client/internal/lazyconn/support_test.go | 31 + client/internal/lazyconn/wgiface.go | 14 + client/internal/peer/conn.go | 383 +++--- client/internal/peer/conn_status.go | 41 +- client/internal/peer/conn_status_test.go | 3 +- client/internal/peer/conn_test.go | 65 +- client/internal/peer/conntype/priority.go | 29 + client/internal/peer/dispatcher/dispatcher.go | 52 + client/internal/peer/guard/guard.go | 56 +- client/internal/peer/handshaker.go | 12 +- client/internal/peer/id/connid.go | 5 + client/internal/peer/iface.go | 2 +- client/internal/peer/status.go | 52 +- client/internal/peer/status_test.go | 8 +- client/internal/peer/wg_watcher.go | 9 +- client/internal/peer/wg_watcher_test.go | 21 +- client/internal/peer/worker/state.go | 55 + client/internal/peer/worker_ice.go | 7 +- client/internal/peerstore/store.go | 27 + .../routemanager/iface/iface_common.go | 2 - client/proto/daemon.pb.go | 1028 +++++++++-------- client/proto/daemon.proto | 3 + client/server/server.go | 7 + client/status/status.go | 25 +- client/status/status_test.go | 6 +- client/ui/client_ui.go | 206 ++-- client/ui/const.go | 1 + management/proto/management.pb.go | 572 ++++----- management/proto/management.proto | 3 + management/server/grpcserver.go | 9 +- 60 files changed, 3274 insertions(+), 1486 deletions(-) create mode 100644 client/internal/conn_mgr.go create mode 100644 client/internal/lazyconn/activity/listen_ip.go create mode 100644 client/internal/lazyconn/activity/listen_ip_linux.go create mode 100644 client/internal/lazyconn/activity/listener.go create mode 100644 client/internal/lazyconn/activity/listener_test.go create mode 100644 client/internal/lazyconn/activity/manager.go create mode 100644 client/internal/lazyconn/activity/manager_test.go create mode 100644 client/internal/lazyconn/doc.go create mode 100644 client/internal/lazyconn/env.go create mode 100644 client/internal/lazyconn/inactivity/inactivity.go create mode 100644 client/internal/lazyconn/inactivity/inactivity_test.go create mode 100644 client/internal/lazyconn/manager/manager.go create mode 100644 client/internal/lazyconn/peercfg.go create mode 100644 client/internal/lazyconn/support.go create mode 100644 client/internal/lazyconn/support_test.go create mode 100644 client/internal/lazyconn/wgiface.go create mode 100644 client/internal/peer/conntype/priority.go create mode 100644 client/internal/peer/dispatcher/dispatcher.go create mode 100644 client/internal/peer/id/connid.go create mode 100644 client/internal/peer/worker/state.go diff --git a/client/cmd/root.go b/client/cmd/root.go index b57bee230..9bcf65df9 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -26,22 +26,23 @@ import ( ) const ( - externalIPMapFlag = "external-ip-map" - dnsResolverAddress = "dns-resolver-address" - enableRosenpassFlag = "enable-rosenpass" - rosenpassPermissiveFlag = "rosenpass-permissive" - preSharedKeyFlag = "preshared-key" - interfaceNameFlag = "interface-name" - wireguardPortFlag = "wireguard-port" - networkMonitorFlag = "network-monitor" - disableAutoConnectFlag = "disable-auto-connect" - serverSSHAllowedFlag = "allow-server-ssh" - extraIFaceBlackListFlag = "extra-iface-blacklist" - dnsRouteIntervalFlag = "dns-router-interval" - systemInfoFlag = "system-info" - blockLANAccessFlag = "block-lan-access" - uploadBundle = "upload-bundle" - uploadBundleURL = "upload-bundle-url" + externalIPMapFlag = "external-ip-map" + dnsResolverAddress = "dns-resolver-address" + enableRosenpassFlag = "enable-rosenpass" + rosenpassPermissiveFlag = "rosenpass-permissive" + preSharedKeyFlag = "preshared-key" + interfaceNameFlag = "interface-name" + wireguardPortFlag = "wireguard-port" + networkMonitorFlag = "network-monitor" + disableAutoConnectFlag = "disable-auto-connect" + serverSSHAllowedFlag = "allow-server-ssh" + extraIFaceBlackListFlag = "extra-iface-blacklist" + dnsRouteIntervalFlag = "dns-router-interval" + systemInfoFlag = "system-info" + blockLANAccessFlag = "block-lan-access" + enableLazyConnectionFlag = "enable-lazy-connection" + uploadBundle = "upload-bundle" + uploadBundleURL = "upload-bundle-url" ) var ( @@ -80,6 +81,7 @@ var ( blockLANAccess bool debugUploadBundle bool debugUploadBundleURL string + lazyConnEnabled bool rootCmd = &cobra.Command{ Use: "netbird", @@ -184,6 +186,7 @@ func init() { upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.") upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted") upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.") + upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand.") debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle") debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL)) diff --git a/client/cmd/status.go b/client/cmd/status.go index 0ddba8b2f..e466f73ab 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -44,7 +44,7 @@ func init() { statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4") statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200") statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud") - statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected") + statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected") } func statusFunc(cmd *cobra.Command, args []string) error { @@ -127,12 +127,12 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) { func parseFilters() error { switch strings.ToLower(statusFilter) { - case "", "disconnected", "connected": + case "", "idle", "connecting", "connected": if strings.ToLower(statusFilter) != "" { enableDetailFlagWhenFilterFlag() } default: - return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter) + return fmt.Errorf("wrong status filter, should be one of connected|connecting|idle, got: %s", statusFilter) } if len(ipsFilter) > 0 { diff --git a/client/cmd/up.go b/client/cmd/up.go index 8cc272834..2dcf2282b 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -194,6 +194,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { ic.BlockLANAccess = &blockLANAccess } + if cmd.Flag(enableLazyConnectionFlag).Changed { + ic.LazyConnectionEnabled = &lazyConnEnabled + } + providedSetupKey, err := getSetupKey() if err != nil { return err @@ -332,6 +336,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { loginRequest.BlockLanAccess = &blockLANAccess } + if cmd.Flag(enableLazyConnectionFlag).Changed { + loginRequest.LazyConnectionEnabled = &lazyConnEnabled + } + var loginErr error var loginResp *proto.LoginResponse diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go index 6f09a63c9..87076fea8 100644 --- a/client/iface/configurer/kernel_unix.go +++ b/client/iface/configurer/kernel_unix.go @@ -201,14 +201,30 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error { func (c *KernelConfigurer) Close() { } -func (c *KernelConfigurer) GetStats(peerKey string) (WGStats, error) { - peer, err := c.getPeer(c.deviceName, peerKey) +func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) { + stats := make(map[string]WGStats) + wg, err := wgctrl.New() if err != nil { - return WGStats{}, fmt.Errorf("get wireguard stats: %w", err) + return nil, fmt.Errorf("wgctl: %w", err) } - return WGStats{ - LastHandshake: peer.LastHandshakeTime, - TxBytes: peer.TransmitBytes, - RxBytes: peer.ReceiveBytes, - }, nil + defer func() { + err = wg.Close() + if err != nil { + log.Errorf("Got error while closing wgctl: %v", err) + } + }() + + wgDevice, err := wg.Device(c.deviceName) + if err != nil { + return nil, fmt.Errorf("get device %s: %w", c.deviceName, err) + } + + for _, peer := range wgDevice.Peers { + stats[peer.PublicKey.String()] = WGStats{ + LastHandshake: peer.LastHandshakeTime, + TxBytes: peer.TransmitBytes, + RxBytes: peer.ReceiveBytes, + } + } + return stats, nil } diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index e536f2650..d7ab1ec6f 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -1,6 +1,7 @@ package configurer import ( + "encoding/base64" "encoding/hex" "fmt" "net" @@ -17,6 +18,13 @@ import ( nbnet "github.com/netbirdio/netbird/util/net" ) +const ( + ipcKeyLastHandshakeTimeSec = "last_handshake_time_sec" + ipcKeyLastHandshakeTimeNsec = "last_handshake_time_nsec" + ipcKeyTxBytes = "tx_bytes" + ipcKeyRxBytes = "rx_bytes" +) + var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found") type WGUSPConfigurer struct { @@ -217,91 +225,75 @@ func (t *WGUSPConfigurer) Close() { } } -func (t *WGUSPConfigurer) GetStats(peerKey string) (WGStats, error) { +func (t *WGUSPConfigurer) GetStats() (map[string]WGStats, error) { ipc, err := t.device.IpcGet() if err != nil { - return WGStats{}, fmt.Errorf("ipc get: %w", err) + return nil, fmt.Errorf("ipc get: %w", err) } - stats, err := findPeerInfo(ipc, peerKey, []string{ - "last_handshake_time_sec", - "last_handshake_time_nsec", - "tx_bytes", - "rx_bytes", - }) - if err != nil { - return WGStats{}, fmt.Errorf("find peer info: %w", err) - } - - sec, err := strconv.ParseInt(stats["last_handshake_time_sec"], 10, 64) - if err != nil { - return WGStats{}, fmt.Errorf("parse handshake sec: %w", err) - } - nsec, err := strconv.ParseInt(stats["last_handshake_time_nsec"], 10, 64) - if err != nil { - return WGStats{}, fmt.Errorf("parse handshake nsec: %w", err) - } - txBytes, err := strconv.ParseInt(stats["tx_bytes"], 10, 64) - if err != nil { - return WGStats{}, fmt.Errorf("parse tx_bytes: %w", err) - } - rxBytes, err := strconv.ParseInt(stats["rx_bytes"], 10, 64) - if err != nil { - return WGStats{}, fmt.Errorf("parse rx_bytes: %w", err) - } - - return WGStats{ - LastHandshake: time.Unix(sec, nsec), - TxBytes: txBytes, - RxBytes: rxBytes, - }, nil + return parseTransfers(ipc) } -func findPeerInfo(ipcInput string, peerKey string, searchConfigKeys []string) (map[string]string, error) { - peerKeyParsed, err := wgtypes.ParseKey(peerKey) - if err != nil { - return nil, fmt.Errorf("parse key: %w", err) - } - - hexKey := hex.EncodeToString(peerKeyParsed[:]) - - lines := strings.Split(ipcInput, "\n") - - configFound := map[string]string{} - foundPeer := false +func parseTransfers(ipc string) (map[string]WGStats, error) { + stats := make(map[string]WGStats) + var ( + currentKey string + currentStats WGStats + hasPeer bool + ) + lines := strings.Split(ipc, "\n") for _, line := range lines { line = strings.TrimSpace(line) // If we're within the details of the found peer and encounter another public key, // this means we're starting another peer's details. So, stop. - if strings.HasPrefix(line, "public_key=") && foundPeer { - break - } - - // Identify the peer with the specific public key - if line == fmt.Sprintf("public_key=%s", hexKey) { - foundPeer = true - } - - for _, key := range searchConfigKeys { - if foundPeer && strings.HasPrefix(line, key+"=") { - v := strings.SplitN(line, "=", 2) - configFound[v[0]] = v[1] + if strings.HasPrefix(line, "public_key=") { + peerID := strings.TrimPrefix(line, "public_key=") + h, err := hex.DecodeString(peerID) + if err != nil { + return nil, fmt.Errorf("decode peerID: %w", err) } + currentKey = base64.StdEncoding.EncodeToString(h) + currentStats = WGStats{} // Reset stats for the new peer + hasPeer = true + stats[currentKey] = currentStats + continue + } + + if !hasPeer { + continue + } + + key := strings.SplitN(line, "=", 2) + if len(key) != 2 { + continue + } + switch key[0] { + case ipcKeyLastHandshakeTimeSec: + hs, err := toLastHandshake(key[1]) + if err != nil { + return nil, err + } + currentStats.LastHandshake = hs + stats[currentKey] = currentStats + case ipcKeyRxBytes: + rxBytes, err := toBytes(key[1]) + if err != nil { + return nil, fmt.Errorf("parse rx_bytes: %w", err) + } + currentStats.RxBytes = rxBytes + stats[currentKey] = currentStats + case ipcKeyTxBytes: + TxBytes, err := toBytes(key[1]) + if err != nil { + return nil, fmt.Errorf("parse tx_bytes: %w", err) + } + currentStats.TxBytes = TxBytes + stats[currentKey] = currentStats } } - // todo: use multierr - for _, key := range searchConfigKeys { - if _, ok := configFound[key]; !ok { - return configFound, fmt.Errorf("config key not found: %s", key) - } - } - if !foundPeer { - return nil, fmt.Errorf("%w: %s", ErrPeerNotFound, peerKey) - } - - return configFound, nil + return stats, nil } func toWgUserspaceString(wgCfg wgtypes.Config) string { @@ -355,6 +347,18 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string { return sb.String() } +func toLastHandshake(stringVar string) (time.Time, error) { + sec, err := strconv.ParseInt(stringVar, 10, 64) + if err != nil { + return time.Time{}, fmt.Errorf("parse handshake sec: %w", err) + } + return time.Unix(sec, 0), nil +} + +func toBytes(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + func getFwmark() int { if nbnet.AdvancedRouting() { return nbnet.ControlPlaneMark diff --git a/client/iface/configurer/usp_test.go b/client/iface/configurer/usp_test.go index 775339f24..e32491c54 100644 --- a/client/iface/configurer/usp_test.go +++ b/client/iface/configurer/usp_test.go @@ -2,10 +2,8 @@ package configurer import ( "encoding/hex" - "fmt" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) @@ -34,58 +32,35 @@ errno=0 ` -func Test_findPeerInfo(t *testing.T) { +func Test_parseTransfers(t *testing.T) { tests := []struct { - name string - peerKey string - searchKeys []string - want map[string]string - wantErr bool + name string + peerKey string + want WGStats }{ { - name: "single", - peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376", - searchKeys: []string{"tx_bytes"}, - want: map[string]string{ - "tx_bytes": "38333", + name: "single", + peerKey: "b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33", + want: WGStats{ + TxBytes: 0, + RxBytes: 0, }, - wantErr: false, }, { - name: "multiple", - peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376", - searchKeys: []string{"tx_bytes", "rx_bytes"}, - want: map[string]string{ - "tx_bytes": "38333", - "rx_bytes": "2224", + name: "multiple", + peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376", + want: WGStats{ + TxBytes: 38333, + RxBytes: 2224, }, - wantErr: false, }, { - name: "lastpeer", - peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58", - searchKeys: []string{"tx_bytes", "rx_bytes"}, - want: map[string]string{ - "tx_bytes": "1212111", - "rx_bytes": "1929999999", + name: "lastpeer", + peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58", + want: WGStats{ + TxBytes: 1212111, + RxBytes: 1929999999, }, - wantErr: false, - }, - { - name: "peer not found", - peerKey: "1111111111111111111111111111111111111111111111111111111111111111", - searchKeys: nil, - want: nil, - wantErr: true, - }, - { - name: "key not found", - peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58", - searchKeys: []string{"tx_bytes", "unknown_key"}, - want: map[string]string{ - "tx_bytes": "1212111", - }, - wantErr: true, }, } for _, tt := range tests { @@ -96,9 +71,19 @@ func Test_findPeerInfo(t *testing.T) { key, err := wgtypes.NewKey(res) require.NoError(t, err) - got, err := findPeerInfo(ipcFixture, key.String(), tt.searchKeys) - assert.Equalf(t, tt.wantErr, err != nil, fmt.Sprintf("findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys)) - assert.Equalf(t, tt.want, got, "findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys) + stats, err := parseTransfers(ipcFixture) + if err != nil { + require.NoError(t, err) + return + } + + stat, ok := stats[key.String()] + if !ok { + require.True(t, ok) + return + } + + require.Equal(t, tt.want, stat) }) } } diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go index 6971b6946..a1d44a150 100644 --- a/client/iface/device/interface.go +++ b/client/iface/device/interface.go @@ -16,5 +16,5 @@ type WGConfigurer interface { AddAllowedIP(peerKey string, allowedIP string) error RemoveAllowedIP(peerKey string, allowedIP string) error Close() - GetStats(peerKey string) (configurer.WGStats, error) + GetStats() (map[string]configurer.WGStats, error) } diff --git a/client/iface/iface.go b/client/iface/iface.go index 9d5262aed..c78a252da 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -212,9 +212,9 @@ func (w *WGIface) GetWGDevice() *wgdevice.Device { return w.tun.Device() } -// GetStats returns the last handshake time, rx and tx bytes for the given peer -func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) { - return w.configurer.GetStats(peerKey) +// GetStats returns the last handshake time, rx and tx bytes +func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) { + return w.configurer.GetStats() } func (w *WGIface) waitUntilRemoved() error { diff --git a/client/internal/config.go b/client/internal/config.go index b2f96cbdc..86dd7ebb1 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -74,6 +74,8 @@ type ConfigInput struct { DisableNotifications *bool DNSLabels domain.List + + LazyConnectionEnabled *bool } // Config Configuration type @@ -138,6 +140,8 @@ type Config struct { ClientCertKeyPath string ClientCertKeyPair *tls.Certificate `json:"-"` + + LazyConnectionEnabled bool } // ReadConfig read config file and return with Config. If it is not exists create a new with default values @@ -524,6 +528,12 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.LazyConnectionEnabled != nil && *input.LazyConnectionEnabled != config.LazyConnectionEnabled { + log.Infof("switching lazy connection to %t", *input.LazyConnectionEnabled) + config.LazyConnectionEnabled = *input.LazyConnectionEnabled + updated = true + } + return updated, nil } diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go new file mode 100644 index 000000000..119ddc1bd --- /dev/null +++ b/client/internal/conn_mgr.go @@ -0,0 +1,303 @@ +package internal + +import ( + "context" + "os" + "strconv" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/lazyconn" + "github.com/netbirdio/netbird/client/internal/lazyconn/manager" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/peer/dispatcher" + "github.com/netbirdio/netbird/client/internal/peerstore" +) + +// ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections. +// +// The connection manager is responsible for: +// - Managing lazy connections via the lazyConnManager +// - Maintaining a list of excluded peers that should always have permanent connections +// - Handling connection establishment based on peer signaling +// +// The implementation is not thread-safe; it is protected by engine.syncMsgMux. +type ConnMgr struct { + peerStore *peerstore.Store + statusRecorder *peer.Status + iface lazyconn.WGIface + dispatcher *dispatcher.ConnectionDispatcher + enabledLocally bool + + lazyConnMgr *manager.Manager + + wg sync.WaitGroup + ctx context.Context + ctxCancel context.CancelFunc +} + +func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface, dispatcher *dispatcher.ConnectionDispatcher) *ConnMgr { + e := &ConnMgr{ + peerStore: peerStore, + statusRecorder: statusRecorder, + iface: iface, + dispatcher: dispatcher, + } + if engineConfig.LazyConnectionEnabled || lazyconn.IsLazyConnEnabledByEnv() { + e.enabledLocally = true + } + return e +} + +// Start initializes the connection manager and starts the lazy connection manager if enabled by env var or cmd line option. +func (e *ConnMgr) Start(ctx context.Context) { + if e.lazyConnMgr != nil { + log.Errorf("lazy connection manager is already started") + return + } + + if !e.enabledLocally { + log.Infof("lazy connection manager is disabled") + return + } + + e.initLazyManager(ctx) + e.statusRecorder.UpdateLazyConnection(true) +} + +// UpdatedRemoteFeatureFlag is called when the remote feature flag is updated. +// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again. +// If disabled, then it closes the lazy connection manager and open the connections to all peers. +func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error { + // do not disable lazy connection manager if it was enabled by env var + if e.enabledLocally { + return nil + } + + if enabled { + // if the lazy connection manager is already started, do not start it again + if e.lazyConnMgr != nil { + return nil + } + + log.Infof("lazy connection manager is enabled by management feature flag") + e.initLazyManager(ctx) + e.statusRecorder.UpdateLazyConnection(true) + return e.addPeersToLazyConnManager(ctx) + } else { + if e.lazyConnMgr == nil { + return nil + } + log.Infof("lazy connection manager is disabled by management feature flag") + e.closeManager(ctx) + e.statusRecorder.UpdateLazyConnection(false) + return nil + } +} + +// SetExcludeList sets the list of peer IDs that should always have permanent connections. +func (e *ConnMgr) SetExcludeList(peerIDs []string) { + if e.lazyConnMgr == nil { + return + } + + excludedPeers := make([]lazyconn.PeerConfig, 0, len(peerIDs)) + + for _, peerID := range peerIDs { + var peerConn *peer.Conn + var exists bool + if peerConn, exists = e.peerStore.PeerConn(peerID); !exists { + log.Warnf("failed to find peer conn for peerID: %s", peerID) + continue + } + + lazyPeerCfg := lazyconn.PeerConfig{ + PublicKey: peerID, + AllowedIPs: peerConn.WgConfig().AllowedIps, + PeerConnID: peerConn.ConnID(), + Log: peerConn.Log, + } + excludedPeers = append(excludedPeers, lazyPeerCfg) + } + + added := e.lazyConnMgr.ExcludePeer(e.ctx, excludedPeers) + for _, peerID := range added { + var peerConn *peer.Conn + var exists bool + if peerConn, exists = e.peerStore.PeerConn(peerID); !exists { + // if the peer not exist in the store, it means that the engine will call the AddPeerConn in next step + continue + } + + peerConn.Log.Infof("peer has been added to lazy connection exclude list, opening permanent connection") + if err := peerConn.Open(e.ctx); err != nil { + peerConn.Log.Errorf("failed to open connection: %v", err) + } + } +} + +func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Conn) (exists bool) { + if success := e.peerStore.AddPeerConn(peerKey, conn); !success { + return true + } + + if !e.isStartedWithLazyMgr() { + if err := conn.Open(ctx); err != nil { + conn.Log.Errorf("failed to open connection: %v", err) + } + return + } + + if !lazyconn.IsSupported(conn.AgentVersionString()) { + conn.Log.Warnf("peer does not support lazy connection (%s), open permanent connection", conn.AgentVersionString()) + if err := conn.Open(ctx); err != nil { + conn.Log.Errorf("failed to open connection: %v", err) + } + return + } + + lazyPeerCfg := lazyconn.PeerConfig{ + PublicKey: peerKey, + AllowedIPs: conn.WgConfig().AllowedIps, + PeerConnID: conn.ConnID(), + Log: conn.Log, + } + excluded, err := e.lazyConnMgr.AddPeer(lazyPeerCfg) + if err != nil { + conn.Log.Errorf("failed to add peer to lazyconn manager: %v", err) + if err := conn.Open(ctx); err != nil { + conn.Log.Errorf("failed to open connection: %v", err) + } + return + } + + if excluded { + conn.Log.Infof("peer is on lazy conn manager exclude list, opening connection") + if err := conn.Open(ctx); err != nil { + conn.Log.Errorf("failed to open connection: %v", err) + } + return + } + + conn.Log.Infof("peer added to lazy conn manager") + return +} + +func (e *ConnMgr) RemovePeerConn(peerKey string) { + conn, ok := e.peerStore.Remove(peerKey) + if !ok { + return + } + defer conn.Close() + + if !e.isStartedWithLazyMgr() { + return + } + + e.lazyConnMgr.RemovePeer(peerKey) + conn.Log.Infof("removed peer from lazy conn manager") +} + +func (e *ConnMgr) OnSignalMsg(ctx context.Context, peerKey string) (*peer.Conn, bool) { + conn, ok := e.peerStore.PeerConn(peerKey) + if !ok { + return nil, false + } + + if !e.isStartedWithLazyMgr() { + return conn, true + } + + if found := e.lazyConnMgr.ActivatePeer(ctx, peerKey); found { + conn.Log.Infof("activated peer from inactive state") + if err := conn.Open(e.ctx); err != nil { + conn.Log.Errorf("failed to open connection: %v", err) + } + } + return conn, true +} + +func (e *ConnMgr) Close() { + if !e.isStartedWithLazyMgr() { + return + } + + e.ctxCancel() + e.wg.Wait() + e.lazyConnMgr = nil +} + +func (e *ConnMgr) initLazyManager(parentCtx context.Context) { + cfg := manager.Config{ + InactivityThreshold: inactivityThresholdEnv(), + } + e.lazyConnMgr = manager.NewManager(cfg, e.peerStore, e.iface, e.dispatcher) + + ctx, cancel := context.WithCancel(parentCtx) + e.ctx = ctx + e.ctxCancel = cancel + + e.wg.Add(1) + go func() { + defer e.wg.Done() + e.lazyConnMgr.Start(ctx) + }() +} + +func (e *ConnMgr) addPeersToLazyConnManager(ctx context.Context) error { + peers := e.peerStore.PeersPubKey() + lazyPeerCfgs := make([]lazyconn.PeerConfig, 0, len(peers)) + for _, peerID := range peers { + var peerConn *peer.Conn + var exists bool + if peerConn, exists = e.peerStore.PeerConn(peerID); !exists { + log.Warnf("failed to find peer conn for peerID: %s", peerID) + continue + } + + lazyPeerCfg := lazyconn.PeerConfig{ + PublicKey: peerID, + AllowedIPs: peerConn.WgConfig().AllowedIps, + PeerConnID: peerConn.ConnID(), + Log: peerConn.Log, + } + lazyPeerCfgs = append(lazyPeerCfgs, lazyPeerCfg) + } + + return e.lazyConnMgr.AddActivePeers(ctx, lazyPeerCfgs) +} + +func (e *ConnMgr) closeManager(ctx context.Context) { + if e.lazyConnMgr == nil { + return + } + + e.ctxCancel() + e.wg.Wait() + e.lazyConnMgr = nil + + for _, peerID := range e.peerStore.PeersPubKey() { + e.peerStore.PeerConnOpen(ctx, peerID) + } +} + +func (e *ConnMgr) isStartedWithLazyMgr() bool { + return e.lazyConnMgr != nil && e.ctxCancel != nil +} + +func inactivityThresholdEnv() *time.Duration { + envValue := os.Getenv(lazyconn.EnvInactivityThreshold) + if envValue == "" { + return nil + } + + parsedMinutes, err := strconv.Atoi(envValue) + if err != nil || parsedMinutes <= 0 { + return nil + } + + d := time.Duration(parsedMinutes) * time.Minute + return &d +} diff --git a/client/internal/connect.go b/client/internal/connect.go index 832d58dcd..1428d2656 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -440,7 +440,8 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe DisableDNS: config.DisableDNS, DisableFirewall: config.DisableFirewall, - BlockLANAccess: config.BlockLANAccess, + BlockLANAccess: config.BlockLANAccess, + LazyConnectionEnabled: config.LazyConnectionEnabled, } if config.PreSharedKey != "" { @@ -481,7 +482,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourP return signalClient, nil } -// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc) +// loginToManagement creates Management ServiceDependencies client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc) func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { serverPublicKey, err := client.GetServerPublicKey() diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index e07f981fe..d1941d678 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -376,6 +376,7 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", g.internalConfig.DisableFirewall)) configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess)) + configContent.WriteString(fmt.Sprintf("LazyConnectionEnabled: %v\n", g.internalConfig.LazyConnectionEnabled)) } func (g *BundleGenerator) addProf() (err error) { diff --git a/client/internal/dns/wgiface.go b/client/internal/dns/wgiface.go index c6c1752e5..28e9cebf1 100644 --- a/client/internal/dns/wgiface.go +++ b/client/internal/dns/wgiface.go @@ -5,7 +5,6 @@ package dns import ( "net" - "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" ) @@ -18,5 +17,4 @@ type WGIface interface { IsUserspaceBind() bool GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetStats(peerKey string) (configurer.WGStats, error) } diff --git a/client/internal/dns/wgiface_windows.go b/client/internal/dns/wgiface_windows.go index 74e5c75a5..d1374fd54 100644 --- a/client/internal/dns/wgiface_windows.go +++ b/client/internal/dns/wgiface_windows.go @@ -1,7 +1,6 @@ package dns import ( - "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" ) @@ -13,6 +12,5 @@ type WGIface interface { IsUserspaceBind() bool GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetStats(peerKey string) (configurer.WGStats, error) GetInterfaceGUIDString() (string, error) } diff --git a/client/internal/engine.go b/client/internal/engine.go index b16232883..eefd28225 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -38,6 +38,7 @@ import ( nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/networkmonitor" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/peerstore" @@ -122,6 +123,8 @@ type EngineConfig struct { DisableFirewall bool BlockLANAccess bool + + LazyConnectionEnabled bool } // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers. @@ -134,6 +137,8 @@ type Engine struct { // peerConns is a map that holds all the peers that are known to this peer peerStore *peerstore.Store + connMgr *ConnMgr + beforePeerHook nbnet.AddHookFunc afterPeerHook nbnet.RemoveHookFunc @@ -170,7 +175,8 @@ type Engine struct { sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error) sshServer nbssh.Server - statusRecorder *peer.Status + statusRecorder *peer.Status + peerConnDispatcher *dispatcher.ConnectionDispatcher firewall firewallManager.Manager routeManager routemanager.Manager @@ -262,6 +268,10 @@ func (e *Engine) Stop() error { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() + if e.connMgr != nil { + e.connMgr.Close() + } + // stopping network monitor first to avoid starting the engine again if e.networkMonitor != nil { e.networkMonitor.Stop() @@ -297,8 +307,7 @@ func (e *Engine) Stop() error { e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{}) e.statusRecorder.UpdateRelayStates([]relay.ProbeResult{}) - err := e.removeAllPeers() - if err != nil { + if err := e.removeAllPeers(); err != nil { return fmt.Errorf("failed to remove all peers: %s", err) } @@ -405,8 +414,7 @@ func (e *Engine) Start() error { e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener) - err = e.wgInterfaceCreate() - if err != nil { + if err = e.wgInterfaceCreate(); err != nil { log.Errorf("failed creating tunnel interface %s: [%s]", e.config.WgIfaceName, err.Error()) e.close() return fmt.Errorf("create wg interface: %w", err) @@ -442,6 +450,11 @@ func (e *Engine) Start() error { NATExternalIPs: e.parseNATExternalIPMappings(), } + e.peerConnDispatcher = dispatcher.NewConnectionDispatcher() + + e.connMgr = NewConnMgr(e.config, e.statusRecorder, e.peerStore, wgIface, e.peerConnDispatcher) + e.connMgr.Start(e.ctx) + e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg) e.srWatcher.Start() @@ -450,7 +463,6 @@ func (e *Engine) Start() error { // starting network monitor at the very last to avoid disruptions e.startNetworkMonitor() - return nil } @@ -550,6 +562,16 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { var modified []*mgmProto.RemotePeerConfig for _, p := range peersUpdate { peerPubKey := p.GetWgPubKey() + currentPeer, ok := e.peerStore.PeerConn(peerPubKey) + if !ok { + continue + } + + if currentPeer.AgentVersionString() != p.AgentVersion { + modified = append(modified, p) + continue + } + allowedIPs, ok := e.peerStore.AllowedIPs(peerPubKey) if !ok { continue @@ -559,8 +581,7 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { continue } - err := e.statusRecorder.UpdatePeerFQDN(peerPubKey, p.GetFqdn()) - if err != nil { + if err := e.statusRecorder.UpdatePeerFQDN(peerPubKey, p.GetFqdn()); err != nil { log.Warnf("error updating peer's %s fqdn in the status recorder, got error: %v", peerPubKey, err) } } @@ -621,16 +642,11 @@ func (e *Engine) removePeer(peerKey string) error { e.sshServer.RemoveAuthorizedKey(peerKey) } - defer func() { - err := e.statusRecorder.RemovePeer(peerKey) - if err != nil { - log.Warnf("received error when removing peer %s from status recorder: %v", peerKey, err) - } - }() + e.connMgr.RemovePeerConn(peerKey) - conn, exists := e.peerStore.Remove(peerKey) - if exists { - conn.Close() + err := e.statusRecorder.RemovePeer(peerKey) + if err != nil { + log.Warnf("received error when removing peer %s from status recorder: %v", peerKey, err) } return nil } @@ -952,6 +968,10 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { return nil } + if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, networkMap.GetPeerConfig().GetLazyConnectionEnabled()); err != nil { + log.Errorf("failed to update lazy connection feature flag: %v", err) + } + if e.firewall != nil { if localipfw, ok := e.firewall.(localIpUpdater); ok { if err := localipfw.UpdateLocalIPs(); err != nil { @@ -976,7 +996,8 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { e.updateDNSForwarder(dnsRouteFeatureFlag, fwdEntries) // Ingress forward rules - if err := e.updateForwardRules(networkMap.GetForwardingRules()); err != nil { + forwardingRules, err := e.updateForwardRules(networkMap.GetForwardingRules()) + if err != nil { log.Errorf("failed to update forward rules, err: %v", err) } @@ -1022,6 +1043,10 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { } } + // must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store + excludedLazyPeers := e.toExcludedLazyPeers(routes, forwardingRules, networkMap.GetRemotePeers()) + e.connMgr.SetExcludeList(excludedLazyPeers) + protoDNSConfig := networkMap.GetDNSConfig() if protoDNSConfig == nil { protoDNSConfig = &mgmProto.DNSConfig{} @@ -1155,7 +1180,7 @@ func (e *Engine) updateOfflinePeers(offlinePeers []*mgmProto.RemotePeerConfig) { IP: strings.Join(offlinePeer.GetAllowedIps(), ","), PubKey: offlinePeer.GetWgPubKey(), FQDN: offlinePeer.GetFqdn(), - ConnStatus: peer.StatusDisconnected, + ConnStatus: peer.StatusIdle, ConnStatusUpdate: time.Now(), Mux: new(sync.RWMutex), } @@ -1191,12 +1216,17 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error { peerIPs = append(peerIPs, allowedNetIP) } - conn, err := e.createPeerConn(peerKey, peerIPs) + conn, err := e.createPeerConn(peerKey, peerIPs, peerConfig.AgentVersion) if err != nil { return fmt.Errorf("create peer connection: %w", err) } - if ok := e.peerStore.AddPeerConn(peerKey, conn); !ok { + err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn, peerIPs[0].Addr().String()) + if err != nil { + log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err) + } + + if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn); exists { conn.Close() return fmt.Errorf("peer already exists: %s", peerKey) } @@ -1205,17 +1235,10 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error { conn.AddBeforeAddPeerHook(e.beforePeerHook) conn.AddAfterRemovePeerHook(e.afterPeerHook) } - - err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn) - if err != nil { - log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err) - } - - conn.Open() return nil } -func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer.Conn, error) { +func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentVersion string) (*peer.Conn, error) { log.Debugf("creating peer connection %s", pubKey) wgConfig := peer.WgConfig{ @@ -1229,11 +1252,12 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer // randomize connection timeout timeout := time.Duration(rand.Intn(PeerConnectionTimeoutMax-PeerConnectionTimeoutMin)+PeerConnectionTimeoutMin) * time.Millisecond config := peer.ConnConfig{ - Key: pubKey, - LocalKey: e.config.WgPrivateKey.PublicKey().String(), - Timeout: timeout, - WgConfig: wgConfig, - LocalWgPort: e.config.WgPort, + Key: pubKey, + LocalKey: e.config.WgPrivateKey.PublicKey().String(), + AgentVersion: agentVersion, + Timeout: timeout, + WgConfig: wgConfig, + LocalWgPort: e.config.WgPort, RosenpassConfig: peer.RosenpassConfig{ PubKey: e.getRosenpassPubKey(), Addr: e.getRosenpassAddr(), @@ -1249,7 +1273,16 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer }, } - peerConn, err := peer.NewConn(e.ctx, config, e.statusRecorder, e.signaler, e.mobileDep.IFaceDiscover, e.relayManager, e.srWatcher, e.connSemaphore) + serviceDependencies := peer.ServiceDependencies{ + StatusRecorder: e.statusRecorder, + Signaler: e.signaler, + IFaceDiscover: e.mobileDep.IFaceDiscover, + RelayManager: e.relayManager, + SrWatcher: e.srWatcher, + Semaphore: e.connSemaphore, + PeerConnDispatcher: e.peerConnDispatcher, + } + peerConn, err := peer.NewConn(config, serviceDependencies) if err != nil { return nil, err } @@ -1270,7 +1303,7 @@ func (e *Engine) receiveSignalEvents() { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() - conn, ok := e.peerStore.PeerConn(msg.Key) + conn, ok := e.connMgr.OnSignalMsg(e.ctx, msg.Key) if !ok { return fmt.Errorf("wrongly addressed message %s", msg.Key) } @@ -1578,13 +1611,39 @@ func (e *Engine) getRosenpassAddr() string { // RunHealthProbes executes health checks for Signal, Management, Relay and WireGuard services // and updates the status recorder with the latest states. func (e *Engine) RunHealthProbes() bool { + e.syncMsgMux.Lock() + signalHealthy := e.signal.IsHealthy() log.Debugf("signal health check: healthy=%t", signalHealthy) managementHealthy := e.mgmClient.IsHealthy() log.Debugf("management health check: healthy=%t", managementHealthy) - results := append(e.probeSTUNs(), e.probeTURNs()...) + stuns := slices.Clone(e.STUNs) + turns := slices.Clone(e.TURNs) + + if e.wgInterface != nil { + stats, err := e.wgInterface.GetStats() + if err != nil { + log.Warnf("failed to get wireguard stats: %v", err) + e.syncMsgMux.Unlock() + return false + } + for _, key := range e.peerStore.PeersPubKey() { + // wgStats could be zero value, in which case we just reset the stats + wgStats, ok := stats[key] + if !ok { + continue + } + if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { + log.Debugf("failed to update wg stats for peer %s: %s", key, err) + } + } + } + + e.syncMsgMux.Unlock() + + results := e.probeICE(stuns, turns) e.statusRecorder.UpdateRelayStates(results) relayHealthy := true @@ -1596,37 +1655,16 @@ func (e *Engine) RunHealthProbes() bool { } log.Debugf("relay health check: healthy=%t", relayHealthy) - for _, key := range e.peerStore.PeersPubKey() { - wgStats, err := e.wgInterface.GetStats(key) - if err != nil { - log.Debugf("failed to get wg stats for peer %s: %s", key, err) - continue - } - // wgStats could be zero value, in which case we just reset the stats - if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { - log.Debugf("failed to update wg stats for peer %s: %s", key, err) - } - } - allHealthy := signalHealthy && managementHealthy && relayHealthy log.Debugf("all health checks completed: healthy=%t", allHealthy) return allHealthy } -func (e *Engine) probeSTUNs() []relay.ProbeResult { - e.syncMsgMux.Lock() - stuns := slices.Clone(e.STUNs) - e.syncMsgMux.Unlock() - - return relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns) -} - -func (e *Engine) probeTURNs() []relay.ProbeResult { - e.syncMsgMux.Lock() - turns := slices.Clone(e.TURNs) - e.syncMsgMux.Unlock() - - return relay.ProbeAll(e.ctx, relay.ProbeTURN, turns) +func (e *Engine) probeICE(stuns, turns []*stun.URI) []relay.ProbeResult { + return append( + relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns), + relay.ProbeAll(e.ctx, relay.ProbeSTUN, turns)..., + ) } // restartEngine restarts the engine by cancelling the client context @@ -1813,21 +1851,21 @@ func (e *Engine) Address() (netip.Addr, error) { return ip.Unmap(), nil } -func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) error { +func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewallManager.ForwardRule, error) { if e.firewall == nil { log.Warn("firewall is disabled, not updating forwarding rules") - return nil + return nil, nil } if len(rules) == 0 { if e.ingressGatewayMgr == nil { - return nil + return nil, nil } err := e.ingressGatewayMgr.Close() e.ingressGatewayMgr = nil e.statusRecorder.SetIngressGwMgr(nil) - return err + return nil, err } if e.ingressGatewayMgr == nil { @@ -1878,7 +1916,33 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) error { log.Errorf("failed to update forwarding rules: %v", err) } - return nberrors.FormatErrorOrNil(merr) + return forwardingRules, nberrors.FormatErrorOrNil(merr) +} + +func (e *Engine) toExcludedLazyPeers(routes []*route.Route, rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) []string { + excludedPeers := make([]string, 0) + for _, r := range routes { + if r.Peer == "" { + continue + } + log.Infof("exclude router peer from lazy connection: %s", r.Peer) + excludedPeers = append(excludedPeers, r.Peer) + } + + for _, r := range rules { + ip := r.TranslatedAddress + for _, p := range peers { + for _, allowedIP := range p.GetAllowedIps() { + if allowedIP != ip.String() { + continue + } + log.Infof("exclude forwarder peer from lazy connection: %s", p.GetWgPubKey()) + excludedPeers = append(excludedPeers, p.GetWgPubKey()) + } + } + } + + return excludedPeers } // isChecksEqual checks if two slices of checks are equal. diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 7afe0fcd6..422059bd8 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -28,8 +28,6 @@ import ( "github.com/netbirdio/management-integrations/integrations" - "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -38,6 +36,7 @@ import ( "github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/routemanager" @@ -53,6 +52,7 @@ import ( "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" + "github.com/netbirdio/netbird/management/server/types" relayClient "github.com/netbirdio/netbird/relay/client" "github.com/netbirdio/netbird/route" signal "github.com/netbirdio/netbird/signal/client" @@ -93,7 +93,7 @@ type MockWGIface struct { GetFilterFunc func() device.PacketFilter GetDeviceFunc func() *device.FilteredDevice GetWGDeviceFunc func() *wgdevice.Device - GetStatsFunc func(peerKey string) (configurer.WGStats, error) + GetStatsFunc func() (map[string]configurer.WGStats, error) GetInterfaceGUIDStringFunc func() (string, error) GetProxyFunc func() wgproxy.Proxy GetNetFunc func() *netstack.Net @@ -171,8 +171,8 @@ func (m *MockWGIface) GetWGDevice() *wgdevice.Device { return m.GetWGDeviceFunc() } -func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) { - return m.GetStatsFunc(peerKey) +func (m *MockWGIface) GetStats() (map[string]configurer.WGStats, error) { + return m.GetStatsFunc() } func (m *MockWGIface) GetProxy() wgproxy.Proxy { @@ -378,6 +378,9 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { }, } }, + UpdatePeerFunc: func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { + return nil + }, } engine.wgInterface = wgIface engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{ @@ -400,6 +403,8 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { engine.udpMux = bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: conn}) engine.ctx = ctx engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{}) + engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface, dispatcher.NewConnectionDispatcher()) + engine.connMgr.Start(ctx) type testCase struct { name string @@ -770,6 +775,8 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { engine.routeManager = mockRouteManager engine.dnsServer = &dns.MockServer{} + engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface, dispatcher.NewConnectionDispatcher()) + engine.connMgr.Start(ctx) defer func() { exitErr := engine.Stop() @@ -966,6 +973,8 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { } engine.dnsServer = mockDNSServer + engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface, dispatcher.NewConnectionDispatcher()) + engine.connMgr.Start(ctx) defer func() { exitErr := engine.Stop() @@ -1476,7 +1485,7 @@ func getConnectedPeers(e *Engine) int { i := 0 for _, id := range e.peerStore.PeersPubKey() { conn, _ := e.peerStore.PeerConn(id) - if conn.Status() == peer.StatusConnected { + if conn.IsConnected() { i++ } } diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index ffeffaf41..e1761ff84 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -35,6 +35,6 @@ type wgIfaceBase interface { GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice GetWGDevice() *wgdevice.Device - GetStats(peerKey string) (configurer.WGStats, error) + GetStats() (map[string]configurer.WGStats, error) GetNet() *netstack.Net } diff --git a/client/internal/lazyconn/activity/listen_ip.go b/client/internal/lazyconn/activity/listen_ip.go new file mode 100644 index 000000000..aff73c5d8 --- /dev/null +++ b/client/internal/lazyconn/activity/listen_ip.go @@ -0,0 +1,9 @@ +//go:build !linux || android + +package activity + +import "net" + +var ( + listenIP = net.IP{127, 0, 0, 1} +) diff --git a/client/internal/lazyconn/activity/listen_ip_linux.go b/client/internal/lazyconn/activity/listen_ip_linux.go new file mode 100644 index 000000000..98beb963e --- /dev/null +++ b/client/internal/lazyconn/activity/listen_ip_linux.go @@ -0,0 +1,10 @@ +//go:build !android + +package activity + +import "net" + +var ( + // use this ip to avoid eBPF proxy congestion + listenIP = net.IP{127, 0, 1, 1} +) diff --git a/client/internal/lazyconn/activity/listener.go b/client/internal/lazyconn/activity/listener.go new file mode 100644 index 000000000..1ef48416a --- /dev/null +++ b/client/internal/lazyconn/activity/listener.go @@ -0,0 +1,106 @@ +package activity + +import ( + "fmt" + "net" + "sync" + "sync/atomic" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/lazyconn" +) + +// Listener it is not a thread safe implementation, do not call Close before ReadPackets. It will cause blocking +type Listener struct { + wgIface lazyconn.WGIface + peerCfg lazyconn.PeerConfig + conn *net.UDPConn + endpoint *net.UDPAddr + done sync.Mutex + + isClosed atomic.Bool // use to avoid error log when closing the listener +} + +func NewListener(wgIface lazyconn.WGIface, cfg lazyconn.PeerConfig) (*Listener, error) { + d := &Listener{ + wgIface: wgIface, + peerCfg: cfg, + } + + conn, err := d.newConn() + if err != nil { + return nil, fmt.Errorf("failed to creating activity listener: %v", err) + } + d.conn = conn + d.endpoint = conn.LocalAddr().(*net.UDPAddr) + + if err := d.createEndpoint(); err != nil { + return nil, err + } + d.done.Lock() + cfg.Log.Infof("created activity listener: %s", conn.LocalAddr().(*net.UDPAddr).String()) + return d, nil +} + +func (d *Listener) ReadPackets() { + for { + n, remoteAddr, err := d.conn.ReadFromUDP(make([]byte, 1)) + if err != nil { + if d.isClosed.Load() { + d.peerCfg.Log.Debugf("exit from activity listener") + } else { + d.peerCfg.Log.Errorf("failed to read from activity listener: %s", err) + } + break + } + + if n < 1 { + d.peerCfg.Log.Warnf("received %d bytes from %s, too short", n, remoteAddr) + continue + } + break + } + + if err := d.removeEndpoint(); err != nil { + d.peerCfg.Log.Errorf("failed to remove endpoint: %s", err) + } + + _ = d.conn.Close() // do not care err because some cases it will return "use of closed network connection" + d.done.Unlock() +} + +func (d *Listener) Close() { + d.peerCfg.Log.Infof("closing listener: %s", d.conn.LocalAddr().String()) + d.isClosed.Store(true) + + if err := d.conn.Close(); err != nil { + d.peerCfg.Log.Errorf("failed to close UDP listener: %s", err) + } + d.done.Lock() +} + +func (d *Listener) removeEndpoint() error { + d.peerCfg.Log.Debugf("removing lazy endpoint: %s", d.endpoint.String()) + return d.wgIface.RemovePeer(d.peerCfg.PublicKey) +} + +func (d *Listener) createEndpoint() error { + d.peerCfg.Log.Debugf("creating lazy endpoint: %s", d.endpoint.String()) + return d.wgIface.UpdatePeer(d.peerCfg.PublicKey, d.peerCfg.AllowedIPs, 0, d.endpoint, nil) +} + +func (d *Listener) newConn() (*net.UDPConn, error) { + addr := &net.UDPAddr{ + Port: 0, + IP: listenIP, + } + + conn, err := net.ListenUDP("udp", addr) + if err != nil { + log.Errorf("failed to create activity listener on %s: %s", addr, err) + return nil, err + } + + return conn, nil +} diff --git a/client/internal/lazyconn/activity/listener_test.go b/client/internal/lazyconn/activity/listener_test.go new file mode 100644 index 000000000..98d7838d2 --- /dev/null +++ b/client/internal/lazyconn/activity/listener_test.go @@ -0,0 +1,41 @@ +package activity + +import ( + "testing" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/lazyconn" +) + +func TestNewListener(t *testing.T) { + peer := &MocPeer{ + PeerID: "examplePublicKey1", + } + + cfg := lazyconn.PeerConfig{ + PublicKey: peer.PeerID, + PeerConnID: peer.ConnID(), + Log: log.WithField("peer", "examplePublicKey1"), + } + + l, err := NewListener(MocWGIface{}, cfg) + if err != nil { + t.Fatalf("failed to create listener: %v", err) + } + + chanClosed := make(chan struct{}) + go func() { + defer close(chanClosed) + l.ReadPackets() + }() + + time.Sleep(1 * time.Second) + l.Close() + + select { + case <-chanClosed: + case <-time.After(time.Second): + } +} diff --git a/client/internal/lazyconn/activity/manager.go b/client/internal/lazyconn/activity/manager.go new file mode 100644 index 000000000..e18b96465 --- /dev/null +++ b/client/internal/lazyconn/activity/manager.go @@ -0,0 +1,95 @@ +package activity + +import ( + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/lazyconn" + peerid "github.com/netbirdio/netbird/client/internal/peer/id" +) + +type Manager struct { + OnActivityChan chan peerid.ConnID + + wgIface lazyconn.WGIface + + peers map[peerid.ConnID]*Listener + done chan struct{} + + mu sync.Mutex +} + +func NewManager(wgIface lazyconn.WGIface) *Manager { + m := &Manager{ + OnActivityChan: make(chan peerid.ConnID, 1), + wgIface: wgIface, + peers: make(map[peerid.ConnID]*Listener), + done: make(chan struct{}), + } + return m +} + +func (m *Manager) MonitorPeerActivity(peerCfg lazyconn.PeerConfig) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, ok := m.peers[peerCfg.PeerConnID]; ok { + log.Warnf("activity listener already exists for: %s", peerCfg.PublicKey) + return nil + } + + listener, err := NewListener(m.wgIface, peerCfg) + if err != nil { + return err + } + m.peers[peerCfg.PeerConnID] = listener + + go m.waitForTraffic(listener, peerCfg.PeerConnID) + return nil +} + +func (m *Manager) RemovePeer(log *log.Entry, peerConnID peerid.ConnID) { + m.mu.Lock() + defer m.mu.Unlock() + + listener, ok := m.peers[peerConnID] + if !ok { + return + } + log.Debugf("removing activity listener") + delete(m.peers, peerConnID) + listener.Close() +} + +func (m *Manager) Close() { + m.mu.Lock() + defer m.mu.Unlock() + + close(m.done) + for peerID, listener := range m.peers { + delete(m.peers, peerID) + listener.Close() + } +} + +func (m *Manager) waitForTraffic(listener *Listener, peerConnID peerid.ConnID) { + listener.ReadPackets() + + m.mu.Lock() + if _, ok := m.peers[peerConnID]; !ok { + m.mu.Unlock() + return + } + delete(m.peers, peerConnID) + m.mu.Unlock() + + m.notify(peerConnID) +} + +func (m *Manager) notify(peerConnID peerid.ConnID) { + select { + case <-m.done: + case m.OnActivityChan <- peerConnID: + } +} diff --git a/client/internal/lazyconn/activity/manager_test.go b/client/internal/lazyconn/activity/manager_test.go new file mode 100644 index 000000000..c7c6c878a --- /dev/null +++ b/client/internal/lazyconn/activity/manager_test.go @@ -0,0 +1,162 @@ +package activity + +import ( + "net" + "net/netip" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/netbirdio/netbird/client/internal/lazyconn" + peerid "github.com/netbirdio/netbird/client/internal/peer/id" +) + +type MocPeer struct { + PeerID string +} + +func (m *MocPeer) ConnID() peerid.ConnID { + return peerid.ConnID(m) +} + +type MocWGIface struct { +} + +func (m MocWGIface) RemovePeer(string) error { + return nil +} + +func (m MocWGIface) UpdatePeer(string, []netip.Prefix, time.Duration, *net.UDPAddr, *wgtypes.Key) error { + return nil + +} + +func TestManager_MonitorPeerActivity(t *testing.T) { + mocWgInterface := &MocWGIface{} + + peer1 := &MocPeer{ + PeerID: "examplePublicKey1", + } + mgr := NewManager(mocWgInterface) + defer mgr.Close() + peerCfg1 := lazyconn.PeerConfig{ + PublicKey: peer1.PeerID, + PeerConnID: peer1.ConnID(), + Log: log.WithField("peer", "examplePublicKey1"), + } + + if err := mgr.MonitorPeerActivity(peerCfg1); err != nil { + t.Fatalf("failed to monitor peer activity: %v", err) + } + + if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil { + t.Fatalf("failed to trigger activity: %v", err) + } + + select { + case peerConnID := <-mgr.OnActivityChan: + if peerConnID != peerCfg1.PeerConnID { + t.Fatalf("unexpected peerConnID: %v", peerConnID) + } + case <-time.After(1 * time.Second): + } +} + +func TestManager_RemovePeerActivity(t *testing.T) { + mocWgInterface := &MocWGIface{} + + peer1 := &MocPeer{ + PeerID: "examplePublicKey1", + } + mgr := NewManager(mocWgInterface) + defer mgr.Close() + + peerCfg1 := lazyconn.PeerConfig{ + PublicKey: peer1.PeerID, + PeerConnID: peer1.ConnID(), + Log: log.WithField("peer", "examplePublicKey1"), + } + + if err := mgr.MonitorPeerActivity(peerCfg1); err != nil { + t.Fatalf("failed to monitor peer activity: %v", err) + } + + addr := mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String() + + mgr.RemovePeer(peerCfg1.Log, peerCfg1.PeerConnID) + + if err := trigger(addr); err != nil { + t.Fatalf("failed to trigger activity: %v", err) + } + + select { + case <-mgr.OnActivityChan: + t.Fatal("should not have active activity") + case <-time.After(1 * time.Second): + } +} + +func TestManager_MultiPeerActivity(t *testing.T) { + mocWgInterface := &MocWGIface{} + + peer1 := &MocPeer{ + PeerID: "examplePublicKey1", + } + mgr := NewManager(mocWgInterface) + defer mgr.Close() + + peerCfg1 := lazyconn.PeerConfig{ + PublicKey: peer1.PeerID, + PeerConnID: peer1.ConnID(), + Log: log.WithField("peer", "examplePublicKey1"), + } + + peer2 := &MocPeer{} + peerCfg2 := lazyconn.PeerConfig{ + PublicKey: peer2.PeerID, + PeerConnID: peer2.ConnID(), + Log: log.WithField("peer", "examplePublicKey2"), + } + + if err := mgr.MonitorPeerActivity(peerCfg1); err != nil { + t.Fatalf("failed to monitor peer activity: %v", err) + } + + if err := mgr.MonitorPeerActivity(peerCfg2); err != nil { + t.Fatalf("failed to monitor peer activity: %v", err) + } + + if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil { + t.Fatalf("failed to trigger activity: %v", err) + } + + if err := trigger(mgr.peers[peerCfg2.PeerConnID].conn.LocalAddr().String()); err != nil { + t.Fatalf("failed to trigger activity: %v", err) + } + + for i := 0; i < 2; i++ { + select { + case <-mgr.OnActivityChan: + case <-time.After(1 * time.Second): + t.Fatal("timed out waiting for activity") + } + } +} + +func trigger(addr string) error { + // Create a connection to the destination UDP address and port + conn, err := net.Dial("udp", addr) + if err != nil { + return err + } + defer conn.Close() + + // Write the bytes to the UDP connection + _, err = conn.Write([]byte{0x01, 0x02, 0x03, 0x04, 0x05}) + if err != nil { + return err + } + return nil +} diff --git a/client/internal/lazyconn/doc.go b/client/internal/lazyconn/doc.go new file mode 100644 index 000000000..156520bd5 --- /dev/null +++ b/client/internal/lazyconn/doc.go @@ -0,0 +1,32 @@ +/* +Package lazyconn provides mechanisms for managing lazy connections, which activate on demand to optimize resource usage and establish connections efficiently. + +## Overview + +The package includes a `Manager` component responsible for: +- Managing lazy connections activated on-demand +- Managing inactivity monitors for lazy connections (based on peer disconnection events) +- Maintaining a list of excluded peers that should always have permanent connections +- Handling remote peer connection initiatives based on peer signaling + +## Thread-Safe Operations + +The `Manager` ensures thread safety across multiple operations, categorized by caller: + +- **Engine (single goroutine)**: + - `AddPeer`: Adds a peer to the connection manager. + - `RemovePeer`: Removes a peer from the connection manager. + - `ActivatePeer`: Activates a lazy connection for a peer. This come from Signal client + - `ExcludePeer`: Marks peers for a permanent connection. Like router peers and other peers that should always have a connection. + +- **Connection Dispatcher (any peer routine)**: + - `onPeerConnected`: Suspend the inactivity monitor for an active peer connection. + - `onPeerDisconnected`: Starts the inactivity monitor for a disconnected peer. + +- **Activity Manager**: + - `onPeerActivity`: Run peer.Open(context). + +- **Inactivity Monitor**: + - `onPeerInactivityTimedOut`: Close peer connection and restart activity monitor. +*/ +package lazyconn diff --git a/client/internal/lazyconn/env.go b/client/internal/lazyconn/env.go new file mode 100644 index 000000000..649d1cd65 --- /dev/null +++ b/client/internal/lazyconn/env.go @@ -0,0 +1,26 @@ +package lazyconn + +import ( + "os" + "strconv" + + log "github.com/sirupsen/logrus" +) + +const ( + EnvEnableLazyConn = "NB_ENABLE_EXPERIMENTAL_LAZY_CONN" + EnvInactivityThreshold = "NB_LAZY_CONN_INACTIVITY_THRESHOLD" +) + +func IsLazyConnEnabledByEnv() bool { + val := os.Getenv(EnvEnableLazyConn) + if val == "" { + return false + } + enabled, err := strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvEnableLazyConn, err) + return false + } + return enabled +} diff --git a/client/internal/lazyconn/inactivity/inactivity.go b/client/internal/lazyconn/inactivity/inactivity.go new file mode 100644 index 000000000..a30c1846d --- /dev/null +++ b/client/internal/lazyconn/inactivity/inactivity.go @@ -0,0 +1,70 @@ +package inactivity + +import ( + "context" + "time" + + peer "github.com/netbirdio/netbird/client/internal/peer/id" +) + +const ( + DefaultInactivityThreshold = 60 * time.Minute // idle after 1 hour inactivity + MinimumInactivityThreshold = 3 * time.Minute +) + +type Monitor struct { + id peer.ConnID + timer *time.Timer + cancel context.CancelFunc + inactivityThreshold time.Duration +} + +func NewInactivityMonitor(peerID peer.ConnID, threshold time.Duration) *Monitor { + i := &Monitor{ + id: peerID, + timer: time.NewTimer(0), + inactivityThreshold: threshold, + } + i.timer.Stop() + return i +} + +func (i *Monitor) Start(ctx context.Context, timeoutChan chan peer.ConnID) { + i.timer.Reset(i.inactivityThreshold) + defer i.timer.Stop() + + ctx, i.cancel = context.WithCancel(ctx) + defer func() { + defer i.cancel() + select { + case <-i.timer.C: + default: + } + }() + + select { + case <-i.timer.C: + select { + case timeoutChan <- i.id: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } +} + +func (i *Monitor) Stop() { + if i.cancel == nil { + return + } + i.cancel() +} + +func (i *Monitor) PauseTimer() { + i.timer.Stop() +} + +func (i *Monitor) ResetTimer() { + i.timer.Reset(i.inactivityThreshold) +} diff --git a/client/internal/lazyconn/inactivity/inactivity_test.go b/client/internal/lazyconn/inactivity/inactivity_test.go new file mode 100644 index 000000000..944512985 --- /dev/null +++ b/client/internal/lazyconn/inactivity/inactivity_test.go @@ -0,0 +1,156 @@ +package inactivity + +import ( + "context" + "testing" + "time" + + peerid "github.com/netbirdio/netbird/client/internal/peer/id" +) + +type MocPeer struct { +} + +func (m *MocPeer) ConnID() peerid.ConnID { + return peerid.ConnID(m) +} + +func TestInactivityMonitor(t *testing.T) { + tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5) + defer testTimeoutCancel() + + p := &MocPeer{} + im := NewInactivityMonitor(p.ConnID(), time.Second*2) + + timeoutChan := make(chan peerid.ConnID) + + exitChan := make(chan struct{}) + + go func() { + defer close(exitChan) + im.Start(tCtx, timeoutChan) + }() + + select { + case <-timeoutChan: + case <-tCtx.Done(): + t.Fatal("timeout") + } + + select { + case <-exitChan: + case <-tCtx.Done(): + t.Fatal("timeout") + } +} + +func TestReuseInactivityMonitor(t *testing.T) { + p := &MocPeer{} + im := NewInactivityMonitor(p.ConnID(), time.Second*2) + + timeoutChan := make(chan peerid.ConnID) + + for i := 2; i > 0; i-- { + exitChan := make(chan struct{}) + + testTimeoutCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5) + + go func() { + defer close(exitChan) + im.Start(testTimeoutCtx, timeoutChan) + }() + + select { + case <-timeoutChan: + case <-testTimeoutCtx.Done(): + t.Fatal("timeout") + } + + select { + case <-exitChan: + case <-testTimeoutCtx.Done(): + t.Fatal("timeout") + } + testTimeoutCancel() + } +} + +func TestStopInactivityMonitor(t *testing.T) { + tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5) + defer testTimeoutCancel() + + p := &MocPeer{} + im := NewInactivityMonitor(p.ConnID(), DefaultInactivityThreshold) + + timeoutChan := make(chan peerid.ConnID) + + exitChan := make(chan struct{}) + + go func() { + defer close(exitChan) + im.Start(tCtx, timeoutChan) + }() + + go func() { + time.Sleep(3 * time.Second) + im.Stop() + }() + + select { + case <-timeoutChan: + t.Fatal("unexpected timeout") + case <-exitChan: + case <-tCtx.Done(): + t.Fatal("timeout") + } +} + +func TestPauseInactivityMonitor(t *testing.T) { + tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*10) + defer testTimeoutCancel() + + p := &MocPeer{} + trashHold := time.Second * 3 + im := NewInactivityMonitor(p.ConnID(), trashHold) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + timeoutChan := make(chan peerid.ConnID) + + exitChan := make(chan struct{}) + + go func() { + defer close(exitChan) + im.Start(ctx, timeoutChan) + }() + + time.Sleep(1 * time.Second) // grant time to start the monitor + im.PauseTimer() + + // check to do not receive timeout + thresholdCtx, thresholdCancel := context.WithTimeout(context.Background(), trashHold+time.Second) + defer thresholdCancel() + select { + case <-exitChan: + t.Fatal("unexpected exit") + case <-timeoutChan: + t.Fatal("unexpected timeout") + case <-thresholdCtx.Done(): + // test ok + case <-tCtx.Done(): + t.Fatal("test timed out") + } + + // test reset timer + im.ResetTimer() + + select { + case <-tCtx.Done(): + t.Fatal("test timed out") + case <-exitChan: + t.Fatal("unexpected exit") + case <-timeoutChan: + // expected timeout + } +} diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go new file mode 100644 index 000000000..af12a73e4 --- /dev/null +++ b/client/internal/lazyconn/manager/manager.go @@ -0,0 +1,404 @@ +package manager + +import ( + "context" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/lazyconn" + "github.com/netbirdio/netbird/client/internal/lazyconn/activity" + "github.com/netbirdio/netbird/client/internal/lazyconn/inactivity" + "github.com/netbirdio/netbird/client/internal/peer/dispatcher" + peerid "github.com/netbirdio/netbird/client/internal/peer/id" + "github.com/netbirdio/netbird/client/internal/peerstore" +) + +const ( + watcherActivity watcherType = iota + watcherInactivity +) + +type watcherType int + +type managedPeer struct { + peerCfg *lazyconn.PeerConfig + expectedWatcher watcherType +} + +type Config struct { + InactivityThreshold *time.Duration +} + +// Manager manages lazy connections +// It is responsible for: +// - Managing lazy connections activated on-demand +// - Managing inactivity monitors for lazy connections (based on peer disconnection events) +// - Maintaining a list of excluded peers that should always have permanent connections +// - Handling connection establishment based on peer signaling +type Manager struct { + peerStore *peerstore.Store + connStateDispatcher *dispatcher.ConnectionDispatcher + inactivityThreshold time.Duration + + connStateListener *dispatcher.ConnectionListener + managedPeers map[string]*lazyconn.PeerConfig + managedPeersByConnID map[peerid.ConnID]*managedPeer + excludes map[string]lazyconn.PeerConfig + managedPeersMu sync.Mutex + + activityManager *activity.Manager + inactivityMonitors map[peerid.ConnID]*inactivity.Monitor + + cancel context.CancelFunc + onInactive chan peerid.ConnID +} + +func NewManager(config Config, peerStore *peerstore.Store, wgIface lazyconn.WGIface, connStateDispatcher *dispatcher.ConnectionDispatcher) *Manager { + log.Infof("setup lazy connection service") + m := &Manager{ + peerStore: peerStore, + connStateDispatcher: connStateDispatcher, + inactivityThreshold: inactivity.DefaultInactivityThreshold, + managedPeers: make(map[string]*lazyconn.PeerConfig), + managedPeersByConnID: make(map[peerid.ConnID]*managedPeer), + excludes: make(map[string]lazyconn.PeerConfig), + activityManager: activity.NewManager(wgIface), + inactivityMonitors: make(map[peerid.ConnID]*inactivity.Monitor), + onInactive: make(chan peerid.ConnID), + } + + if config.InactivityThreshold != nil { + if *config.InactivityThreshold >= inactivity.MinimumInactivityThreshold { + m.inactivityThreshold = *config.InactivityThreshold + } else { + log.Warnf("inactivity threshold is too low, using %v", m.inactivityThreshold) + } + } + + m.connStateListener = &dispatcher.ConnectionListener{ + OnConnected: m.onPeerConnected, + OnDisconnected: m.onPeerDisconnected, + } + + connStateDispatcher.AddListener(m.connStateListener) + + return m +} + +// Start starts the manager and listens for peer activity and inactivity events +func (m *Manager) Start(ctx context.Context) { + defer m.close() + + ctx, m.cancel = context.WithCancel(ctx) + for { + select { + case <-ctx.Done(): + return + case peerConnID := <-m.activityManager.OnActivityChan: + m.onPeerActivity(ctx, peerConnID) + case peerConnID := <-m.onInactive: + m.onPeerInactivityTimedOut(peerConnID) + } + } +} + +// ExcludePeer marks peers for a permanent connection +// It removes peers from the managed list if they are added to the exclude list +// Adds them back to the managed list and start the inactivity listener if they are removed from the exclude list. In +// this case, we suppose that the connection status is connected or connecting. +// If the peer is not exists yet in the managed list then the responsibility is the upper layer to call the AddPeer function +func (m *Manager) ExcludePeer(ctx context.Context, peerConfigs []lazyconn.PeerConfig) []string { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + added := make([]string, 0) + excludes := make(map[string]lazyconn.PeerConfig, len(peerConfigs)) + + for _, peerCfg := range peerConfigs { + log.Infof("update excluded lazy connection list with peer: %s", peerCfg.PublicKey) + excludes[peerCfg.PublicKey] = peerCfg + } + + // if a peer is newly added to the exclude list, remove from the managed peers list + for pubKey, peerCfg := range excludes { + if _, wasExcluded := m.excludes[pubKey]; wasExcluded { + continue + } + + added = append(added, pubKey) + peerCfg.Log.Infof("peer newly added to lazy connection exclude list") + m.removePeer(pubKey) + } + + // if a peer has been removed from exclude list then it should be added to the managed peers + for pubKey, peerCfg := range m.excludes { + if _, stillExcluded := excludes[pubKey]; stillExcluded { + continue + } + + peerCfg.Log.Infof("peer removed from lazy connection exclude list") + + if err := m.addActivePeer(ctx, peerCfg); err != nil { + log.Errorf("failed to add peer to lazy connection manager: %s", err) + continue + } + } + + m.excludes = excludes + return added +} + +func (m *Manager) AddPeer(peerCfg lazyconn.PeerConfig) (bool, error) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + peerCfg.Log.Debugf("adding peer to lazy connection manager") + + _, exists := m.excludes[peerCfg.PublicKey] + if exists { + return true, nil + } + + if _, ok := m.managedPeers[peerCfg.PublicKey]; ok { + peerCfg.Log.Warnf("peer already managed") + return false, nil + } + + if err := m.activityManager.MonitorPeerActivity(peerCfg); err != nil { + return false, err + } + + im := inactivity.NewInactivityMonitor(peerCfg.PeerConnID, m.inactivityThreshold) + m.inactivityMonitors[peerCfg.PeerConnID] = im + + m.managedPeers[peerCfg.PublicKey] = &peerCfg + m.managedPeersByConnID[peerCfg.PeerConnID] = &managedPeer{ + peerCfg: &peerCfg, + expectedWatcher: watcherActivity, + } + return false, nil +} + +// AddActivePeers adds a list of peers to the lazy connection manager +// suppose these peers was in connected or in connecting states +func (m *Manager) AddActivePeers(ctx context.Context, peerCfg []lazyconn.PeerConfig) error { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + for _, cfg := range peerCfg { + if _, ok := m.managedPeers[cfg.PublicKey]; ok { + cfg.Log.Errorf("peer already managed") + continue + } + + if err := m.addActivePeer(ctx, cfg); err != nil { + cfg.Log.Errorf("failed to add peer to lazy connection manager: %v", err) + return err + } + } + return nil +} + +func (m *Manager) RemovePeer(peerID string) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + m.removePeer(peerID) +} + +// ActivatePeer activates a peer connection when a signal message is received +func (m *Manager) ActivatePeer(ctx context.Context, peerID string) (found bool) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + cfg, ok := m.managedPeers[peerID] + if !ok { + return false + } + + mp, ok := m.managedPeersByConnID[cfg.PeerConnID] + if !ok { + return false + } + + // signal messages coming continuously after success activation, with this avoid the multiple activation + if mp.expectedWatcher == watcherInactivity { + return false + } + + mp.expectedWatcher = watcherInactivity + + m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID) + + im, ok := m.inactivityMonitors[cfg.PeerConnID] + if !ok { + cfg.Log.Errorf("inactivity monitor not found for peer") + return false + } + + mp.peerCfg.Log.Infof("starting inactivity monitor") + go im.Start(ctx, m.onInactive) + + return true +} + +func (m *Manager) addActivePeer(ctx context.Context, peerCfg lazyconn.PeerConfig) error { + if _, ok := m.managedPeers[peerCfg.PublicKey]; ok { + peerCfg.Log.Warnf("peer already managed") + return nil + } + + im := inactivity.NewInactivityMonitor(peerCfg.PeerConnID, m.inactivityThreshold) + m.inactivityMonitors[peerCfg.PeerConnID] = im + + m.managedPeers[peerCfg.PublicKey] = &peerCfg + m.managedPeersByConnID[peerCfg.PeerConnID] = &managedPeer{ + peerCfg: &peerCfg, + expectedWatcher: watcherInactivity, + } + + peerCfg.Log.Infof("starting inactivity monitor on peer that has been removed from exclude list") + go im.Start(ctx, m.onInactive) + return nil +} + +func (m *Manager) removePeer(peerID string) { + cfg, ok := m.managedPeers[peerID] + if !ok { + return + } + + cfg.Log.Infof("removing lazy peer") + + if im, ok := m.inactivityMonitors[cfg.PeerConnID]; ok { + im.Stop() + delete(m.inactivityMonitors, cfg.PeerConnID) + cfg.Log.Debugf("inactivity monitor stopped") + } + + m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID) + delete(m.managedPeers, peerID) + delete(m.managedPeersByConnID, cfg.PeerConnID) +} + +func (m *Manager) close() { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + m.cancel() + + m.connStateDispatcher.RemoveListener(m.connStateListener) + m.activityManager.Close() + for _, iw := range m.inactivityMonitors { + iw.Stop() + } + m.inactivityMonitors = make(map[peerid.ConnID]*inactivity.Monitor) + m.managedPeers = make(map[string]*lazyconn.PeerConfig) + m.managedPeersByConnID = make(map[peerid.ConnID]*managedPeer) + log.Infof("lazy connection manager closed") +} + +func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + mp, ok := m.managedPeersByConnID[peerConnID] + if !ok { + log.Errorf("peer not found by conn id: %v", peerConnID) + return + } + + if mp.expectedWatcher != watcherActivity { + mp.peerCfg.Log.Warnf("ignore activity event") + return + } + + mp.peerCfg.Log.Infof("detected peer activity") + + mp.expectedWatcher = watcherInactivity + + mp.peerCfg.Log.Infof("starting inactivity monitor") + go m.inactivityMonitors[peerConnID].Start(ctx, m.onInactive) + + m.peerStore.PeerConnOpen(ctx, mp.peerCfg.PublicKey) +} + +func (m *Manager) onPeerInactivityTimedOut(peerConnID peerid.ConnID) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + mp, ok := m.managedPeersByConnID[peerConnID] + if !ok { + log.Errorf("peer not found by id: %v", peerConnID) + return + } + + if mp.expectedWatcher != watcherInactivity { + mp.peerCfg.Log.Warnf("ignore inactivity event") + return + } + + mp.peerCfg.Log.Infof("connection timed out") + + // this is blocking operation, potentially can be optimized + m.peerStore.PeerConnClose(mp.peerCfg.PublicKey) + + mp.peerCfg.Log.Infof("start activity monitor") + + mp.expectedWatcher = watcherActivity + + // just in case free up + m.inactivityMonitors[peerConnID].PauseTimer() + + if err := m.activityManager.MonitorPeerActivity(*mp.peerCfg); err != nil { + mp.peerCfg.Log.Errorf("failed to create activity monitor: %v", err) + return + } +} + +func (m *Manager) onPeerConnected(peerConnID peerid.ConnID) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + mp, ok := m.managedPeersByConnID[peerConnID] + if !ok { + return + } + + if mp.expectedWatcher != watcherInactivity { + return + } + + iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID] + if !ok { + mp.peerCfg.Log.Errorf("inactivity monitor not found for peer") + return + } + + mp.peerCfg.Log.Infof("peer connected, pausing inactivity monitor while connection is not disconnected") + iw.PauseTimer() +} + +func (m *Manager) onPeerDisconnected(peerConnID peerid.ConnID) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + mp, ok := m.managedPeersByConnID[peerConnID] + if !ok { + return + } + + if mp.expectedWatcher != watcherInactivity { + return + } + + iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID] + if !ok { + return + } + + mp.peerCfg.Log.Infof("reset inactivity monitor timer") + iw.ResetTimer() +} diff --git a/client/internal/lazyconn/peercfg.go b/client/internal/lazyconn/peercfg.go new file mode 100644 index 000000000..987d06a3e --- /dev/null +++ b/client/internal/lazyconn/peercfg.go @@ -0,0 +1,16 @@ +package lazyconn + +import ( + "net/netip" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/peer/id" +) + +type PeerConfig struct { + PublicKey string + AllowedIPs []netip.Prefix + PeerConnID id.ConnID + Log *log.Entry +} diff --git a/client/internal/lazyconn/support.go b/client/internal/lazyconn/support.go new file mode 100644 index 000000000..5e765c2d6 --- /dev/null +++ b/client/internal/lazyconn/support.go @@ -0,0 +1,41 @@ +package lazyconn + +import ( + "strings" + + "github.com/hashicorp/go-version" +) + +var ( + minVersion = version.Must(version.NewVersion("0.45.0")) +) + +func IsSupported(agentVersion string) bool { + if agentVersion == "development" { + return true + } + + // filter out versions like this: a6c5960, a7d5c522, d47be154 + if !strings.Contains(agentVersion, ".") { + return false + } + + normalizedVersion := normalizeVersion(agentVersion) + inputVer, err := version.NewVersion(normalizedVersion) + if err != nil { + return false + } + + return inputVer.GreaterThanOrEqual(minVersion) +} + +func normalizeVersion(version string) string { + // Remove prefixes like 'v' or 'a' + if len(version) > 0 && (version[0] == 'v' || version[0] == 'a') { + version = version[1:] + } + + // Remove any suffixes like '-dirty', '-dev', '-SNAPSHOT', etc. + parts := strings.Split(version, "-") + return parts[0] +} diff --git a/client/internal/lazyconn/support_test.go b/client/internal/lazyconn/support_test.go new file mode 100644 index 000000000..9ae95a4a4 --- /dev/null +++ b/client/internal/lazyconn/support_test.go @@ -0,0 +1,31 @@ +package lazyconn + +import "testing" + +func TestIsSupported(t *testing.T) { + tests := []struct { + version string + want bool + }{ + {"development", true}, + {"0.45.0", true}, + {"v0.45.0", true}, + {"0.45.1", true}, + {"0.45.1-SNAPSHOT-559e6731", true}, + {"v0.45.1-dev", true}, + {"a7d5c522", false}, + {"0.9.6", false}, + {"0.9.6-SNAPSHOT", false}, + {"0.9.6-SNAPSHOT-2033650", false}, + {"meta_wt_version", false}, + {"v0.31.1-dev", false}, + {"", false}, + } + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + if got := IsSupported(tt.version); got != tt.want { + t.Errorf("IsSupported() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/client/internal/lazyconn/wgiface.go b/client/internal/lazyconn/wgiface.go new file mode 100644 index 000000000..090a9319c --- /dev/null +++ b/client/internal/lazyconn/wgiface.go @@ -0,0 +1,14 @@ +package lazyconn + +import ( + "net" + "net/netip" + "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type WGIface interface { + RemovePeer(peerKey string) error + UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error +} diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 44e8997bc..5037a0bd0 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -17,8 +17,12 @@ import ( "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/wgproxy" + "github.com/netbirdio/netbird/client/internal/peer/conntype" + "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" + "github.com/netbirdio/netbird/client/internal/peer/id" + "github.com/netbirdio/netbird/client/internal/peer/worker" "github.com/netbirdio/netbird/client/internal/stdnet" relayClient "github.com/netbirdio/netbird/relay/client" "github.com/netbirdio/netbird/route" @@ -26,32 +30,20 @@ import ( semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" ) -type ConnPriority int - -func (cp ConnPriority) String() string { - switch cp { - case connPriorityNone: - return "None" - case connPriorityRelay: - return "PriorityRelay" - case connPriorityICETurn: - return "PriorityICETurn" - case connPriorityICEP2P: - return "PriorityICEP2P" - default: - return fmt.Sprintf("ConnPriority(%d)", cp) - } -} - const ( defaultWgKeepAlive = 25 * time.Second - - connPriorityNone ConnPriority = 0 - connPriorityRelay ConnPriority = 1 - connPriorityICETurn ConnPriority = 2 - connPriorityICEP2P ConnPriority = 3 ) +type ServiceDependencies struct { + StatusRecorder *Status + Signaler *Signaler + IFaceDiscover stdnet.ExternalIFaceDiscover + RelayManager *relayClient.Manager + SrWatcher *guard.SRWatcher + Semaphore *semaphoregroup.SemaphoreGroup + PeerConnDispatcher *dispatcher.ConnectionDispatcher +} + type WgConfig struct { WgListenPort int RemoteKey string @@ -76,6 +68,8 @@ type ConnConfig struct { // LocalKey is a public key of a local peer LocalKey string + AgentVersion string + Timeout time.Duration WgConfig WgConfig @@ -89,22 +83,23 @@ type ConnConfig struct { } type Conn struct { - log *log.Entry + Log *log.Entry mu sync.Mutex ctx context.Context ctxCancel context.CancelFunc config ConnConfig statusRecorder *Status signaler *Signaler + iFaceDiscover stdnet.ExternalIFaceDiscover relayManager *relayClient.Manager - handshaker *Handshaker + srWatcher *guard.SRWatcher onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string) onDisconnected func(remotePeer string) - statusRelay *AtomicConnStatus - statusICE *AtomicConnStatus - currentConnPriority ConnPriority + statusRelay *worker.AtomicWorkerStatus + statusICE *worker.AtomicWorkerStatus + currentConnPriority conntype.ConnPriority opened bool // this flag is used to prevent close in case of not opened connection workerICE *WorkerICE @@ -120,9 +115,12 @@ type Conn struct { wgProxyICE wgproxy.Proxy wgProxyRelay wgproxy.Proxy + handshaker *Handshaker - guard *guard.Guard - semaphore *semaphoregroup.SemaphoreGroup + guard *guard.Guard + semaphore *semaphoregroup.SemaphoreGroup + peerConnDispatcher *dispatcher.ConnectionDispatcher + wg sync.WaitGroup // debug purpose dumpState *stateDump @@ -130,91 +128,101 @@ type Conn struct { // NewConn creates a new not opened Conn to the remote peer. // To establish a connection run Conn.Open -func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Status, signaler *Signaler, iFaceDiscover stdnet.ExternalIFaceDiscover, relayManager *relayClient.Manager, srWatcher *guard.SRWatcher, semaphore *semaphoregroup.SemaphoreGroup) (*Conn, error) { +func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) { if len(config.WgConfig.AllowedIps) == 0 { return nil, fmt.Errorf("allowed IPs is empty") } - ctx, ctxCancel := context.WithCancel(engineCtx) connLog := log.WithField("peer", config.Key) var conn = &Conn{ - log: connLog, - ctx: ctx, - ctxCancel: ctxCancel, - config: config, - statusRecorder: statusRecorder, - signaler: signaler, - relayManager: relayManager, - statusRelay: NewAtomicConnStatus(), - statusICE: NewAtomicConnStatus(), - semaphore: semaphore, - dumpState: newStateDump(config.Key, connLog, statusRecorder), + Log: connLog, + config: config, + statusRecorder: services.StatusRecorder, + signaler: services.Signaler, + iFaceDiscover: services.IFaceDiscover, + relayManager: services.RelayManager, + srWatcher: services.SrWatcher, + semaphore: services.Semaphore, + peerConnDispatcher: services.PeerConnDispatcher, + statusRelay: worker.NewAtomicStatus(), + statusICE: worker.NewAtomicStatus(), + dumpState: newStateDump(config.Key, connLog, services.StatusRecorder), } - ctrl := isController(config) - conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager, conn.dumpState) - - relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally() - workerICE, err := NewWorkerICE(ctx, connLog, config, conn, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally) - if err != nil { - return nil, err - } - conn.workerICE = workerICE - - conn.handshaker = NewHandshaker(ctx, connLog, config, signaler, conn.workerICE, conn.workerRelay) - - conn.handshaker.AddOnNewOfferListener(conn.workerRelay.OnNewOffer) - if os.Getenv("NB_FORCE_RELAY") != "true" { - conn.handshaker.AddOnNewOfferListener(conn.workerICE.OnNewOffer) - } - - conn.guard = guard.NewGuard(connLog, ctrl, conn.isConnectedOnAllWay, config.Timeout, srWatcher) - - go conn.handshaker.Listen() - - go conn.dumpState.Start(ctx) return conn, nil } // Open opens connection to the remote peer // It will try to establish a connection using ICE and in parallel with relay. The higher priority connection type will // be used. -func (conn *Conn) Open() { - conn.semaphore.Add(conn.ctx) - conn.log.Debugf("open connection to peer") +func (conn *Conn) Open(engineCtx context.Context) error { + conn.semaphore.Add(engineCtx) conn.mu.Lock() defer conn.mu.Unlock() - conn.opened = true + + if conn.opened { + conn.semaphore.Done(engineCtx) + return nil + } + + conn.ctx, conn.ctxCancel = context.WithCancel(engineCtx) + + conn.workerRelay = NewWorkerRelay(conn.Log, isController(conn.config), conn.config, conn, conn.relayManager, conn.dumpState) + + relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally() + workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally) + if err != nil { + return err + } + conn.workerICE = workerICE + + conn.handshaker = NewHandshaker(conn.Log, conn.config, conn.signaler, conn.workerICE, conn.workerRelay) + + conn.handshaker.AddOnNewOfferListener(conn.workerRelay.OnNewOffer) + if os.Getenv("NB_FORCE_RELAY") != "true" { + conn.handshaker.AddOnNewOfferListener(conn.workerICE.OnNewOffer) + } + + conn.guard = guard.NewGuard(conn.Log, conn.isConnectedOnAllWay, conn.config.Timeout, conn.srWatcher) + + conn.wg.Add(1) + go func() { + defer conn.wg.Done() + conn.handshaker.Listen(conn.ctx) + }() + go conn.dumpState.Start(conn.ctx) peerState := State{ PubKey: conn.config.Key, - IP: conn.config.WgConfig.AllowedIps[0].Addr().String(), ConnStatusUpdate: time.Now(), - ConnStatus: StatusDisconnected, + ConnStatus: StatusConnecting, Mux: new(sync.RWMutex), } - err := conn.statusRecorder.UpdatePeerState(peerState) - if err != nil { - conn.log.Warnf("error while updating the state err: %v", err) + if err := conn.statusRecorder.UpdatePeerState(peerState); err != nil { + conn.Log.Warnf("error while updating the state err: %v", err) } - go conn.startHandshakeAndReconnect(conn.ctx) -} + conn.wg.Add(1) + go func() { + defer conn.wg.Done() + conn.waitInitialRandomSleepTime(conn.ctx) + conn.semaphore.Done(conn.ctx) -func (conn *Conn) startHandshakeAndReconnect(ctx context.Context) { - defer conn.semaphore.Done(conn.ctx) - conn.waitInitialRandomSleepTime(ctx) + conn.dumpState.SendOffer() + if err := conn.handshaker.sendOffer(); err != nil { + conn.Log.Errorf("failed to send initial offer: %v", err) + } - conn.dumpState.SendOffer() - err := conn.handshaker.sendOffer() - if err != nil { - conn.log.Errorf("failed to send initial offer: %v", err) - } - - go conn.guard.Start(ctx) - go conn.listenGuardEvent(ctx) + conn.wg.Add(1) + go func() { + conn.guard.Start(conn.ctx, conn.onGuardEvent) + conn.wg.Done() + }() + }() + conn.opened = true + return nil } // Close closes this peer Conn issuing a close event to the Conn closeCh @@ -223,14 +231,14 @@ func (conn *Conn) Close() { defer conn.wgWatcherWg.Wait() defer conn.mu.Unlock() - conn.log.Infof("close peer connection") - conn.ctxCancel() - if !conn.opened { - conn.log.Debugf("ignore close connection to peer") + conn.Log.Debugf("ignore close connection to peer") return } + conn.Log.Infof("close peer connection") + conn.ctxCancel() + conn.workerRelay.DisableWgWatcher() conn.workerRelay.CloseConn() conn.workerICE.Close() @@ -238,7 +246,7 @@ func (conn *Conn) Close() { if conn.wgProxyRelay != nil { err := conn.wgProxyRelay.CloseConn() if err != nil { - conn.log.Errorf("failed to close wg proxy for relay: %v", err) + conn.Log.Errorf("failed to close wg proxy for relay: %v", err) } conn.wgProxyRelay = nil } @@ -246,13 +254,13 @@ func (conn *Conn) Close() { if conn.wgProxyICE != nil { err := conn.wgProxyICE.CloseConn() if err != nil { - conn.log.Errorf("failed to close wg proxy for ice: %v", err) + conn.Log.Errorf("failed to close wg proxy for ice: %v", err) } conn.wgProxyICE = nil } if err := conn.removeWgPeer(); err != nil { - conn.log.Errorf("failed to remove wg endpoint: %v", err) + conn.Log.Errorf("failed to remove wg endpoint: %v", err) } conn.freeUpConnID() @@ -262,14 +270,16 @@ func (conn *Conn) Close() { } conn.setStatusToDisconnected() - conn.log.Infof("peer connection has been closed") + conn.opened = false + conn.wg.Wait() + conn.Log.Infof("peer connection closed") } // OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise // doesn't block, discards the message if connection wasn't ready func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool { conn.dumpState.RemoteAnswer() - conn.log.Infof("OnRemoteAnswer, priority: %s, status ICE: %s, status relay: %s", conn.currentConnPriority, conn.statusICE, conn.statusRelay) + conn.Log.Infof("OnRemoteAnswer, priority: %s, status ICE: %s, status relay: %s", conn.currentConnPriority, conn.statusICE, conn.statusRelay) return conn.handshaker.OnRemoteAnswer(answer) } @@ -298,7 +308,7 @@ func (conn *Conn) SetOnDisconnected(handler func(remotePeer string)) { func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool { conn.dumpState.RemoteOffer() - conn.log.Infof("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay) + conn.Log.Infof("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay) return conn.handshaker.OnRemoteOffer(offer) } @@ -307,19 +317,24 @@ func (conn *Conn) WgConfig() WgConfig { return conn.config.WgConfig } -// Status returns current status of the Conn -func (conn *Conn) Status() ConnStatus { +// IsConnected unit tests only +// refactor unit test to use status recorder use refactor status recorded to manage connection status in peer.Conn +func (conn *Conn) IsConnected() bool { conn.mu.Lock() defer conn.mu.Unlock() - return conn.evalStatus() + return conn.currentConnPriority != conntype.None } func (conn *Conn) GetKey() string { return conn.config.Key } +func (conn *Conn) ConnID() id.ConnID { + return id.ConnID(conn) +} + // configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected -func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) { +func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConnInfo ICEConnInfo) { conn.mu.Lock() defer conn.mu.Unlock() @@ -327,21 +342,21 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC return } - if remoteConnNil(conn.log, iceConnInfo.RemoteConn) { - conn.log.Errorf("remote ICE connection is nil") + if remoteConnNil(conn.Log, iceConnInfo.RemoteConn) { + conn.Log.Errorf("remote ICE connection is nil") return } // this never should happen, because Relay is the lower priority and ICE always close the deprecated connection before upgrade // todo consider to remove this check if conn.currentConnPriority > priority { - conn.log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority) - conn.statusICE.Set(StatusConnected) + conn.Log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority) + conn.statusICE.SetConnected() conn.updateIceState(iceConnInfo) return } - conn.log.Infof("set ICE to active connection") + conn.Log.Infof("set ICE to active connection") conn.dumpState.P2PConnected() var ( @@ -353,7 +368,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC conn.dumpState.NewLocalProxy() wgProxy, err = conn.newProxy(iceConnInfo.RemoteConn) if err != nil { - conn.log.Errorf("failed to add turn net.Conn to local proxy: %v", err) + conn.Log.Errorf("failed to add turn net.Conn to local proxy: %v", err) return } ep = wgProxy.EndpointAddr() @@ -369,7 +384,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC } if err := conn.runBeforeAddPeerHooks(ep.IP); err != nil { - conn.log.Errorf("Before add peer hook failed: %v", err) + conn.Log.Errorf("Before add peer hook failed: %v", err) } conn.workerRelay.DisableWgWatcher() @@ -388,10 +403,16 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC return } wgConfigWorkaround() + + oldState := conn.currentConnPriority conn.currentConnPriority = priority - conn.statusICE.Set(StatusConnected) + conn.statusICE.SetConnected() conn.updateIceState(iceConnInfo) conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr) + + if oldState == conntype.None { + conn.peerConnDispatcher.NotifyConnected(conn.ConnID()) + } } func (conn *Conn) onICEStateDisconnected() { @@ -402,22 +423,22 @@ func (conn *Conn) onICEStateDisconnected() { return } - conn.log.Tracef("ICE connection state changed to disconnected") + conn.Log.Tracef("ICE connection state changed to disconnected") if conn.wgProxyICE != nil { if err := conn.wgProxyICE.CloseConn(); err != nil { - conn.log.Warnf("failed to close deprecated wg proxy conn: %v", err) + conn.Log.Warnf("failed to close deprecated wg proxy conn: %v", err) } } // switch back to relay connection if conn.isReadyToUpgrade() { - conn.log.Infof("ICE disconnected, set Relay to active connection") + conn.Log.Infof("ICE disconnected, set Relay to active connection") conn.dumpState.SwitchToRelay() conn.wgProxyRelay.Work() if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr(), conn.rosenpassRemoteKey); err != nil { - conn.log.Errorf("failed to switch to relay conn: %v", err) + conn.Log.Errorf("failed to switch to relay conn: %v", err) } conn.wgWatcherWg.Add(1) @@ -425,17 +446,18 @@ func (conn *Conn) onICEStateDisconnected() { defer conn.wgWatcherWg.Done() conn.workerRelay.EnableWgWatcher(conn.ctx) }() - conn.currentConnPriority = connPriorityRelay + conn.currentConnPriority = conntype.Relay } else { - conn.log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", connPriorityNone.String()) - conn.currentConnPriority = connPriorityNone + conn.Log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", conntype.None.String()) + conn.currentConnPriority = conntype.None + conn.peerConnDispatcher.NotifyDisconnected(conn.ConnID()) } - changed := conn.statusICE.Get() != StatusDisconnected + changed := conn.statusICE.Get() != worker.StatusDisconnected if changed { conn.guard.SetICEConnDisconnected() } - conn.statusICE.Set(StatusDisconnected) + conn.statusICE.SetDisconnected() peerState := State{ PubKey: conn.config.Key, @@ -446,7 +468,7 @@ func (conn *Conn) onICEStateDisconnected() { err := conn.statusRecorder.UpdatePeerICEStateToDisconnected(peerState) if err != nil { - conn.log.Warnf("unable to set peer's state to disconnected ice, got error: %v", err) + conn.Log.Warnf("unable to set peer's state to disconnected ice, got error: %v", err) } } @@ -456,41 +478,41 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { if conn.ctx.Err() != nil { if err := rci.relayedConn.Close(); err != nil { - conn.log.Warnf("failed to close unnecessary relayed connection: %v", err) + conn.Log.Warnf("failed to close unnecessary relayed connection: %v", err) } return } conn.dumpState.RelayConnected() - conn.log.Debugf("Relay connection has been established, setup the WireGuard") + conn.Log.Debugf("Relay connection has been established, setup the WireGuard") wgProxy, err := conn.newProxy(rci.relayedConn) if err != nil { - conn.log.Errorf("failed to add relayed net.Conn to local proxy: %v", err) + conn.Log.Errorf("failed to add relayed net.Conn to local proxy: %v", err) return } conn.dumpState.NewLocalProxy() - conn.log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String()) + conn.Log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String()) if conn.isICEActive() { - conn.log.Infof("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) + conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) conn.setRelayedProxy(wgProxy) - conn.statusRelay.Set(StatusConnected) + conn.statusRelay.SetConnected() conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey) return } if err := conn.runBeforeAddPeerHooks(wgProxy.EndpointAddr().IP); err != nil { - conn.log.Errorf("Before add peer hook failed: %v", err) + conn.Log.Errorf("Before add peer hook failed: %v", err) } wgProxy.Work() if err := conn.configureWGEndpoint(wgProxy.EndpointAddr(), rci.rosenpassPubKey); err != nil { if err := wgProxy.CloseConn(); err != nil { - conn.log.Warnf("Failed to close relay connection: %v", err) + conn.Log.Warnf("Failed to close relay connection: %v", err) } - conn.log.Errorf("Failed to update WireGuard peer configuration: %v", err) + conn.Log.Errorf("Failed to update WireGuard peer configuration: %v", err) return } @@ -502,12 +524,13 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { wgConfigWorkaround() conn.rosenpassRemoteKey = rci.rosenpassPubKey - conn.currentConnPriority = connPriorityRelay - conn.statusRelay.Set(StatusConnected) + conn.currentConnPriority = conntype.Relay + conn.statusRelay.SetConnected() conn.setRelayedProxy(wgProxy) conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey) - conn.log.Infof("start to communicate with peer via relay") + conn.Log.Infof("start to communicate with peer via relay") conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr) + conn.peerConnDispatcher.NotifyConnected(conn.ConnID()) } func (conn *Conn) onRelayDisconnected() { @@ -518,14 +541,15 @@ func (conn *Conn) onRelayDisconnected() { return } - conn.log.Infof("relay connection is disconnected") + conn.Log.Debugf("relay connection is disconnected") - if conn.currentConnPriority == connPriorityRelay { - conn.log.Infof("clean up WireGuard config") + if conn.currentConnPriority == conntype.Relay { + conn.Log.Debugf("clean up WireGuard config") if err := conn.removeWgPeer(); err != nil { - conn.log.Errorf("failed to remove wg endpoint: %v", err) + conn.Log.Errorf("failed to remove wg endpoint: %v", err) } - conn.currentConnPriority = connPriorityNone + conn.currentConnPriority = conntype.None + conn.peerConnDispatcher.NotifyDisconnected(conn.ConnID()) } if conn.wgProxyRelay != nil { @@ -533,11 +557,11 @@ func (conn *Conn) onRelayDisconnected() { conn.wgProxyRelay = nil } - changed := conn.statusRelay.Get() != StatusDisconnected + changed := conn.statusRelay.Get() != worker.StatusDisconnected if changed { conn.guard.SetRelayedConnDisconnected() } - conn.statusRelay.Set(StatusDisconnected) + conn.statusRelay.SetDisconnected() peerState := State{ PubKey: conn.config.Key, @@ -546,22 +570,15 @@ func (conn *Conn) onRelayDisconnected() { ConnStatusUpdate: time.Now(), } if err := conn.statusRecorder.UpdatePeerRelayedStateToDisconnected(peerState); err != nil { - conn.log.Warnf("unable to save peer's state to Relay disconnected, got error: %v", err) + conn.Log.Warnf("unable to save peer's state to Relay disconnected, got error: %v", err) } } -func (conn *Conn) listenGuardEvent(ctx context.Context) { - for { - select { - case <-conn.guard.Reconnect: - conn.log.Infof("send offer to peer") - conn.dumpState.SendOffer() - if err := conn.handshaker.SendOffer(); err != nil { - conn.log.Errorf("failed to send offer: %v", err) - } - case <-ctx.Done(): - return - } +func (conn *Conn) onGuardEvent() { + conn.Log.Debugf("send offer to peer") + conn.dumpState.SendOffer() + if err := conn.handshaker.SendOffer(); err != nil { + conn.Log.Errorf("failed to send offer: %v", err) } } @@ -588,7 +605,7 @@ func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []by err := conn.statusRecorder.UpdatePeerRelayedState(peerState) if err != nil { - conn.log.Warnf("unable to save peer's Relay state, got error: %v", err) + conn.Log.Warnf("unable to save peer's Relay state, got error: %v", err) } } @@ -607,17 +624,18 @@ func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo) { err := conn.statusRecorder.UpdatePeerICEState(peerState) if err != nil { - conn.log.Warnf("unable to save peer's ICE state, got error: %v", err) + conn.Log.Warnf("unable to save peer's ICE state, got error: %v", err) } } func (conn *Conn) setStatusToDisconnected() { - conn.statusRelay.Set(StatusDisconnected) - conn.statusICE.Set(StatusDisconnected) + conn.statusRelay.SetDisconnected() + conn.statusICE.SetDisconnected() + conn.currentConnPriority = conntype.None peerState := State{ PubKey: conn.config.Key, - ConnStatus: StatusDisconnected, + ConnStatus: StatusIdle, ConnStatusUpdate: time.Now(), Mux: new(sync.RWMutex), } @@ -625,10 +643,10 @@ func (conn *Conn) setStatusToDisconnected() { if err != nil { // pretty common error because by that time Engine can already remove the peer and status won't be available. // todo rethink status updates - conn.log.Debugf("error while updating peer's state, err: %v", err) + conn.Log.Debugf("error while updating peer's state, err: %v", err) } if err := conn.statusRecorder.UpdateWireGuardPeerState(conn.config.Key, configurer.WGStats{}); err != nil { - conn.log.Debugf("failed to reset wireguard stats for peer: %s", err) + conn.Log.Debugf("failed to reset wireguard stats for peer: %s", err) } } @@ -656,27 +674,20 @@ func (conn *Conn) waitInitialRandomSleepTime(ctx context.Context) { } func (conn *Conn) isRelayed() bool { - if conn.statusRelay.Get() == StatusDisconnected && (conn.statusICE.Get() == StatusDisconnected || conn.statusICE.Get() == StatusConnecting) { + switch conn.currentConnPriority { + case conntype.Relay, conntype.ICETurn: + return true + default: return false } - - if conn.currentConnPriority == connPriorityICEP2P { - return false - } - - return true } func (conn *Conn) evalStatus() ConnStatus { - if conn.statusRelay.Get() == StatusConnected || conn.statusICE.Get() == StatusConnected { + if conn.statusRelay.Get() == worker.StatusConnected || conn.statusICE.Get() == worker.StatusConnected { return StatusConnected } - if conn.statusRelay.Get() == StatusConnecting || conn.statusICE.Get() == StatusConnecting { - return StatusConnecting - } - - return StatusDisconnected + return StatusConnecting } func (conn *Conn) isConnectedOnAllWay() (connected bool) { @@ -689,12 +700,12 @@ func (conn *Conn) isConnectedOnAllWay() (connected bool) { } }() - if conn.statusICE.Get() == StatusDisconnected { + if conn.statusICE.Get() == worker.StatusDisconnected { return false } if conn.workerRelay.IsRelayConnectionSupportedWithPeer() { - if conn.statusRelay.Get() != StatusConnected { + if conn.statusRelay.Get() == worker.StatusDisconnected { return false } } @@ -716,7 +727,7 @@ func (conn *Conn) freeUpConnID() { if conn.connIDRelay != "" { for _, hook := range conn.afterRemovePeerHooks { if err := hook(conn.connIDRelay); err != nil { - conn.log.Errorf("After remove peer hook failed: %v", err) + conn.Log.Errorf("After remove peer hook failed: %v", err) } } conn.connIDRelay = "" @@ -725,7 +736,7 @@ func (conn *Conn) freeUpConnID() { if conn.connIDICE != "" { for _, hook := range conn.afterRemovePeerHooks { if err := hook(conn.connIDICE); err != nil { - conn.log.Errorf("After remove peer hook failed: %v", err) + conn.Log.Errorf("After remove peer hook failed: %v", err) } } conn.connIDICE = "" @@ -733,7 +744,7 @@ func (conn *Conn) freeUpConnID() { } func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) { - conn.log.Debugf("setup proxied WireGuard connection") + conn.Log.Debugf("setup proxied WireGuard connection") udpAddr := &net.UDPAddr{ IP: conn.config.WgConfig.AllowedIps[0].Addr().AsSlice(), Port: conn.config.WgConfig.WgListenPort, @@ -741,18 +752,18 @@ func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) { wgProxy := conn.config.WgConfig.WgInterface.GetProxy() if err := wgProxy.AddTurnConn(conn.ctx, udpAddr, remoteConn); err != nil { - conn.log.Errorf("failed to add turn net.Conn to local proxy: %v", err) + conn.Log.Errorf("failed to add turn net.Conn to local proxy: %v", err) return nil, err } return wgProxy, nil } func (conn *Conn) isReadyToUpgrade() bool { - return conn.wgProxyRelay != nil && conn.currentConnPriority != connPriorityRelay + return conn.wgProxyRelay != nil && conn.currentConnPriority != conntype.Relay } func (conn *Conn) isICEActive() bool { - return (conn.currentConnPriority == connPriorityICEP2P || conn.currentConnPriority == connPriorityICETurn) && conn.statusICE.Get() == StatusConnected + return (conn.currentConnPriority == conntype.ICEP2P || conn.currentConnPriority == conntype.ICETurn) && conn.statusICE.Get() == worker.StatusConnected } func (conn *Conn) removeWgPeer() error { @@ -760,10 +771,10 @@ func (conn *Conn) removeWgPeer() error { } func (conn *Conn) handleConfigurationFailure(err error, wgProxy wgproxy.Proxy) { - conn.log.Warnf("Failed to update wg peer configuration: %v", err) + conn.Log.Warnf("Failed to update wg peer configuration: %v", err) if wgProxy != nil { if ierr := wgProxy.CloseConn(); ierr != nil { - conn.log.Warnf("Failed to close wg proxy: %v", ierr) + conn.Log.Warnf("Failed to close wg proxy: %v", ierr) } } if conn.wgProxyRelay != nil { @@ -773,16 +784,16 @@ func (conn *Conn) handleConfigurationFailure(err error, wgProxy wgproxy.Proxy) { func (conn *Conn) logTraceConnState() { if conn.workerRelay.IsRelayConnectionSupportedWithPeer() { - conn.log.Tracef("connectivity guard check, relay state: %s, ice state: %s", conn.statusRelay, conn.statusICE) + conn.Log.Tracef("connectivity guard check, relay state: %s, ice state: %s", conn.statusRelay, conn.statusICE) } else { - conn.log.Tracef("connectivity guard check, ice state: %s", conn.statusICE) + conn.Log.Tracef("connectivity guard check, ice state: %s", conn.statusICE) } } func (conn *Conn) setRelayedProxy(proxy wgproxy.Proxy) { if conn.wgProxyRelay != nil { if err := conn.wgProxyRelay.CloseConn(); err != nil { - conn.log.Warnf("failed to close deprecated wg proxy conn: %v", err) + conn.Log.Warnf("failed to close deprecated wg proxy conn: %v", err) } } conn.wgProxyRelay = proxy @@ -793,6 +804,10 @@ func (conn *Conn) AllowedIP() netip.Addr { return conn.config.WgConfig.AllowedIps[0].Addr() } +func (conn *Conn) AgentVersionString() string { + return conn.config.AgentVersion +} + func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key { if conn.config.RosenpassConfig.PubKey == nil { return conn.config.WgConfig.PreSharedKey @@ -804,7 +819,7 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key { determKey, err := conn.rosenpassDetermKey() if err != nil { - conn.log.Errorf("failed to generate Rosenpass initial key: %v", err) + conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err) return conn.config.WgConfig.PreSharedKey } diff --git a/client/internal/peer/conn_status.go b/client/internal/peer/conn_status.go index 3c747864f..73acc5ef5 100644 --- a/client/internal/peer/conn_status.go +++ b/client/internal/peer/conn_status.go @@ -1,58 +1,29 @@ package peer import ( - "sync/atomic" - log "github.com/sirupsen/logrus" ) const ( - // StatusConnected indicate the peer is in connected state - StatusConnected ConnStatus = iota + // StatusIdle indicate the peer is in disconnected state + StatusIdle ConnStatus = iota // StatusConnecting indicate the peer is in connecting state StatusConnecting - // StatusDisconnected indicate the peer is in disconnected state - StatusDisconnected + // StatusConnected indicate the peer is in connected state + StatusConnected ) // ConnStatus describe the status of a peer's connection type ConnStatus int32 -// AtomicConnStatus is a thread-safe wrapper for ConnStatus -type AtomicConnStatus struct { - status atomic.Int32 -} - -// NewAtomicConnStatus creates a new AtomicConnStatus with the given initial status -func NewAtomicConnStatus() *AtomicConnStatus { - acs := &AtomicConnStatus{} - acs.Set(StatusDisconnected) - return acs -} - -// Get returns the current connection status -func (acs *AtomicConnStatus) Get() ConnStatus { - return ConnStatus(acs.status.Load()) -} - -// Set updates the connection status -func (acs *AtomicConnStatus) Set(status ConnStatus) { - acs.status.Store(int32(status)) -} - -// String returns the string representation of the current status -func (acs *AtomicConnStatus) String() string { - return acs.Get().String() -} - func (s ConnStatus) String() string { switch s { case StatusConnecting: return "Connecting" case StatusConnected: return "Connected" - case StatusDisconnected: - return "Disconnected" + case StatusIdle: + return "Idle" default: log.Errorf("unknown status: %d", s) return "INVALID_PEER_CONNECTION_STATUS" diff --git a/client/internal/peer/conn_status_test.go b/client/internal/peer/conn_status_test.go index 6088df55d..e8c5efe5f 100644 --- a/client/internal/peer/conn_status_test.go +++ b/client/internal/peer/conn_status_test.go @@ -14,7 +14,7 @@ func TestConnStatus_String(t *testing.T) { want string }{ {"StatusConnected", StatusConnected, "Connected"}, - {"StatusDisconnected", StatusDisconnected, "Disconnected"}, + {"StatusIdle", StatusIdle, "Idle"}, {"StatusConnecting", StatusConnecting, "Connecting"}, } @@ -24,5 +24,4 @@ func TestConnStatus_String(t *testing.T) { assert.Equal(t, got, table.want, "they should be equal") }) } - } diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index 6d55cfff4..c5055e646 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -1,7 +1,6 @@ package peer import ( - "context" "fmt" "os" "sync" @@ -11,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peer/guard" "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/stdnet" @@ -18,6 +18,8 @@ import ( semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" ) +var testDispatcher = dispatcher.NewConnectionDispatcher() + var connConf = ConnConfig{ Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=", LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=", @@ -48,7 +50,13 @@ func TestNewConn_interfaceFilter(t *testing.T) { func TestConn_GetKey(t *testing.T) { swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig) - conn, err := NewConn(context.Background(), connConf, nil, nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1)) + + sd := ServiceDependencies{ + SrWatcher: swWatcher, + Semaphore: semaphoregroup.NewSemaphoreGroup(1), + PeerConnDispatcher: testDispatcher, + } + conn, err := NewConn(connConf, sd) if err != nil { return } @@ -60,7 +68,13 @@ func TestConn_GetKey(t *testing.T) { func TestConn_OnRemoteOffer(t *testing.T) { swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig) - conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1)) + sd := ServiceDependencies{ + StatusRecorder: NewRecorder("https://mgm"), + SrWatcher: swWatcher, + Semaphore: semaphoregroup.NewSemaphoreGroup(1), + PeerConnDispatcher: testDispatcher, + } + conn, err := NewConn(connConf, sd) if err != nil { return } @@ -94,7 +108,13 @@ func TestConn_OnRemoteOffer(t *testing.T) { func TestConn_OnRemoteAnswer(t *testing.T) { swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig) - conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1)) + sd := ServiceDependencies{ + StatusRecorder: NewRecorder("https://mgm"), + SrWatcher: swWatcher, + Semaphore: semaphoregroup.NewSemaphoreGroup(1), + PeerConnDispatcher: testDispatcher, + } + conn, err := NewConn(connConf, sd) if err != nil { return } @@ -125,43 +145,6 @@ func TestConn_OnRemoteAnswer(t *testing.T) { wg.Wait() } -func TestConn_Status(t *testing.T) { - swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig) - conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1)) - if err != nil { - return - } - - tables := []struct { - name string - statusIce ConnStatus - statusRelay ConnStatus - want ConnStatus - }{ - {"StatusConnected", StatusConnected, StatusConnected, StatusConnected}, - {"StatusDisconnected", StatusDisconnected, StatusDisconnected, StatusDisconnected}, - {"StatusConnecting", StatusConnecting, StatusConnecting, StatusConnecting}, - {"StatusConnectingIce", StatusConnecting, StatusDisconnected, StatusConnecting}, - {"StatusConnectingIceAlternative", StatusConnecting, StatusConnected, StatusConnected}, - {"StatusConnectingRelay", StatusDisconnected, StatusConnecting, StatusConnecting}, - {"StatusConnectingRelayAlternative", StatusConnected, StatusConnecting, StatusConnected}, - } - - for _, table := range tables { - t.Run(table.name, func(t *testing.T) { - si := NewAtomicConnStatus() - si.Set(table.statusIce) - conn.statusICE = si - - sr := NewAtomicConnStatus() - sr.Set(table.statusRelay) - conn.statusRelay = sr - - got := conn.Status() - assert.Equal(t, got, table.want, "they should be equal") - }) - } -} func TestConn_presharedKey(t *testing.T) { conn1 := Conn{ diff --git a/client/internal/peer/conntype/priority.go b/client/internal/peer/conntype/priority.go new file mode 100644 index 000000000..6746ca7d4 --- /dev/null +++ b/client/internal/peer/conntype/priority.go @@ -0,0 +1,29 @@ +package conntype + +import ( + "fmt" +) + +const ( + None ConnPriority = 0 + Relay ConnPriority = 1 + ICETurn ConnPriority = 2 + ICEP2P ConnPriority = 3 +) + +type ConnPriority int + +func (cp ConnPriority) String() string { + switch cp { + case None: + return "None" + case Relay: + return "PriorityRelay" + case ICETurn: + return "PriorityICETurn" + case ICEP2P: + return "PriorityICEP2P" + default: + return fmt.Sprintf("ConnPriority(%d)", cp) + } +} diff --git a/client/internal/peer/dispatcher/dispatcher.go b/client/internal/peer/dispatcher/dispatcher.go new file mode 100644 index 000000000..06124bc35 --- /dev/null +++ b/client/internal/peer/dispatcher/dispatcher.go @@ -0,0 +1,52 @@ +package dispatcher + +import ( + "sync" + + "github.com/netbirdio/netbird/client/internal/peer/id" +) + +type ConnectionListener struct { + OnConnected func(peerID id.ConnID) + OnDisconnected func(peerID id.ConnID) +} + +type ConnectionDispatcher struct { + listeners map[*ConnectionListener]struct{} + mu sync.Mutex +} + +func NewConnectionDispatcher() *ConnectionDispatcher { + return &ConnectionDispatcher{ + listeners: make(map[*ConnectionListener]struct{}), + } +} + +func (e *ConnectionDispatcher) AddListener(listener *ConnectionListener) { + e.mu.Lock() + defer e.mu.Unlock() + e.listeners[listener] = struct{}{} +} + +func (e *ConnectionDispatcher) RemoveListener(listener *ConnectionListener) { + e.mu.Lock() + defer e.mu.Unlock() + + delete(e.listeners, listener) +} + +func (e *ConnectionDispatcher) NotifyConnected(peerConnID id.ConnID) { + e.mu.Lock() + defer e.mu.Unlock() + for listener := range e.listeners { + listener.OnConnected(peerConnID) + } +} + +func (e *ConnectionDispatcher) NotifyDisconnected(peerConnID id.ConnID) { + e.mu.Lock() + defer e.mu.Unlock() + for listener := range e.listeners { + listener.OnDisconnected(peerConnID) + } +} diff --git a/client/internal/peer/guard/guard.go b/client/internal/peer/guard/guard.go index 1fc2b4a4a..155104323 100644 --- a/client/internal/peer/guard/guard.go +++ b/client/internal/peer/guard/guard.go @@ -8,10 +8,6 @@ import ( log "github.com/sirupsen/logrus" ) -const ( - reconnectMaxElapsedTime = 30 * time.Minute -) - type isConnectedFunc func() bool // Guard is responsible for the reconnection logic. @@ -25,7 +21,6 @@ type isConnectedFunc func() bool type Guard struct { Reconnect chan struct{} log *log.Entry - isController bool isConnectedOnAllWay isConnectedFunc timeout time.Duration srWatcher *SRWatcher @@ -33,11 +28,10 @@ type Guard struct { iCEConnDisconnected chan struct{} } -func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard { +func NewGuard(log *log.Entry, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard { return &Guard{ Reconnect: make(chan struct{}, 1), log: log, - isController: isController, isConnectedOnAllWay: isConnectedFn, timeout: timeout, srWatcher: srWatcher, @@ -46,12 +40,8 @@ func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, } } -func (g *Guard) Start(ctx context.Context) { - if g.isController { - g.reconnectLoopWithRetry(ctx) - } else { - g.listenForDisconnectEvents(ctx) - } +func (g *Guard) Start(ctx context.Context, eventCallback func()) { + g.reconnectLoopWithRetry(ctx, eventCallback) } func (g *Guard) SetRelayedConnDisconnected() { @@ -68,9 +58,9 @@ func (g *Guard) SetICEConnDisconnected() { } } -// reconnectLoopWithRetry periodically check (max 30 min) the connection status. +// reconnectLoopWithRetry periodically check the connection status. // Try to send offer while the P2P is not established or while the Relay is not connected if is it supported -func (g *Guard) reconnectLoopWithRetry(ctx context.Context) { +func (g *Guard) reconnectLoopWithRetry(ctx context.Context, callback func()) { waitForInitialConnectionTry(ctx) srReconnectedChan := g.srWatcher.NewListener() @@ -93,7 +83,7 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) { } if !g.isConnectedOnAllWay() { - g.triggerOfferSending() + callback() } case <-g.relayedConnDisconnected: @@ -121,39 +111,12 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) { } } -// listenForDisconnectEvents is used when the peer is not a controller and it should reconnect to the peer -// when the connection is lost. It will try to establish a connection only once time if before the connection was established -// It track separately the ice and relay connection status. Just because a lower priority connection reestablished it does not -// mean that to switch to it. We always force to use the higher priority connection. -func (g *Guard) listenForDisconnectEvents(ctx context.Context) { - srReconnectedChan := g.srWatcher.NewListener() - defer g.srWatcher.RemoveListener(srReconnectedChan) - - g.log.Infof("start listen for reconnect events...") - for { - select { - case <-g.relayedConnDisconnected: - g.log.Debugf("Relay connection changed, triggering reconnect") - g.triggerOfferSending() - case <-g.iCEConnDisconnected: - g.log.Debugf("ICE state changed, try to send new offer") - g.triggerOfferSending() - case <-srReconnectedChan: - g.triggerOfferSending() - case <-ctx.Done(): - g.log.Debugf("context is done, stop reconnect loop") - return - } - } -} - func (g *Guard) prepareExponentTicker(ctx context.Context) *backoff.Ticker { bo := backoff.WithContext(&backoff.ExponentialBackOff{ InitialInterval: 800 * time.Millisecond, RandomizationFactor: 0.1, Multiplier: 2, MaxInterval: g.timeout, - MaxElapsedTime: reconnectMaxElapsedTime, Stop: backoff.Stop, Clock: backoff.SystemClock, }, ctx) @@ -164,13 +127,6 @@ func (g *Guard) prepareExponentTicker(ctx context.Context) *backoff.Ticker { return ticker } -func (g *Guard) triggerOfferSending() { - select { - case g.Reconnect <- struct{}{}: - default: - } -} - // Give chance to the peer to establish the initial connection. // With it, we can decrease to send necessary offer func waitForInitialConnectionTry(ctx context.Context) { diff --git a/client/internal/peer/handshaker.go b/client/internal/peer/handshaker.go index 224ea0262..bf4335fe5 100644 --- a/client/internal/peer/handshaker.go +++ b/client/internal/peer/handshaker.go @@ -43,7 +43,6 @@ type OfferAnswer struct { type Handshaker struct { mu sync.Mutex - ctx context.Context log *log.Entry config ConnConfig signaler *Signaler @@ -57,9 +56,8 @@ type Handshaker struct { remoteAnswerCh chan OfferAnswer } -func NewHandshaker(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ice *WorkerICE, relay *WorkerRelay) *Handshaker { +func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *WorkerICE, relay *WorkerRelay) *Handshaker { return &Handshaker{ - ctx: ctx, log: log, config: config, signaler: signaler, @@ -74,10 +72,10 @@ func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAn h.onNewOfferListeners = append(h.onNewOfferListeners, offer) } -func (h *Handshaker) Listen() { +func (h *Handshaker) Listen(ctx context.Context) { for { h.log.Info("wait for remote offer confirmation") - remoteOfferAnswer, err := h.waitForRemoteOfferConfirmation() + remoteOfferAnswer, err := h.waitForRemoteOfferConfirmation(ctx) if err != nil { var connectionClosedError *ConnectionClosedError if errors.As(err, &connectionClosedError) { @@ -127,7 +125,7 @@ func (h *Handshaker) OnRemoteAnswer(answer OfferAnswer) bool { } } -func (h *Handshaker) waitForRemoteOfferConfirmation() (*OfferAnswer, error) { +func (h *Handshaker) waitForRemoteOfferConfirmation(ctx context.Context) (*OfferAnswer, error) { select { case remoteOfferAnswer := <-h.remoteOffersCh: // received confirmation from the remote peer -> ready to proceed @@ -137,7 +135,7 @@ func (h *Handshaker) waitForRemoteOfferConfirmation() (*OfferAnswer, error) { return &remoteOfferAnswer, nil case remoteOfferAnswer := <-h.remoteAnswerCh: return &remoteOfferAnswer, nil - case <-h.ctx.Done(): + case <-ctx.Done(): // closed externally return nil, NewConnectionClosedError(h.config.Key) } diff --git a/client/internal/peer/id/connid.go b/client/internal/peer/id/connid.go new file mode 100644 index 000000000..43c4c7300 --- /dev/null +++ b/client/internal/peer/id/connid.go @@ -0,0 +1,5 @@ +package id + +import "unsafe" + +type ConnID unsafe.Pointer diff --git a/client/internal/peer/iface.go b/client/internal/peer/iface.go index 32ac5c7db..0bcc7a68e 100644 --- a/client/internal/peer/iface.go +++ b/client/internal/peer/iface.go @@ -15,7 +15,7 @@ import ( type WGIface interface { UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error RemovePeer(peerKey string) error - GetStats(peerKey string) (configurer.WGStats, error) + GetStats() (map[string]configurer.WGStats, error) GetProxy() wgproxy.Proxy Address() wgaddr.Address } diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 3eca6a8c9..69e333bf1 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -135,14 +135,15 @@ type NSGroupState struct { // FullStatus contains the full state held by the Status instance type FullStatus struct { - Peers []State - ManagementState ManagementState - SignalState SignalState - LocalPeerState LocalPeerState - RosenpassState RosenpassState - Relays []relay.ProbeResult - NSGroupStates []NSGroupState - NumOfForwardingRules int + Peers []State + ManagementState ManagementState + SignalState SignalState + LocalPeerState LocalPeerState + RosenpassState RosenpassState + Relays []relay.ProbeResult + NSGroupStates []NSGroupState + NumOfForwardingRules int + LazyConnectionEnabled bool } // Status holds a state of peers, signal, management connections and relays @@ -164,6 +165,7 @@ type Status struct { rosenpassPermissive bool nsGroupStates []NSGroupState resolvedDomainsStates map[domain.Domain]ResolvedDomainInfo + lazyConnectionEnabled bool // To reduce the number of notification invocation this bool will be true when need to call the notification // Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events @@ -219,7 +221,7 @@ func (d *Status) ReplaceOfflinePeers(replacement []State) { } // AddPeer adds peer to Daemon status map -func (d *Status) AddPeer(peerPubKey string, fqdn string) error { +func (d *Status) AddPeer(peerPubKey string, fqdn string, ip string) error { d.mux.Lock() defer d.mux.Unlock() @@ -229,7 +231,8 @@ func (d *Status) AddPeer(peerPubKey string, fqdn string) error { } d.peers[peerPubKey] = State{ PubKey: peerPubKey, - ConnStatus: StatusDisconnected, + IP: ip, + ConnStatus: StatusIdle, FQDN: fqdn, Mux: new(sync.RWMutex), } @@ -511,9 +514,9 @@ func shouldSkipNotify(receivedConnStatus ConnStatus, curr State) bool { switch { case receivedConnStatus == StatusConnecting: return true - case receivedConnStatus == StatusDisconnected && curr.ConnStatus == StatusConnecting: + case receivedConnStatus == StatusIdle && curr.ConnStatus == StatusConnecting: return true - case receivedConnStatus == StatusDisconnected && curr.ConnStatus == StatusDisconnected: + case receivedConnStatus == StatusIdle && curr.ConnStatus == StatusIdle: return curr.IP != "" default: return false @@ -689,6 +692,12 @@ func (d *Status) UpdateRosenpass(rosenpassEnabled, rosenpassPermissive bool) { d.rosenpassEnabled = rosenpassEnabled } +func (d *Status) UpdateLazyConnection(enabled bool) { + d.mux.Lock() + defer d.mux.Unlock() + d.lazyConnectionEnabled = enabled +} + // MarkSignalDisconnected sets SignalState to disconnected func (d *Status) MarkSignalDisconnected(err error) { d.mux.Lock() @@ -761,6 +770,12 @@ func (d *Status) GetRosenpassState() RosenpassState { } } +func (d *Status) GetLazyConnection() bool { + d.mux.Lock() + defer d.mux.Unlock() + return d.lazyConnectionEnabled +} + func (d *Status) GetManagementState() ManagementState { d.mux.Lock() defer d.mux.Unlock() @@ -872,12 +887,13 @@ func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo // GetFullStatus gets full status func (d *Status) GetFullStatus() FullStatus { fullStatus := FullStatus{ - ManagementState: d.GetManagementState(), - SignalState: d.GetSignalState(), - Relays: d.GetRelayStates(), - RosenpassState: d.GetRosenpassState(), - NSGroupStates: d.GetDNSStates(), - NumOfForwardingRules: len(d.ForwardingRules()), + ManagementState: d.GetManagementState(), + SignalState: d.GetSignalState(), + Relays: d.GetRelayStates(), + RosenpassState: d.GetRosenpassState(), + NSGroupStates: d.GetDNSStates(), + NumOfForwardingRules: len(d.ForwardingRules()), + LazyConnectionEnabled: d.GetLazyConnection(), } d.mux.Lock() diff --git a/client/internal/peer/status_test.go b/client/internal/peer/status_test.go index 931ec9005..bdf8f087a 100644 --- a/client/internal/peer/status_test.go +++ b/client/internal/peer/status_test.go @@ -10,22 +10,24 @@ import ( func TestAddPeer(t *testing.T) { key := "abc" + ip := "100.108.254.1" status := NewRecorder("https://mgm") - err := status.AddPeer(key, "abc.netbird") + err := status.AddPeer(key, "abc.netbird", ip) assert.NoError(t, err, "shouldn't return error") _, exists := status.peers[key] assert.True(t, exists, "value was found") - err = status.AddPeer(key, "abc.netbird") + err = status.AddPeer(key, "abc.netbird", ip) assert.Error(t, err, "should return error on duplicate") } func TestGetPeer(t *testing.T) { key := "abc" + ip := "100.108.254.1" status := NewRecorder("https://mgm") - err := status.AddPeer(key, "abc.netbird") + err := status.AddPeer(key, "abc.netbird", ip) assert.NoError(t, err, "shouldn't return error") peerStatus, err := status.GetPeer(key) diff --git a/client/internal/peer/wg_watcher.go b/client/internal/peer/wg_watcher.go index 589f405bc..218872c15 100644 --- a/client/internal/peer/wg_watcher.go +++ b/client/internal/peer/wg_watcher.go @@ -2,6 +2,7 @@ package peer import ( "context" + "fmt" "sync" "time" @@ -20,7 +21,7 @@ var ( ) type WGInterfaceStater interface { - GetStats(key string) (configurer.WGStats, error) + GetStats() (map[string]configurer.WGStats, error) } type WGWatcher struct { @@ -146,9 +147,13 @@ func (w *WGWatcher) handshakeCheck(lastHandshake time.Time) (*time.Time, bool) { } func (w *WGWatcher) wgState() (time.Time, error) { - wgState, err := w.wgIfaceStater.GetStats(w.peerKey) + wgStates, err := w.wgIfaceStater.GetStats() if err != nil { return time.Time{}, err } + wgState, ok := wgStates[w.peerKey] + if !ok { + return time.Time{}, fmt.Errorf("peer %s not found in WireGuard endpoints", w.peerKey) + } return wgState.LastHandshake, nil } diff --git a/client/internal/peer/wg_watcher_test.go b/client/internal/peer/wg_watcher_test.go index 8bfb1af4c..d7c277eff 100644 --- a/client/internal/peer/wg_watcher_test.go +++ b/client/internal/peer/wg_watcher_test.go @@ -11,26 +11,11 @@ import ( ) type MocWgIface struct { - initial bool - lastHandshake time.Time - stop bool + stop bool } -func (m *MocWgIface) GetStats(key string) (configurer.WGStats, error) { - if !m.initial { - m.initial = true - return configurer.WGStats{}, nil - } - - if !m.stop { - m.lastHandshake = time.Now() - } - - stats := configurer.WGStats{ - LastHandshake: m.lastHandshake, - } - - return stats, nil +func (m *MocWgIface) GetStats() (map[string]configurer.WGStats, error) { + return map[string]configurer.WGStats{}, nil } func (m *MocWgIface) disconnect() { diff --git a/client/internal/peer/worker/state.go b/client/internal/peer/worker/state.go new file mode 100644 index 000000000..14b53aa4e --- /dev/null +++ b/client/internal/peer/worker/state.go @@ -0,0 +1,55 @@ +package worker + +import ( + "sync/atomic" + + log "github.com/sirupsen/logrus" +) + +const ( + StatusDisconnected Status = iota + StatusConnected +) + +type Status int32 + +func (s Status) String() string { + switch s { + case StatusDisconnected: + return "Disconnected" + case StatusConnected: + return "Connected" + default: + log.Errorf("unknown status: %d", s) + return "unknown" + } +} + +// AtomicWorkerStatus is a thread-safe wrapper for worker status +type AtomicWorkerStatus struct { + status atomic.Int32 +} + +func NewAtomicStatus() *AtomicWorkerStatus { + acs := &AtomicWorkerStatus{} + acs.SetDisconnected() + return acs +} + +// Get returns the current connection status +func (acs *AtomicWorkerStatus) Get() Status { + return Status(acs.status.Load()) +} + +func (acs *AtomicWorkerStatus) SetConnected() { + acs.status.Store(int32(StatusConnected)) +} + +func (acs *AtomicWorkerStatus) SetDisconnected() { + acs.status.Store(int32(StatusDisconnected)) +} + +// String returns the string representation of the current status +func (acs *AtomicWorkerStatus) String() string { + return acs.Get().String() +} diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go index 4ff13b455..e99c50d25 100644 --- a/client/internal/peer/worker_ice.go +++ b/client/internal/peer/worker_ice.go @@ -14,6 +14,7 @@ import ( "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" + "github.com/netbirdio/netbird/client/internal/peer/conntype" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/route" @@ -397,10 +398,10 @@ func isRelayed(pair *ice.CandidatePair) bool { return false } -func selectedPriority(pair *ice.CandidatePair) ConnPriority { +func selectedPriority(pair *ice.CandidatePair) conntype.ConnPriority { if isRelayed(pair) { - return connPriorityICETurn + return conntype.ICETurn } else { - return connPriorityICEP2P + return conntype.ICEP2P } } diff --git a/client/internal/peerstore/store.go b/client/internal/peerstore/store.go index 15d34d3d0..81ac7a5b6 100644 --- a/client/internal/peerstore/store.go +++ b/client/internal/peerstore/store.go @@ -1,6 +1,7 @@ package peerstore import ( + "context" "net/netip" "sync" @@ -79,6 +80,32 @@ func (s *Store) PeerConn(pubKey string) (*peer.Conn, bool) { return p, true } +func (s *Store) PeerConnOpen(ctx context.Context, pubKey string) { + s.peerConnsMu.RLock() + defer s.peerConnsMu.RUnlock() + + p, ok := s.peerConns[pubKey] + if !ok { + return + } + // this can be blocked because of the connect open limiter semaphore + if err := p.Open(ctx); err != nil { + p.Log.Errorf("failed to open peer connection: %v", err) + } + +} + +func (s *Store) PeerConnClose(pubKey string) { + s.peerConnsMu.RLock() + defer s.peerConnsMu.RUnlock() + + p, ok := s.peerConns[pubKey] + if !ok { + return + } + p.Close() +} + func (s *Store) PeersPubKey() []string { s.peerConnsMu.RLock() defer s.peerConnsMu.RUnlock() diff --git a/client/internal/routemanager/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go index 9e1f8058a..02d3f4c6f 100644 --- a/client/internal/routemanager/iface/iface_common.go +++ b/client/internal/routemanager/iface/iface_common.go @@ -3,7 +3,6 @@ package iface import ( "net" - "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" ) @@ -18,5 +17,4 @@ type wgIfaceBase interface { IsUserspaceBind() bool GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetStats(peerKey string) (configurer.WGStats, error) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index a4e8b3e9f..402dd2f9a 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.9 +// protoc v3.21.12 // source: daemon.proto package proto @@ -277,7 +277,8 @@ type LoginRequest struct { // cleanDNSLabels clean map list of DNS labels. // This is needed because the generated code // omits initialized empty slices due to omitempty tags - CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` + CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` + LazyConnectionEnabled *bool `protobuf:"varint,28,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"` } func (x *LoginRequest) Reset() { @@ -502,6 +503,13 @@ func (x *LoginRequest) GetCleanDNSLabels() bool { return false } +func (x *LoginRequest) GetLazyConnectionEnabled() bool { + if x != nil && x.LazyConnectionEnabled != nil { + return *x.LazyConnectionEnabled + } + return false +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1657,6 +1665,7 @@ type FullStatus struct { DnsServers []*NSGroupState `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"` NumberOfForwardingRules int32 `protobuf:"varint,8,opt,name=NumberOfForwardingRules,proto3" json:"NumberOfForwardingRules,omitempty"` Events []*SystemEvent `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"` + LazyConnectionEnabled bool `protobuf:"varint,9,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` } func (x *FullStatus) Reset() { @@ -1747,6 +1756,13 @@ func (x *FullStatus) GetEvents() []*SystemEvent { return nil } +func (x *FullStatus) GetLazyConnectionEnabled() bool { + if x != nil { + return x.LazyConnectionEnabled + } + return false +} + // Networks type ListNetworksRequest struct { state protoimpl.MessageState @@ -3609,7 +3625,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0e, 0x0a, 0x0c, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xae, 0x0c, 0x0a, 0x0c, 0x4c, 0x6f, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x83, 0x0d, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, @@ -3688,511 +3704,519 @@ var file_daemon_proto_rawDesc = []byte{ 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, - 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, - 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, - 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, - 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, - 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, - 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, - 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, - 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, - 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, - 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, - 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, - 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, - 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, - 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, - 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, - 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, - 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, - 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, - 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, - 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, - 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, - 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, - 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, - 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, - 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, - 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, - 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, - 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, - 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, 0x05, 0x0a, - 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, - 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, - 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, - 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, - 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, - 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, - 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, - 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, - 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, - 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, - 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, - 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, - 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, - 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, - 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, - 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, - 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, - 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, - 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, - 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, - 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xb9, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, - 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, - 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, - 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, - 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, - 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, - 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, - 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, - 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, - 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, - 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, - 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, - 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, - 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, - 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, - 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, - 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, - 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, - 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, - 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, - 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, - 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, - 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, - 0x22, 0x88, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, - 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, - 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, - 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, - 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, 0x44, - 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, - 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, - 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, + 0x62, 0x65, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x1c, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x0f, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, + 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, + 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, + 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, + 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, + 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, + 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, + 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, + 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, + 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, + 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, + 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, + 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, + 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, + 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, + 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, + 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, + 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, + 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, + 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, + 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0xee, 0x03, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, + 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, + 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, + 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, + 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, + 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, + 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, + 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, + 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, + 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, + 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, + 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, + 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, + 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, + 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, + 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, + 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, + 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, + 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, + 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, + 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, + 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, + 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, + 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xef, 0x03, 0x0a, 0x0a, + 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, + 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, + 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, + 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, + 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x15, 0x0a, + 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, + 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, + 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, + 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, + 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, + 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, + 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, + 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, + 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, + 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, + 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, + 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, + 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x88, 0x01, 0x0a, 0x12, + 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, + 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, - 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, - 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, - 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, - 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, - 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, - 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, - 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, - 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, - 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, - 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, - 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, - 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, - 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, - 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, - 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, - 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, - 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, - 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, - 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, - 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, - 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, - 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, - 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, - 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, - 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, - 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, - 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, - 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, - 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, - 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, - 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, - 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, - 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, - 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, - 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, - 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, - 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, - 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, - 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, - 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, - 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, - 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, - 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, - 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, - 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, - 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, - 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, - 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, - 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, - 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, - 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, - 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, - 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, - 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, + 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, + 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, + 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, + 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, + 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, + 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, + 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, + 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, + 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, + 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, + 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, + 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, + 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, + 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, + 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, + 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, + 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, + 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, + 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, + 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, + 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, + 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, + 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, + 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, + 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, + 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, + 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, + 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, + 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, + 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, + 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, + 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, + 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, 0x0a, 0x0d, 0x44, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, + 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, + 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, + 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, + 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, - 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, - 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, - 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, - 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, - 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, - 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, - 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, - 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, + 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, + 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, + 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, - 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, - 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, + 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, + 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, + 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 282c307d6..6fa391c8e 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -134,6 +134,7 @@ message LoginRequest { // omits initialized empty slices due to omitempty tags bool cleanDNSLabels = 27; + optional bool lazyConnectionEnabled = 28; } message LoginResponse { @@ -274,6 +275,8 @@ message FullStatus { int32 NumberOfForwardingRules = 8; repeated SystemEvent events = 7; + + bool lazyConnectionEnabled = 9; } // Networks diff --git a/client/server/server.go b/client/server/server.go index 5bc008fec..43b3eb3b7 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -139,6 +139,7 @@ func (s *Server) Start() error { s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive) + s.statusRecorder.UpdateLazyConnection(config.LazyConnectionEnabled) if s.sessionWatcher == nil { s.sessionWatcher = internal.NewSessionWatcher(s.rootCtx, s.statusRecorder) @@ -417,6 +418,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro s.latestConfigInput.DisableNotifications = msg.DisableNotifications } + if msg.LazyConnectionEnabled != nil { + inputConfig.LazyConnectionEnabled = msg.LazyConnectionEnabled + s.latestConfigInput.LazyConnectionEnabled = msg.LazyConnectionEnabled + } + s.mutex.Unlock() if msg.OptionalPreSharedKey != nil { @@ -804,6 +810,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled pbFullStatus.LocalPeerState.Networks = maps.Keys(fullStatus.LocalPeerState.Routes) pbFullStatus.NumberOfForwardingRules = int32(fullStatus.NumOfForwardingRules) + pbFullStatus.LazyConnectionEnabled = fullStatus.LazyConnectionEnabled for _, peerState := range fullStatus.Peers { pbPeerState := &proto.PeerState{ diff --git a/client/status/status.go b/client/status/status.go index f37e5b0f0..18056e363 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -97,6 +97,7 @@ type OutputOverview struct { NumberOfForwardingRules int `json:"forwardingRules" yaml:"forwardingRules"` NSServerGroups []NsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` Events []SystemEventOutput `json:"events" yaml:"events"` + LazyConnectionEnabled bool `json:"lazyConnectionEnabled" yaml:"lazyConnectionEnabled"` } func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) OutputOverview { @@ -136,6 +137,7 @@ func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, status NumberOfForwardingRules: int(pbFullStatus.GetNumberOfForwardingRules()), NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), Events: mapEvents(pbFullStatus.GetEvents()), + LazyConnectionEnabled: pbFullStatus.GetLazyConnectionEnabled(), } if anon { @@ -206,7 +208,7 @@ func mapPeers( transferSent := int64(0) isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() - if skipDetailByFilters(pbPeerState, isPeerConnected, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) { + if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) { continue } if isPeerConnected { @@ -384,6 +386,11 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, } } + lazyConnectionEnabledStatus := "false" + if overview.LazyConnectionEnabled { + lazyConnectionEnabledStatus = "true" + } + peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) goos := runtime.GOOS @@ -405,6 +412,7 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, "NetBird IP: %s\n"+ "Interface type: %s\n"+ "Quantum resistance: %s\n"+ + "Lazy connection: %s\n"+ "Networks: %s\n"+ "Forwarding rules: %d\n"+ "Peers count: %s\n", @@ -419,6 +427,7 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, interfaceIP, interfaceTypeString, rosenpassEnabledStatus, + lazyConnectionEnabledStatus, networks, overview.NumberOfForwardingRules, peersCountString, @@ -533,23 +542,13 @@ func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bo return peersString } -func skipDetailByFilters( - peerState *proto.PeerState, - isConnected bool, - statusFilter string, - prefixNamesFilter []string, - prefixNamesFilterMap map[string]struct{}, - ipsFilter map[string]struct{}, -) bool { +func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) bool { statusEval := false ipEval := false nameEval := true if statusFilter != "" { - lowerStatusFilter := strings.ToLower(statusFilter) - if lowerStatusFilter == "disconnected" && isConnected { - statusEval = true - } else if lowerStatusFilter == "connected" && !isConnected { + if !strings.EqualFold(peerStatus, statusFilter) { statusEval = true } } diff --git a/client/status/status_test.go b/client/status/status_test.go index e48b441f5..33eda4b9e 100644 --- a/client/status/status_test.go +++ b/client/status/status_test.go @@ -383,7 +383,8 @@ func TestParsingToJSON(t *testing.T) { "error": "timeout" } ], - "events": [] + "events": [], + "lazyConnectionEnabled": false }` // @formatter:on @@ -484,6 +485,7 @@ dnsServers: enabled: false error: timeout events: [] +lazyConnectionEnabled: false ` assert.Equal(t, expectedYAML, yaml) @@ -548,6 +550,7 @@ FQDN: some-localhost.awesome-domain.com NetBird IP: 192.168.178.100/16 Interface type: Kernel Quantum resistance: false +Lazy connection: false Networks: 10.10.0.0/24 Forwarding rules: 0 Peers count: 2/2 Connected @@ -570,6 +573,7 @@ FQDN: some-localhost.awesome-domain.com NetBird IP: 192.168.178.100/16 Interface type: Kernel Quantum resistance: false +Lazy connection: false Networks: 10.10.0.0/24 Forwarding rules: 0 Peers count: 2/2 Connected diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index b9fefe518..c23b78582 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -193,6 +193,7 @@ type serviceClient struct { mAllowSSH *systray.MenuItem mAutoConnect *systray.MenuItem mEnableRosenpass *systray.MenuItem + mLazyConnEnabled *systray.MenuItem mNotifications *systray.MenuItem mAdvancedSettings *systray.MenuItem mCreateDebugBundle *systray.MenuItem @@ -632,6 +633,7 @@ func (s *serviceClient) onTrayReady() { s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false) + s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable lazy connection", lazyConnMenuDescr, false) s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", notificationsMenuDescr, false) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", advancedSettingsMenuDescr) s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr) @@ -691,104 +693,114 @@ func (s *serviceClient) onTrayReady() { go s.eventManager.Start(s.ctx) - go func() { - for { - select { - case <-s.mUp.ClickedCh: - s.mUp.Disable() - go func() { - defer s.mUp.Enable() - err := s.menuUpClick() - if err != nil { - s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) - return - } - }() - case <-s.mDown.ClickedCh: - s.mDown.Disable() - go func() { - defer s.mDown.Enable() - err := s.menuDownClick() - if err != nil { - s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) - return - } - }() - case <-s.mAllowSSH.ClickedCh: - if s.mAllowSSH.Checked() { - s.mAllowSSH.Uncheck() - } else { - s.mAllowSSH.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mAutoConnect.ClickedCh: - if s.mAutoConnect.Checked() { - s.mAutoConnect.Uncheck() - } else { - s.mAutoConnect.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mEnableRosenpass.ClickedCh: - if s.mEnableRosenpass.Checked() { - s.mEnableRosenpass.Uncheck() - } else { - s.mEnableRosenpass.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mAdvancedSettings.ClickedCh: - s.mAdvancedSettings.Disable() - go func() { - defer s.mAdvancedSettings.Enable() - defer s.getSrvConfig() - s.runSelfCommand("settings", "true") - }() - case <-s.mCreateDebugBundle.ClickedCh: - s.mCreateDebugBundle.Disable() - go func() { - defer s.mCreateDebugBundle.Enable() - s.runSelfCommand("debug", "true") - }() - case <-s.mQuit.ClickedCh: - systray.Quit() - return - case <-s.mGitHub.ClickedCh: - err := openURL("https://github.com/netbirdio/netbird") - if err != nil { - log.Errorf("%s", err) - } - case <-s.mUpdate.ClickedCh: - err := openURL(version.DownloadUrl()) - if err != nil { - log.Errorf("%s", err) - } - case <-s.mNetworks.ClickedCh: - s.mNetworks.Disable() - go func() { - defer s.mNetworks.Enable() - s.runSelfCommand("networks", "true") - }() - case <-s.mNotifications.ClickedCh: - if s.mNotifications.Checked() { - s.mNotifications.Uncheck() - } else { - s.mNotifications.Check() - } - if s.eventManager != nil { - s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - } + go s.listenEvents() +} +func (s *serviceClient) listenEvents() { + for { + select { + case <-s.mUp.ClickedCh: + s.mUp.Disable() + go func() { + defer s.mUp.Enable() + err := s.menuUpClick() + if err != nil { + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) + return + } + }() + case <-s.mDown.ClickedCh: + s.mDown.Disable() + go func() { + defer s.mDown.Enable() + err := s.menuDownClick() + if err != nil { + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) + return + } + }() + case <-s.mAllowSSH.ClickedCh: + if s.mAllowSSH.Checked() { + s.mAllowSSH.Uncheck() + } else { + s.mAllowSSH.Check() + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } + case <-s.mAutoConnect.ClickedCh: + if s.mAutoConnect.Checked() { + s.mAutoConnect.Uncheck() + } else { + s.mAutoConnect.Check() + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } + case <-s.mEnableRosenpass.ClickedCh: + if s.mEnableRosenpass.Checked() { + s.mEnableRosenpass.Uncheck() + } else { + s.mEnableRosenpass.Check() + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } + case <-s.mLazyConnEnabled.ClickedCh: + if s.mLazyConnEnabled.Checked() { + s.mLazyConnEnabled.Uncheck() + } else { + s.mLazyConnEnabled.Check() + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } + case <-s.mAdvancedSettings.ClickedCh: + s.mAdvancedSettings.Disable() + go func() { + defer s.mAdvancedSettings.Enable() + defer s.getSrvConfig() + s.runSelfCommand("settings", "true") + }() + case <-s.mCreateDebugBundle.ClickedCh: + s.mCreateDebugBundle.Disable() + go func() { + defer s.mCreateDebugBundle.Enable() + s.runSelfCommand("debug", "true") + }() + case <-s.mQuit.ClickedCh: + systray.Quit() + return + case <-s.mGitHub.ClickedCh: + err := openURL("https://github.com/netbirdio/netbird") + if err != nil { + log.Errorf("%s", err) + } + case <-s.mUpdate.ClickedCh: + err := openURL(version.DownloadUrl()) + if err != nil { + log.Errorf("%s", err) + } + case <-s.mNetworks.ClickedCh: + s.mNetworks.Disable() + go func() { + defer s.mNetworks.Enable() + s.runSelfCommand("networks", "true") + }() + case <-s.mNotifications.ClickedCh: + if s.mNotifications.Checked() { + s.mNotifications.Uncheck() + } else { + s.mNotifications.Check() + } + if s.eventManager != nil { + s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } } - }() + } } func (s *serviceClient) runSelfCommand(command, arg string) { @@ -1020,6 +1032,7 @@ func (s *serviceClient) updateConfig() error { sshAllowed := s.mAllowSSH.Checked() rosenpassEnabled := s.mEnableRosenpass.Checked() notificationsDisabled := !s.mNotifications.Checked() + lazyConnectionEnabled := s.mLazyConnEnabled.Checked() loginRequest := proto.LoginRequest{ IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", @@ -1027,6 +1040,7 @@ func (s *serviceClient) updateConfig() error { RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, DisableNotifications: ¬ificationsDisabled, + LazyConnectionEnabled: &lazyConnectionEnabled, } if err := s.restartClient(&loginRequest); err != nil { diff --git a/client/ui/const.go b/client/ui/const.go index 0253750d1..cd4e7db8e 100644 --- a/client/ui/const.go +++ b/client/ui/const.go @@ -5,6 +5,7 @@ const ( allowSSHMenuDescr = "Allow SSH connections" autoConnectMenuDescr = "Connect automatically when the service starts" quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass" + lazyConnMenuDescr = "[Experimental] Enable lazy connect" notificationsMenuDescr = "Enable notifications" advancedSettingsMenuDescr = "Advanced settings of the application" debugBundleMenuDescr = "Create and open debug information bundle" diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index ceec3f643..11e72d6ee 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v5.29.3 +// protoc v3.21.12 // source: management.proto package proto @@ -1624,6 +1624,7 @@ type PeerConfig struct { // Peer fully qualified domain name Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` RoutingPeerDnsResolutionEnabled bool `protobuf:"varint,5,opt,name=RoutingPeerDnsResolutionEnabled,proto3" json:"RoutingPeerDnsResolutionEnabled,omitempty"` + LazyConnectionEnabled bool `protobuf:"varint,6,opt,name=LazyConnectionEnabled,proto3" json:"LazyConnectionEnabled,omitempty"` } func (x *PeerConfig) Reset() { @@ -1693,6 +1694,13 @@ func (x *PeerConfig) GetRoutingPeerDnsResolutionEnabled() bool { return false } +func (x *PeerConfig) GetLazyConnectionEnabled() bool { + if x != nil { + return x.LazyConnectionEnabled + } + return false +} + // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections type NetworkMap struct { state protoimpl.MessageState @@ -1856,7 +1864,8 @@ type RemotePeerConfig struct { // SSHConfig is a SSH config of the remote peer. SSHConfig.sshPubKey should be ignored because peer knows it's SSH key. SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"` // Peer fully qualified domain name - Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + AgentVersion string `protobuf:"bytes,5,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"` } func (x *RemotePeerConfig) Reset() { @@ -1919,6 +1928,13 @@ func (x *RemotePeerConfig) GetFqdn() string { return "" } +func (x *RemotePeerConfig) GetAgentVersion() string { + if x != nil { + return x.AgentVersion + } + return "" +} + // SSHConfig represents SSH configurations of a peer. type SSHConfig struct { state protoimpl.MessageState @@ -3512,7 +3528,7 @@ var file_management_proto_rawDesc = []byte{ 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x02, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, @@ -3525,287 +3541,293 @@ var file_management_proto_rawDesc = []byte{ 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, - 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, - 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 0x0a, + 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, + 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, + 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, + 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, - 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, - 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, - 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, - 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, - 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, - 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, - 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, - 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, - 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, - 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, - 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, - 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, - 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, - 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, - 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, - 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, - 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, - 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, - 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, - 0x6c, 0x61, 0x67, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, - 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, - 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, - 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, - 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, - 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, - 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, - 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, - 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, - 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, - 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, - 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, - 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, - 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, - 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, - 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, - 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, - 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, - 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, - 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, - 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, - 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, - 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, - 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, - 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, - 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, - 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, - 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, - 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, - 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, - 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, - 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, - 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, - 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, - 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, + 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, + 0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, + 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, + 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, + 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, + 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, + 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, + 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, + 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, + 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, + 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x22, 0xed, 0x01, + 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, + 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, + 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, + 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, + 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, + 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, + 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, + 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, + 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, + 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, + 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, + 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, + 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, + 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, + 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, + 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, + 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, + 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, + 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, + 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, + 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, + 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, + 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, + 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, + 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, + 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, + 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, + 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, + 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, + 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, + 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, + 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, + 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, + 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, + 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, - 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, - 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, + 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, - 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, + 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, - 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, + 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management/proto/management.proto b/management/proto/management.proto index 41a01fce4..3f985d5fe 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -254,6 +254,8 @@ message PeerConfig { string fqdn = 4; bool RoutingPeerDnsResolutionEnabled = 5; + + bool LazyConnectionEnabled = 6; } // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections @@ -312,6 +314,7 @@ message RemotePeerConfig { // Peer fully qualified domain name string fqdn = 4; + string agentVersion = 5; } // SSHConfig represents SSH configurations of a peer. diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 178120002..3f89ac4f9 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -691,10 +691,11 @@ func toSyncResponse(ctx context.Context, config *types.Config, peer *nbpeer.Peer func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig { for _, rPeer := range peers { dst = append(dst, &proto.RemotePeerConfig{ - WgPubKey: rPeer.Key, - AllowedIps: []string{rPeer.IP.String() + "/32"}, - SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)}, - Fqdn: rPeer.FQDN(dnsName), + WgPubKey: rPeer.Key, + AllowedIps: []string{rPeer.IP.String() + "/32"}, + SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)}, + Fqdn: rPeer.FQDN(dnsName), + AgentVersion: rPeer.Meta.WtVersion, }) } return dst From e806d9de38a41016d2e1a519f88cf8bbf2f3ba44 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 21 May 2025 13:48:55 +0200 Subject: [PATCH 022/213] [client] Fix legacy routes when connecting to management servers older than v0.30.0 (#3854) --- client/internal/acl/manager.go | 6 ------ client/internal/engine.go | 8 ++++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 6fa35d5c2..a6316d7a2 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -76,12 +76,6 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRout d.applyPeerACLs(networkMap) - // If we got empty rules list but management did not set the networkMap.FirewallRulesIsEmpty flag, - // then the mgmt server is older than the client, and we need to allow all traffic for routes - isLegacy := len(networkMap.RoutesFirewallRules) == 0 && !networkMap.RoutesFirewallRulesIsEmpty - if err := d.firewall.SetLegacyManagement(isLegacy); err != nil { - log.Errorf("failed to set legacy management flag: %v", err) - } if err := d.applyRouteACLs(networkMap.RoutesFirewallRules, dnsRouteFeatureFlag); err != nil { log.Errorf("Failed to apply route ACLs: %v", err) diff --git a/client/internal/engine.go b/client/internal/engine.go index eefd28225..7c501e5aa 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -978,6 +978,14 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { log.Errorf("failed to update local IPs: %v", err) } } + + // If we got empty rules list but management did not set the networkMap.FirewallRulesIsEmpty flag, + // then the mgmt server is older than the client, and we need to allow all traffic for routes. + // This needs to be toggled before applying routes. + isLegacy := len(networkMap.RoutesFirewallRules) == 0 && !networkMap.RoutesFirewallRulesIsEmpty + if err := e.firewall.SetLegacyManagement(isLegacy); err != nil { + log.Errorf("failed to set legacy management flag: %v", err) + } } dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap) From e9108ffe6cfba0ff41b0b1565934461f962ff80c Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Wed, 21 May 2025 17:50:54 +0300 Subject: [PATCH 023/213] [client] Add latest gzipped rotated log file to the debug bundle (#3848) [client] Add latest gzipped rotated log file to the debug bundle --- client/internal/debug/debug.go | 71 ++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index d1941d678..2192872df 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bufio" "bytes" + "compress/gzip" "encoding/json" "errors" "fmt" @@ -534,6 +535,33 @@ func (g *BundleGenerator) addLogfile() error { return fmt.Errorf("add client log file to zip: %w", err) } + // add latest rotated log file + pattern := filepath.Join(logDir, "client-*.log.gz") + files, err := filepath.Glob(pattern) + if err != nil { + log.Warnf("failed to glob rotated logs: %v", err) + } else if len(files) > 0 { + // pick the file with the latest ModTime + sort.Slice(files, func(i, j int) bool { + fi, err := os.Stat(files[i]) + if err != nil { + log.Warnf("failed to stat rotated log %s: %v", files[i], err) + return false + } + fj, err := os.Stat(files[j]) + if err != nil { + log.Warnf("failed to stat rotated log %s: %v", files[j], err) + return false + } + return fi.ModTime().Before(fj.ModTime()) + }) + latest := files[len(files)-1] + name := filepath.Base(latest) + if err := g.addSingleLogFileGz(latest, name); err != nil { + log.Warnf("failed to add rotated log %s: %v", name, err) + } + } + stdErrLogPath := filepath.Join(logDir, errorLogFile) stdoutLogPath := filepath.Join(logDir, stdoutLogFile) if runtime.GOOS == "darwin" { @@ -564,16 +592,13 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error { } }() - var logReader io.Reader + var logReader io.Reader = logFile if g.anonymize { var writer *io.PipeWriter logReader, writer = io.Pipe() go anonymizeLog(logFile, writer, g.anonymizer) - } else { - logReader = logFile } - if err := g.addFileToZip(logReader, targetName); err != nil { return fmt.Errorf("add %s to zip: %w", targetName, err) } @@ -581,6 +606,44 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error { return nil } +// addSingleLogFileGz adds a single gzipped log file to the archive +func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error { + f, err := os.Open(logPath) + if err != nil { + return fmt.Errorf("open gz log file %s: %w", targetName, err) + } + defer f.Close() + + gzr, err := gzip.NewReader(f) + if err != nil { + return fmt.Errorf("create gzip reader: %w", err) + } + defer gzr.Close() + + var logReader io.Reader = gzr + if g.anonymize { + var pw *io.PipeWriter + logReader, pw = io.Pipe() + go anonymizeLog(gzr, pw, g.anonymizer) + } + + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + if _, err := io.Copy(gw, logReader); err != nil { + return fmt.Errorf("re-gzip: %w", err) + } + + if err := gw.Close(); err != nil { + return fmt.Errorf("close gzip writer: %w", err) + } + + if err := g.addFileToZip(&buf, targetName); err != nil { + return fmt.Errorf("add anonymized gz: %w", err) + } + + return nil +} + func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error { header := &zip.FileHeader{ Name: filename, From 802a18167c33869bf5fc61f8ebae5414dd5c4580 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 21 May 2025 20:18:21 +0200 Subject: [PATCH 024/213] [client] Do not reconnect to mgm server in case of handler error (#3856) * Do not reconnect to mgm server in case of handler error Set to nil the flow grpc client to nil * Better error handling --- client/internal/netflow/manager.go | 10 ++++++++-- management/client/grpc.go | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/internal/netflow/manager.go b/client/internal/netflow/manager.go index 0f1cdce37..bf80e5a9f 100644 --- a/client/internal/netflow/manager.go +++ b/client/internal/netflow/manager.go @@ -123,8 +123,14 @@ func (m *Manager) disableFlow() error { m.logger.Close() - if m.receiverClient != nil { - return m.receiverClient.Close() + if m.receiverClient == nil { + return nil + } + + err := m.receiverClient.Close() + m.receiverClient = nil + if err != nil { + return fmt.Errorf("close: %w", err) } return nil diff --git a/management/client/grpc.go b/management/client/grpc.go index 2f4729e23..e85e16e76 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -260,8 +260,6 @@ func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, se if err := msgHandler(decryptedResp); err != nil { log.Errorf("failed handling an update message received from Management Service: %v", err.Error()) - // hide any grpc error code that is not relevant for management - return fmt.Errorf("msg handler error: %v", err.Error()) } } } From 9b53cad75210a82c580bb98f6319beb54786781e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 21 May 2025 21:40:36 +0100 Subject: [PATCH 025/213] [misc] add CLA note (#3859) --- .github/pull_request_template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c4bd3140b..c68aeb29c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,3 +13,5 @@ - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) - [ ] Extended the README / documentation, if necessary + +> By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement: https://gist.github.com/netbirddev/37ff3dc89cf853c6df89d1e5ae07030a From 8e948739f107db7957b336304dda69063855eb47 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Thu, 22 May 2025 10:38:58 +0200 Subject: [PATCH 026/213] Fix CLA link in the PR template (#3860) --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c68aeb29c..f41bc9e89 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,4 +14,4 @@ - [ ] Created tests that fail without the change (if possible) - [ ] Extended the README / documentation, if necessary -> By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement: https://gist.github.com/netbirddev/37ff3dc89cf853c6df89d1e5ae07030a +> By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). From c03435061cd2afe0ae4ef5e4cbce54a33dbe2645 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Thu, 22 May 2025 14:09:00 +0100 Subject: [PATCH 027/213] [management] lazy connection account setting (#3855) --- management/server/account.go | 19 +++++++++++++++---- management/server/activity/codes.go | 6 ++++++ management/server/grpcserver.go | 13 +++++++------ management/server/http/api/openapi.yml | 5 +++++ management/server/http/api/types.gen.go | 3 +++ .../handlers/accounts/accounts_handler.go | 4 ++++ .../accounts/accounts_handler_test.go | 4 ++++ management/server/peer.go | 4 ++-- management/server/peer_test.go | 4 ++-- management/server/types/settings.go | 4 ++++ 10 files changed, 52 insertions(+), 14 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index a7b3e628b..6dc449c1e 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -339,13 +339,20 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountRoutingPeerDNSResolutionDisabled, nil) } updateAccountPeers = true - account.Network.Serial++ + } + + if oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled { + if newSettings.LazyConnectionEnabled { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionEnabled, nil) + } else { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionDisabled, nil) + } + updateAccountPeers = true } if oldSettings.DNSDomain != newSettings.DNSDomain { am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil) updateAccountPeers = true - account.Network.Serial++ } err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID) @@ -358,7 +365,11 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco return nil, fmt.Errorf("groups propagation failed: %w", err) } - updatedAccount := account.UpdateSettings(newSettings) + account.UpdateSettings(newSettings) + + if updateAccountPeers { + account.Network.Serial++ + } err = am.Store.SaveAccount(ctx, account) if err != nil { @@ -374,7 +385,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco go am.UpdateAccountPeers(ctx, accountID) } - return updatedAccount, nil + return account, nil } func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error { diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index ed4be82e2..d9f56f097 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -171,6 +171,9 @@ const ( ResourceRemovedFromGroup Activity = 83 AccountDNSDomainUpdated Activity = 84 + + AccountLazyConnectionEnabled Activity = 85 + AccountLazyConnectionDisabled Activity = 86 ) var activityMap = map[Activity]Code{ @@ -268,6 +271,9 @@ var activityMap = map[Activity]Code{ ResourceRemovedFromGroup: {"Resource removed from group", "resource.group.delete"}, AccountDNSDomainUpdated: {"Account DNS domain updated", "account.dns.domain.update"}, + + AccountLazyConnectionEnabled: {"Account lazy connection enabled", "account.setting.lazy.connection.enable"}, + AccountLazyConnectionDisabled: {"Account lazy connection disabled", "account.setting.lazy.connection.disable"}, } // StringCode returns a string code of the activity diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 3f89ac4f9..5786dc871 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -517,7 +517,7 @@ func (s *GRPCServer) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil), - PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(settings), false), + PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(settings), settings), Checks: toProtocolChecks(ctx, postureChecks), } @@ -632,20 +632,21 @@ func toNetbirdConfig(config *types.Config, turnCredentials *Token, relayToken *T return nbConfig } -func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dnsResolutionOnRoutingPeerEnabled bool) *proto.PeerConfig { +func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings) *proto.PeerConfig { netmask, _ := network.Net.Mask.Size() fqdn := peer.FQDN(dnsName) return &proto.PeerConfig{ Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network SshConfig: &proto.SSHConfig{SshEnabled: peer.SSHEnabled}, Fqdn: fqdn, - RoutingPeerDnsResolutionEnabled: dnsResolutionOnRoutingPeerEnabled, + RoutingPeerDnsResolutionEnabled: settings.RoutingPeerDNSResolutionEnabled, + LazyConnectionEnabled: settings.LazyConnectionEnabled, } } -func toSyncResponse(ctx context.Context, config *types.Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnabled bool, extraSettings *types.ExtraSettings) *proto.SyncResponse { +func toSyncResponse(ctx context.Context, config *types.Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings) *proto.SyncResponse { response := &proto.SyncResponse{ - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnabled), + PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings), NetworkMap: &proto.NetworkMap{ Serial: networkMap.Network.CurrentSerial(), Routes: toProtocolRoutes(networkMap.Routes), @@ -731,7 +732,7 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p return status.Errorf(codes.Internal, "error handling request") } - plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(settings), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled, settings.Extra) + plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(settings), postureChecks, nil, settings, settings.Extra) encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp) if err != nil { diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 5ddfb5332..8edee32a7 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -118,6 +118,11 @@ components: example: my-organization.org extra: $ref: '#/components/schemas/AccountExtraSettings' + lazy_connection_enabled: + x-experimental: true + description: Enables or disables experimental lazy connection + type: boolean + example: true required: - peer_login_expiration_enabled - peer_login_expiration diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index c2efb835a..5dd6f23b8 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -289,6 +289,9 @@ type AccountSettings struct { // JwtGroupsEnabled Allows extract groups from JWT claim and add it to account groups. JwtGroupsEnabled *bool `json:"jwt_groups_enabled,omitempty"` + // LazyConnectionEnabled Enables or disables experimental lazy connection + LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"` + // PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds). PeerInactivityExpiration int `json:"peer_inactivity_expiration"` diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index 7cad26bd6..638524e31 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -122,6 +122,9 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { if req.Settings.DnsDomain != nil { settings.DNSDomain = *req.Settings.DnsDomain } + if req.Settings.LazyConnectionEnabled != nil { + settings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled + } updatedAccount, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings) if err != nil { @@ -181,6 +184,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A JwtAllowGroups: &jwtAllowGroups, RegularUsersViewBlocked: settings.RegularUsersViewBlocked, RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled, + LazyConnectionEnabled: &settings.LazyConnectionEnabled, DnsDomain: &settings.DNSDomain, } diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index 57bbffc7c..fec5140f4 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -108,6 +108,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtAllowGroups: &[]string{}, RegularUsersViewBlocked: true, RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), DnsDomain: sr(""), }, expectedArray: true, @@ -129,6 +130,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtAllowGroups: &[]string{}, RegularUsersViewBlocked: false, RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), DnsDomain: sr(""), }, expectedArray: false, @@ -150,6 +152,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtAllowGroups: &[]string{"test"}, RegularUsersViewBlocked: true, RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), DnsDomain: sr(""), }, expectedArray: false, @@ -171,6 +174,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtAllowGroups: &[]string{}, RegularUsersViewBlocked: true, RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), DnsDomain: sr(""), }, expectedArray: false, diff --git a/management/server/peer.go b/management/server/peer.go index 8bc6cdb05..f91db928d 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1221,7 +1221,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account return } - update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSetting) + update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting) am.peersUpdateManager.SendUpdate(ctx, p.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap}) }(peer) } @@ -1306,7 +1306,7 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI return } - update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSettings) + update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings) am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap}) } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 9c1de0659..3d782f04c 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1157,8 +1157,8 @@ func TestToSyncResponse(t *testing.T) { }, } dnsCache := &DNSConfigCache{} - - response := toSyncResponse(context.Background(), config, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, true, nil) + accountSettings := &types.Settings{RoutingPeerDNSResolutionEnabled: true} + response := toSyncResponse(context.Background(), config, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil) assert.NotNil(t, response) // assert peer config diff --git a/management/server/types/settings.go b/management/server/types/settings.go index c8de2a98c..bd361f3ff 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -44,6 +44,9 @@ type Settings struct { // Extra is a dictionary of Account settings Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` + + // LazyConnectionEnabled indicates wether the experimental feature is enabled or disabled + LazyConnectionEnabled bool `gorm:"default:false"` } // Copy copies the Settings struct @@ -61,6 +64,7 @@ func (s *Settings) Copy() *Settings { PeerInactivityExpiration: s.PeerInactivityExpiration, RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled, + LazyConnectionEnabled: s.LazyConnectionEnabled, DNSDomain: s.DNSDomain, } if s.Extra != nil { From 24f932b2ce2482707da7b3ae8b0896c0d47d795b Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 22 May 2025 16:28:14 +0300 Subject: [PATCH 028/213] [management] Update traffic events pagination filters (#3857) --- management/server/http/api/openapi.yml | 8 +++++++- management/server/http/api/types.gen.go | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 8edee32a7..64feab975 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -4300,6 +4300,12 @@ paths: required: false schema: type: string + - name: reporter_id + in: query + description: Filter by reporter ID + required: false + schema: + type: string - name: protocol in: query description: Filter by protocol @@ -4329,7 +4335,7 @@ paths: enum: [INGRESS, EGRESS, DIRECTION_UNKNOWN] - name: search in: query - description: Filters events with a partial match on user email, source and destination names and source and destination addresses + description: Case-insensitive partial match on user email, source/destination names, and source/destination addresses required: false schema: type: string diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 5dd6f23b8..647b17e32 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -1787,6 +1787,9 @@ type GetApiEventsNetworkTrafficParams struct { // UserId Filter by user ID UserId *string `form:"user_id,omitempty" json:"user_id,omitempty"` + // ReporterId Filter by reporter ID + ReporterId *string `form:"reporter_id,omitempty" json:"reporter_id,omitempty"` + // Protocol Filter by protocol Protocol *int `form:"protocol,omitempty" json:"protocol,omitempty"` @@ -1799,7 +1802,7 @@ type GetApiEventsNetworkTrafficParams struct { // Direction Filter by direction Direction *GetApiEventsNetworkTrafficParamsDirection `form:"direction,omitempty" json:"direction,omitempty"` - // Search Filters events with a partial match on user email, source and destination names and source and destination addresses + // Search Case-insensitive partial match on user email, source/destination names, and source/destination addresses Search *string `form:"search,omitempty" json:"search,omitempty"` // StartDate Start date for filtering events (ISO 8601 format, e.g., 2024-01-01T00:00:00Z). From 2a89d6e47a4c144dd1e1162f3e5d3cb73525ae77 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 23 May 2025 00:16:19 +0300 Subject: [PATCH 029/213] [management] Extend nameserver match domain validation (#3864) * Enhance match domain validation logic and add test cases Signed-off-by: bcmmbaga * remove the leading dot and root dot support ns regex Signed-off-by: bcmmbaga * Remove support for wildcard ns match domain Signed-off-by: bcmmbaga --------- Signed-off-by: bcmmbaga --- management/server/nameserver.go | 12 +++++------- management/server/nameserver_test.go | 28 ++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/management/server/nameserver.go b/management/server/nameserver.go index 797d7c11c..18339335d 100644 --- a/management/server/nameserver.go +++ b/management/server/nameserver.go @@ -18,7 +18,9 @@ import ( "github.com/netbirdio/netbird/management/server/types" ) -const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$` +const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*[*.a-z]{1,}$` + +var invalidDomainName = errors.New("invalid domain name") // GetNameServerGroup gets a nameserver group object from account and nameserver group IDs func (am *DefaultAccountManager) GetNameServerGroup(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) { @@ -319,13 +321,9 @@ func validateDomain(domain string) error { return errors.New("domain should consists of only letters, numbers, and hyphens with no leading, trailing hyphens, or spaces") } - labels, valid := dns.IsDomainName(domain) + _, valid := dns.IsDomainName(domain) if !valid { - return errors.New("invalid domain name") - } - - if labels < 2 { - return errors.New("domain should consists of a minimum of two labels") + return invalidDomainName } return nil diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 1ba790797..dc4be594f 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -899,13 +899,33 @@ func TestValidateDomain(t *testing.T) { errFunc: require.NoError, }, { - name: "Invalid domain name with double hyphen", - domain: "test--example.com", + name: "Valid domain name with only one label", + domain: "example", + errFunc: require.NoError, + }, + { + name: "Valid domain name with trailing dot", + domain: "example.", + errFunc: require.NoError, + }, + { + name: "Invalid wildcard domain name", + domain: "*.example", errFunc: require.Error, }, { - name: "Invalid domain name with only one label", - domain: "com", + name: "Invalid domain name with leading dot", + domain: ".com", + errFunc: require.Error, + }, + { + name: "Invalid domain name with dot only", + domain: ".", + errFunc: require.Error, + }, + { + name: "Invalid domain name with double hyphen", + domain: "test--example.com", errFunc: require.Error, }, { From a0482ebc7b38b2672160a03f65ca49ba6ca314a5 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 23 May 2025 14:04:12 +0200 Subject: [PATCH 030/213] [client] avoid overwriting state manager on iOS (#3870) --- client/internal/engine.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index 7c501e5aa..d6bcc66f6 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -241,6 +241,8 @@ func NewEngine( checks: checks, connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), } + + path := statemanager.GetDefaultStatePath() if runtime.GOOS == "ios" { if !fileExists(mobileDep.StateFilePath) { err := createFile(mobileDep.StateFilePath) @@ -250,11 +252,9 @@ func NewEngine( } } - engine.stateManager = statemanager.New(mobileDep.StateFilePath) - } - if path := statemanager.GetDefaultStatePath(); path != "" { - engine.stateManager = statemanager.New(path) + path = mobileDep.StateFilePath } + engine.stateManager = statemanager.New(path) return engine } From 5bed6777d568f1e0e96a4c3dbd290e175502eb81 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Fri, 23 May 2025 14:42:42 +0100 Subject: [PATCH 031/213] [management] force account id on save groups update (#3850) --- management/server/account.go | 4 ++-- management/server/group.go | 2 +- management/server/posture_checks_test.go | 2 +- management/server/store/sql_store.go | 12 ++++++++++-- management/server/store/sql_store_test.go | 6 +++--- management/server/store/store.go | 2 +- management/server/user.go | 2 +- 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 6dc449c1e..033ec5fa1 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1248,7 +1248,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth return nil } - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, newGroupsToCreate); err != nil { + if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, newGroupsToCreate); err != nil { return fmt.Errorf("error saving groups: %w", err) } @@ -1282,7 +1282,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth return fmt.Errorf("error modifying user peers in groups: %w", err) } - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, updatedGroups); err != nil { + if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, updatedGroups); err != nil { return fmt.Errorf("error saving groups: %w", err) } diff --git a/management/server/group.go b/management/server/group.go index 87d649228..c26a0cfc1 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -116,7 +116,7 @@ func (am *DefaultAccountManager) SaveGroups(ctx context.Context, accountID, user return err } - return transaction.SaveGroups(ctx, store.LockingStrengthUpdate, groupsToSave) + return transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave) }) if err != nil { return err diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go index 232955f7d..8bd2fab66 100644 --- a/management/server/posture_checks_test.go +++ b/management/server/posture_checks_test.go @@ -455,7 +455,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) { AccountID: account.Id, Peers: []string{}, } - err = manager.Store.SaveGroups(context.Background(), store.LockingStrengthUpdate, []*types.Group{groupA, groupB}) + err = manager.Store.SaveGroups(context.Background(), store.LockingStrengthUpdate, account.Id, []*types.Group{groupA, groupB}) require.NoError(t, err, "failed to save groups") postureCheckA := &posture.Checks{ diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index eb194ca9b..6c3104ef0 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -448,12 +448,20 @@ func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, u } // SaveGroups saves the given list of groups to the database. -func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, groups []*types.Group) error { +func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error { if len(groups) == 0 { return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&groups) + result := s.db. + Clauses( + clause.Locking{Strength: string(lockStrength)}, + clause.OnConflict{ + Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}}, + UpdateAll: true, + }, + ). + Create(&groups) if result.Error != nil { return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error) } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 8e99b34e1..2c1f5f8e6 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -1324,11 +1324,11 @@ func TestSqlStore_SaveGroups(t *testing.T) { Peers: []string{"peer3", "peer4"}, }, } - err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groups) require.NoError(t, err) groups[1].Peers = []string{} - err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groups) require.NoError(t, err) group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID) @@ -3240,7 +3240,7 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { }) } - err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groupsToSave) + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groupsToSave) require.NoError(t, err) accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) diff --git a/management/server/store/store.go b/management/server/store/store.go index 3d529ceb5..b3c2fceff 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -98,7 +98,7 @@ type Store interface { GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types.Group, error) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) - SaveGroups(ctx context.Context, lockStrength LockingStrength, groups []*types.Group) error + SaveGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error SaveGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error DeleteGroup(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) error DeleteGroups(ctx context.Context, strength LockingStrength, accountID string, groupIDs []string) error diff --git a/management/server/user.go b/management/server/user.go index 44ad3b68f..2c762a8eb 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -676,7 +676,7 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact return false, nil, nil, nil, fmt.Errorf("error modifying user peers in groups: %w", err) } - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, updatedGroups); err != nil { + if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, update.AccountID, updatedGroups); err != nil { return false, nil, nil, nil, fmt.Errorf("error saving groups: %w", err) } } From 670446d42e385397b8be87b13c5fd504c303ce86 Mon Sep 17 00:00:00 2001 From: "M. Essam" Date: Sun, 25 May 2025 17:57:34 +0300 Subject: [PATCH 032/213] [management/client/rest] Fix panic on unknown errors (#3865) --- management/client/rest/accounts_test.go | 9 +++++++++ management/client/rest/client.go | 11 ++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go index f6d48d874..d2ace4ec9 100644 --- a/management/client/rest/accounts_test.go +++ b/management/client/rest/accounts_test.go @@ -66,6 +66,15 @@ func TestAccounts_List_Err(t *testing.T) { }) } +func TestAccounts_List_ConnErr(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + ret, err := c.Accounts.List(context.Background()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "404") + assert.Empty(t, ret) + }) +} + func TestAccounts_Update_200(t *testing.T) { withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { diff --git a/management/client/rest/client.go b/management/client/rest/client.go index 886a59f2c..25e8ad0da 100644 --- a/management/client/rest/client.go +++ b/management/client/rest/client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" @@ -134,7 +135,8 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re if resp.StatusCode > 299 { parsedErr, pErr := parseResponse[util.ErrorResponse](resp) if pErr != nil { - return nil, err + + return nil, pErr } return nil, errors.New(parsedErr.Message) } @@ -145,13 +147,16 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re func parseResponse[T any](resp *http.Response) (T, error) { var ret T if resp.Body == nil { - return ret, errors.New("No body") + return ret, fmt.Errorf("Body missing, HTTP Error code %d", resp.StatusCode) } bs, err := io.ReadAll(resp.Body) if err != nil { return ret, err } err = json.Unmarshal(bs, &ret) + if err != nil { + return ret, fmt.Errorf("Error code %d, error unmarshalling body: %w", resp.StatusCode, err) + } - return ret, err + return ret, nil } From 5523040acd7cbe97510b1b3f7bd47c885feb974f Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 27 May 2025 13:47:53 +0300 Subject: [PATCH 033/213] [management] Add correlated network traffic event schema (#3680) --- management/server/http/api/openapi.yml | 132 ++++++++++++++---------- management/server/http/api/types.gen.go | 82 ++++++++------- management/server/store/store.go | 29 +++--- management/server/testutil/store.go | 56 +++++++--- management/server/testutil/store_ios.go | 8 +- 5 files changed, 183 insertions(+), 124 deletions(-) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 64feab975..f6ea39a8e 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -1925,13 +1925,71 @@ components: - os - address - dns_label - NetworkTrafficEvent: + NetworkTrafficUser: type: object properties: id: type: string - description: "ID of the event. Unique." - example: "18e204d6-f7c6-405d-8025-70becb216add" + description: "UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated)." + example: "google-oauth2|123456789012345678901" + email: + type: string + description: "Email of the user who initiated the event (if any)." + example: "alice@netbird.io" + name: + type: string + description: "Name of the user who initiated the event (if any)." + example: "Alice Smith" + required: + - id + - email + - name + NetworkTrafficPolicy: + type: object + properties: + id: + type: string + description: "ID of the policy that allowed this event." + example: "ch8i4ug6lnn4g9hqv7m0" + name: + type: string + description: "Name of the policy that allowed this event." + example: "All to All" + required: + - id + - name + NetworkTrafficICMP: + type: object + properties: + type: + type: integer + description: "ICMP type (if applicable)." + example: 8 + code: + type: integer + description: "ICMP code (if applicable)." + example: 0 + required: + - type + - code + NetworkTrafficSubEvent: + type: object + properties: + type: + type: string + description: Type of the event (e.g., TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP). + example: TYPE_START + timestamp: + type: string + format: date-time + description: Timestamp of the event as sent by the peer. + example: 2025-03-20T16:23:58.125397Z + required: + - type + - timestamp + NetworkTrafficEvent: + type: object + properties: flow_id: type: string description: "FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection)." @@ -1940,43 +1998,20 @@ components: type: string description: "ID of the reporter of the event (e.g., the peer that reported the event)." example: "ch8i4ug6lnn4g9hqv7m0" - timestamp: - type: string - format: date-time - description: "Timestamp of the event. Send by the peer." - example: "2025-03-20T16:23:58.125397Z" - receive_timestamp: - type: string - format: date-time - description: "Timestamp when the event was received by our API." - example: "2025-03-20T16:23:58.125397Z" source: $ref: '#/components/schemas/NetworkTrafficEndpoint' - user_id: - type: string - nullable: true - description: "UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated)." - example: "google-oauth2|123456789012345678901" - user_email: - type: string - nullable: true - description: "Email of the user who initiated the event (if any)." - example: "alice@netbird.io" - user_name: - type: string - nullable: true - description: "Name of the user who initiated the event (if any)." - example: "Alice Smith" destination: $ref: '#/components/schemas/NetworkTrafficEndpoint' + user: + $ref: '#/components/schemas/NetworkTrafficUser' + policy: + $ref: '#/components/schemas/NetworkTrafficPolicy' + icmp: + $ref: '#/components/schemas/NetworkTrafficICMP' protocol: type: integer description: "Protocol is the protocol of the traffic (e.g. 1 = ICMP, 6 = TCP, 17 = UDP, etc.)." example: 6 - type: - type: string - description: "Type of the event (e.g. TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP)." - example: "TYPE_START" direction: type: string description: "Direction of the traffic (e.g. DIRECTION_UNKNOWN, INGRESS, EGRESS)." @@ -1997,43 +2032,28 @@ components: type: integer description: "Number of packets transmitted." example: 5 - policy_id: - type: string - description: "ID of the policy that allowed this event." - example: "ch8i4ug6lnn4g9hqv7m0" - policy_name: - type: string - description: "Name of the policy that allowed this event." - example: "All to All" - icmp_type: - type: integer - description: "ICMP type (if applicable)." - example: 8 - icmp_code: - type: integer - description: "ICMP code (if applicable)." - example: 0 + events: + type: array + description: "List of events that are correlated to this flow (e.g., start, end)." + items: + $ref: '#/components/schemas/NetworkTrafficSubEvent' required: - id - flow_id - reporter_id - - timestamp - receive_timestamp - source - - user_id - - user_email - destination + - user + - policy + - icmp - protocol - - type - direction - rx_bytes - rx_packets - tx_bytes - tx_packets - - policy_id - - policy_name - - icmp_type - - icmp_code + - events NetworkTrafficEventsResponse: type: object properties: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 647b17e32..0a09d7ca2 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -883,30 +883,17 @@ type NetworkTrafficEvent struct { // Direction Direction of the traffic (e.g. DIRECTION_UNKNOWN, INGRESS, EGRESS). Direction string `json:"direction"` + // Events List of events that are correlated to this flow (e.g., start, end). + Events []NetworkTrafficSubEvent `json:"events"` + // FlowId FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection). - FlowId string `json:"flow_id"` - - // IcmpCode ICMP code (if applicable). - IcmpCode int `json:"icmp_code"` - - // IcmpType ICMP type (if applicable). - IcmpType int `json:"icmp_type"` - - // Id ID of the event. Unique. - Id string `json:"id"` - - // PolicyId ID of the policy that allowed this event. - PolicyId string `json:"policy_id"` - - // PolicyName Name of the policy that allowed this event. - PolicyName string `json:"policy_name"` + FlowId string `json:"flow_id"` + Icmp NetworkTrafficICMP `json:"icmp"` + Policy NetworkTrafficPolicy `json:"policy"` // Protocol Protocol is the protocol of the traffic (e.g. 1 = ICMP, 6 = TCP, 17 = UDP, etc.). Protocol int `json:"protocol"` - // ReceiveTimestamp Timestamp when the event was received by our API. - ReceiveTimestamp time.Time `json:"receive_timestamp"` - // ReporterId ID of the reporter of the event (e.g., the peer that reported the event). ReporterId string `json:"reporter_id"` @@ -917,26 +904,12 @@ type NetworkTrafficEvent struct { RxPackets int `json:"rx_packets"` Source NetworkTrafficEndpoint `json:"source"` - // Timestamp Timestamp of the event. Send by the peer. - Timestamp time.Time `json:"timestamp"` - // TxBytes Number of bytes transmitted. TxBytes int `json:"tx_bytes"` // TxPackets Number of packets transmitted. - TxPackets int `json:"tx_packets"` - - // Type Type of the event (e.g. TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP). - Type string `json:"type"` - - // UserEmail Email of the user who initiated the event (if any). - UserEmail *string `json:"user_email"` - - // UserId UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated). - UserId *string `json:"user_id"` - - // UserName Name of the user who initiated the event (if any). - UserName *string `json:"user_name"` + TxPackets int `json:"tx_packets"` + User NetworkTrafficUser `json:"user"` } // NetworkTrafficEventsResponse defines model for NetworkTrafficEventsResponse. @@ -957,6 +930,15 @@ type NetworkTrafficEventsResponse struct { TotalRecords int `json:"total_records"` } +// NetworkTrafficICMP defines model for NetworkTrafficICMP. +type NetworkTrafficICMP struct { + // Code ICMP code (if applicable). + Code int `json:"code"` + + // Type ICMP type (if applicable). + Type int `json:"type"` +} + // NetworkTrafficLocation defines model for NetworkTrafficLocation. type NetworkTrafficLocation struct { // CityName Name of the city (if known). @@ -966,6 +948,36 @@ type NetworkTrafficLocation struct { CountryCode string `json:"country_code"` } +// NetworkTrafficPolicy defines model for NetworkTrafficPolicy. +type NetworkTrafficPolicy struct { + // Id ID of the policy that allowed this event. + Id string `json:"id"` + + // Name Name of the policy that allowed this event. + Name string `json:"name"` +} + +// NetworkTrafficSubEvent defines model for NetworkTrafficSubEvent. +type NetworkTrafficSubEvent struct { + // Timestamp Timestamp of the event as sent by the peer. + Timestamp time.Time `json:"timestamp"` + + // Type Type of the event (e.g., TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP). + Type string `json:"type"` +} + +// NetworkTrafficUser defines model for NetworkTrafficUser. +type NetworkTrafficUser struct { + // Email Email of the user who initiated the event (if any). + Email string `json:"email"` + + // Id UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated). + Id string `json:"id"` + + // Name Name of the user who initiated the event (if any). + Name string `json:"name"` +} + // OSVersionCheck Posture check for the version of operating system type OSVersionCheck struct { // Android Posture check for the version of operating system diff --git a/management/server/store/store.go b/management/server/store/store.go index b3c2fceff..fff809247 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -365,11 +365,14 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( return nil, nil, fmt.Errorf("failed to add all group to account: %v", err) } + var sqlStore Store + var cleanup func() + maxRetries := 2 for i := 0; i < maxRetries; i++ { - sqlStore, cleanUp, err := getSqlStoreEngine(ctx, store, kind) + sqlStore, cleanup, err = getSqlStoreEngine(ctx, store, kind) if err == nil { - return sqlStore, cleanUp, nil + return sqlStore, cleanup, nil } if i < maxRetries-1 { time.Sleep(100 * time.Millisecond) @@ -427,16 +430,16 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind types.Engine) } func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) { - if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { + dsn, ok := os.LookupEnv(postgresDsnEnv) + if !ok || dsn == "" { var err error - _, err = testutil.CreatePostgresTestContainer() + _, dsn, err = testutil.CreatePostgresTestContainer() if err != nil { return nil, nil, err } } - dsn, ok := os.LookupEnv(postgresDsnEnv) - if !ok { + if dsn == "" { return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) } @@ -447,28 +450,28 @@ func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Eng dsn, cleanup, err := createRandomDB(dsn, db, kind) if err != nil { - return nil, cleanup, err + return nil, nil, err } store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) if err != nil { - return nil, cleanup, err + return nil, nil, err } return store, cleanup, nil } func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) { - if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { + dsn, ok := os.LookupEnv(mysqlDsnEnv) + if !ok || dsn == "" { var err error - _, err = testutil.CreateMysqlTestContainer() + _, dsn, err = testutil.CreateMysqlTestContainer() if err != nil { return nil, nil, err } } - dsn, ok := os.LookupEnv(mysqlDsnEnv) - if !ok { + if dsn == "" { return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } @@ -479,7 +482,7 @@ func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine dsn, cleanup, err := createRandomDB(dsn, db, kind) if err != nil { - return nil, cleanup, err + return nil, nil, err } store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go index ca022bfef..7f6a824a4 100644 --- a/management/server/testutil/store.go +++ b/management/server/testutil/store.go @@ -5,7 +5,6 @@ package testutil import ( "context" - "os" "time" log "github.com/sirupsen/logrus" @@ -16,11 +15,25 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +var ( + pgContainer *postgres.PostgresContainer + mysqlContainer *mysql.MySQLContainer +) + // CreateMysqlTestContainer creates a new MySQL container for testing. -func CreateMysqlTestContainer() (func(), error) { +func CreateMysqlTestContainer() (func(), string, error) { ctx := context.Background() - myContainer, err := mysql.RunContainer(ctx, + if mysqlContainer != nil { + connStr, err := mysqlContainer.ConnectionString(ctx) + if err != nil { + return nil, "", err + } + return noOpCleanup, connStr, nil + } + + var err error + mysqlContainer, err = mysql.RunContainer(ctx, testcontainers.WithImage("mlsmaycon/warmed-mysql:8"), mysql.WithDatabase("testing"), mysql.WithUsername("root"), @@ -31,31 +44,39 @@ func CreateMysqlTestContainer() (func(), error) { ), ) if err != nil { - return nil, err + return nil, "", err } cleanup := func() { - os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN") timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) defer cancelFunc() - if err = myContainer.Terminate(timeoutCtx); err != nil { - log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", myContainer.GetContainerID(), err) + if err = mysqlContainer.Terminate(timeoutCtx); err != nil { + log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", mysqlContainer.GetContainerID(), err) } } - talksConn, err := myContainer.ConnectionString(ctx) + talksConn, err := mysqlContainer.ConnectionString(ctx) if err != nil { - return nil, err + return nil, "", err } - return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_MYSQL_DSN", talksConn) + return cleanup, talksConn, nil } // CreatePostgresTestContainer creates a new PostgreSQL container for testing. -func CreatePostgresTestContainer() (func(), error) { +func CreatePostgresTestContainer() (func(), string, error) { ctx := context.Background() - pgContainer, err := postgres.RunContainer(ctx, + if pgContainer != nil { + connStr, err := pgContainer.ConnectionString(ctx) + if err != nil { + return nil, "", err + } + return noOpCleanup, connStr, nil + } + + var err error + pgContainer, err = postgres.RunContainer(ctx, testcontainers.WithImage("postgres:16-alpine"), postgres.WithDatabase("netbird"), postgres.WithUsername("root"), @@ -66,11 +87,10 @@ func CreatePostgresTestContainer() (func(), error) { ), ) if err != nil { - return nil, err + return nil, "", err } cleanup := func() { - os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN") timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) defer cancelFunc() if err = pgContainer.Terminate(timeoutCtx); err != nil { @@ -80,10 +100,14 @@ func CreatePostgresTestContainer() (func(), error) { talksConn, err := pgContainer.ConnectionString(ctx) if err != nil { - return nil, err + return nil, "", err } - return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN", talksConn) + return cleanup, talksConn, nil +} + +func noOpCleanup() { + // no-op } // CreateRedisTestContainer creates a new Redis container for testing. diff --git a/management/server/testutil/store_ios.go b/management/server/testutil/store_ios.go index a614258d2..c3dd839d3 100644 --- a/management/server/testutil/store_ios.go +++ b/management/server/testutil/store_ios.go @@ -3,16 +3,16 @@ package testutil -func CreatePostgresTestContainer() (func(), error) { +func CreatePostgresTestContainer() (func(), string, error) { return func() { // Empty function for Postgres - }, nil + }, "", nil } -func CreateMysqlTestContainer() (func(), error) { +func CreateMysqlTestContainer() (func(), string, error) { return func() { // Empty function for MySQL - }, nil + }, "", nil } func CreateRedisTestContainer() (func(), string, error) { From cdd27a9fe56932bb1b780e39fbaa112e475dd155 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 27 May 2025 13:32:54 +0200 Subject: [PATCH 034/213] [client, android] Fix/android enable server route (#3806) Enable the server route; otherwise, the manager throws an error and the engine will restart. --- .../{server_nonandroid.go => server.go} | 2 -- .../internal/routemanager/server_android.go | 27 ------------------- 2 files changed, 29 deletions(-) rename client/internal/routemanager/{server_nonandroid.go => server.go} (99%) delete mode 100644 client/internal/routemanager/server_android.go diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server.go similarity index 99% rename from client/internal/routemanager/server_nonandroid.go rename to client/internal/routemanager/server.go index 131d4c170..5bacb856c 100644 --- a/client/internal/routemanager/server_nonandroid.go +++ b/client/internal/routemanager/server.go @@ -1,5 +1,3 @@ -//go:build !android - package routemanager import ( diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go deleted file mode 100644 index 953210e9e..000000000 --- a/client/internal/routemanager/server_android.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build android - -package routemanager - -import ( - "context" - "fmt" - - firewall "github.com/netbirdio/netbird/client/firewall/manager" - "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/routemanager/iface" - "github.com/netbirdio/netbird/route" -) - -type serverRouter struct { -} - -func (r serverRouter) cleanUp() { -} - -func (r serverRouter) updateRoutes(map[route.ID]*route.Route, bool) error { - return nil -} - -func newServerRouter(context.Context, iface.WGIface, firewall.Manager, *peer.Status) (*serverRouter, error) { - return nil, fmt.Errorf("server route not supported on this os") -} From a0d28f9851bc607ea8458024c629303b343174c7 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 27 May 2025 14:42:00 +0300 Subject: [PATCH 035/213] [management] Reset test containers after cleanup (#3885) --- management/server/testutil/store.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go index 7f6a824a4..db418c45b 100644 --- a/management/server/testutil/store.go +++ b/management/server/testutil/store.go @@ -48,10 +48,13 @@ func CreateMysqlTestContainer() (func(), string, error) { } cleanup := func() { - timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) - defer cancelFunc() - if err = mysqlContainer.Terminate(timeoutCtx); err != nil { - log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", mysqlContainer.GetContainerID(), err) + if mysqlContainer != nil { + timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) + defer cancelFunc() + if err = mysqlContainer.Terminate(timeoutCtx); err != nil { + log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", mysqlContainer.GetContainerID(), err) + } + mysqlContainer = nil // reset the container to allow recreation } } @@ -91,11 +94,15 @@ func CreatePostgresTestContainer() (func(), string, error) { } cleanup := func() { - timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) - defer cancelFunc() - if err = pgContainer.Terminate(timeoutCtx); err != nil { - log.WithContext(ctx).Warnf("failed to stop postgres container %s: %s", pgContainer.GetContainerID(), err) + if pgContainer != nil { + timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) + defer cancelFunc() + if err = pgContainer.Terminate(timeoutCtx); err != nil { + log.WithContext(ctx).Warnf("failed to stop postgres container %s: %s", pgContainer.GetContainerID(), err) + } + pgContainer = nil // reset the container to allow recreation } + } talksConn, err := pgContainer.ConnectionString(ctx) From 6f436e57b50f0ef7efa71267173c1f27cf0988e7 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 27 May 2025 16:42:06 +0200 Subject: [PATCH 036/213] [server-test] Install libs for i386 tests (#3887) Install libs for i386 tests --- .github/workflows/golang-test-linux.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index d585ba209..cbce3e6e4 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -223,6 +223,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386 + - name: Get Go environment run: | echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV @@ -269,6 +273,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386 + - name: Get Go environment run: | echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV From 0492c1724ad28d152f34a2ead28970558ae57053 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 27 May 2025 17:12:04 +0200 Subject: [PATCH 037/213] [client, android] Fix/notifier threading (#3807) - Fix potential deadlocks - When adding a listener, immediately notify with the last known IP and fqdn. --- client/internal/peer/notifier.go | 102 ++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/client/internal/peer/notifier.go b/client/internal/peer/notifier.go index f1175c2c4..8d1954fe5 100644 --- a/client/internal/peer/notifier.go +++ b/client/internal/peer/notifier.go @@ -18,6 +18,8 @@ type notifier struct { currentClientState bool lastNotification int lastNumberOfPeers int + lastFqdnAddress string + lastIPAddress string } func newNotifier() *notifier { @@ -25,15 +27,22 @@ func newNotifier() *notifier { } func (n *notifier) setListener(listener Listener) { + n.serverStateLock.Lock() + lastNotification := n.lastNotification + numOfPeers := n.lastNumberOfPeers + fqdnAddress := n.lastFqdnAddress + address := n.lastIPAddress + n.serverStateLock.Unlock() + n.listenersLock.Lock() defer n.listenersLock.Unlock() - n.serverStateLock.Lock() - n.notifyListener(listener, n.lastNotification) - listener.OnPeersListChanged(n.lastNumberOfPeers) - n.serverStateLock.Unlock() - n.listener = listener + + listener.OnAddressChanged(fqdnAddress, address) + notifyListener(listener, lastNotification) + // run on go routine to avoid on Java layer to call go functions on same thread + go listener.OnPeersListChanged(numOfPeers) } func (n *notifier) removeListener() { @@ -44,41 +53,44 @@ func (n *notifier) removeListener() { func (n *notifier) updateServerStates(mgmState bool, signalState bool) { n.serverStateLock.Lock() - defer n.serverStateLock.Unlock() - calculatedState := n.calculateState(mgmState, signalState) if !n.isServerStateChanged(calculatedState) { + n.serverStateLock.Unlock() return } n.lastNotification = calculatedState + n.serverStateLock.Unlock() - n.notify(n.lastNotification) + n.notify(calculatedState) } func (n *notifier) clientStart() { n.serverStateLock.Lock() - defer n.serverStateLock.Unlock() n.currentClientState = true n.lastNotification = stateConnecting - n.notify(n.lastNotification) + n.serverStateLock.Unlock() + + n.notify(stateConnecting) } func (n *notifier) clientStop() { n.serverStateLock.Lock() - defer n.serverStateLock.Unlock() n.currentClientState = false n.lastNotification = stateDisconnected - n.notify(n.lastNotification) + n.serverStateLock.Unlock() + + n.notify(stateDisconnected) } func (n *notifier) clientTearDown() { n.serverStateLock.Lock() - defer n.serverStateLock.Unlock() n.currentClientState = false n.lastNotification = stateDisconnecting - n.notify(n.lastNotification) + n.serverStateLock.Unlock() + + n.notify(stateDisconnecting) } func (n *notifier) isServerStateChanged(newState int) bool { @@ -87,26 +99,14 @@ func (n *notifier) isServerStateChanged(newState int) bool { func (n *notifier) notify(state int) { n.listenersLock.Lock() - defer n.listenersLock.Unlock() - if n.listener == nil { + listener := n.listener + n.listenersLock.Unlock() + + if listener == nil { return } - n.notifyListener(n.listener, state) -} -func (n *notifier) notifyListener(l Listener, state int) { - go func() { - switch state { - case stateDisconnected: - l.OnDisconnected() - case stateConnected: - l.OnConnected() - case stateConnecting: - l.OnConnecting() - case stateDisconnecting: - l.OnDisconnecting() - } - }() + notifyListener(listener, state) } func (n *notifier) calculateState(managementConn, signalConn bool) int { @@ -126,20 +126,48 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int { } func (n *notifier) peerListChanged(numOfPeers int) { + n.serverStateLock.Lock() n.lastNumberOfPeers = numOfPeers + n.serverStateLock.Unlock() + n.listenersLock.Lock() - defer n.listenersLock.Unlock() - if n.listener == nil { + listener := n.listener + n.listenersLock.Unlock() + + if listener == nil { return } - n.listener.OnPeersListChanged(numOfPeers) + + // run on go routine to avoid on Java layer to call go functions on same thread + go listener.OnPeersListChanged(numOfPeers) } func (n *notifier) localAddressChanged(fqdn, address string) { + n.serverStateLock.Lock() + n.lastFqdnAddress = fqdn + n.lastIPAddress = address + n.serverStateLock.Unlock() + n.listenersLock.Lock() - defer n.listenersLock.Unlock() - if n.listener == nil { + listener := n.listener + n.listenersLock.Unlock() + + if listener == nil { return } - n.listener.OnAddressChanged(fqdn, address) + + listener.OnAddressChanged(fqdn, address) +} + +func notifyListener(l Listener, state int) { + switch state { + case stateDisconnected: + l.OnDisconnected() + case stateConnected: + l.OnConnected() + case stateConnecting: + l.OnConnecting() + case stateDisconnecting: + l.OnDisconnecting() + } } From 684501fd35c8b2222e899129243ff50a2fb736bf Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 29 May 2025 18:50:00 +0300 Subject: [PATCH 038/213] [management] Prevent deletion of peers linked to network routers (#3881) - Prevent deletion of peers linked to network routers - Add API endpoint to list all network routers --- management/server/http/api/openapi.yml | 25 +++++++++++++++++ .../http/handlers/networks/routers_handler.go | 28 ++++++++++++++++++- management/server/peer.go | 27 ++++++++++++++++-- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index f6ea39a8e..58134d375 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -4068,6 +4068,31 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/networks/routers: + get: + summary: List all Network Routers + description: Returns a list of all routers in a network + tags: [ Networks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A JSON Array of Routers + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NetworkRouter' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" /api/dns/nameservers: get: summary: List all Nameserver Groups diff --git a/management/server/http/handlers/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go index f1a3fba0b..6b00534fc 100644 --- a/management/server/http/handlers/networks/routers_handler.go +++ b/management/server/http/handlers/networks/routers_handler.go @@ -19,7 +19,8 @@ type routersHandler struct { func addRouterEndpoints(routersManager routers.Manager, router *mux.Router) { routersHandler := newRoutersHandler(routersManager) - router.HandleFunc("/networks/{networkId}/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS") + router.HandleFunc("/networks/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS") + router.HandleFunc("/networks/{networkId}/routers", routersHandler.getNetworkRouters).Methods("GET", "OPTIONS") router.HandleFunc("/networks/{networkId}/routers", routersHandler.createRouter).Methods("POST", "OPTIONS") router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.getRouter).Methods("GET", "OPTIONS") router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.updateRouter).Methods("PUT", "OPTIONS") @@ -41,6 +42,31 @@ func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) { accountID, userID := userAuth.AccountId, userAuth.UserId + routersMap, err := h.routersManager.GetAllRoutersInAccount(r.Context(), accountID, userID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + routersResponse := make([]*api.NetworkRouter, 0) + for _, routers := range routersMap { + for _, router := range routers { + routersResponse = append(routersResponse, router.ToAPIResponse()) + } + } + + util.WriteJSONObject(r.Context(), w, routersResponse) +} + +func (h *routersHandler) getNetworkRouters(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + accountID, userID := userAuth.AccountId, userAuth.UserId + networkID := mux.Vars(r)["networkId"] routers, err := h.routersManager.GetAllRoutersInNetwork(r.Context(), accountID, userID, networkID) if err != nil { diff --git a/management/server/peer.go b/management/server/peer.go index f91db928d..4a468a6cd 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -17,6 +17,7 @@ import ( "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/geolocation" + routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" @@ -352,7 +353,7 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return err } - if err = am.validatePeerDelete(ctx, accountID, peerID); err != nil { + if err = am.validatePeerDelete(ctx, transaction, accountID, peerID); err != nil { return err } @@ -1543,7 +1544,7 @@ func ConvertSliceToMap(existingLabels []string) map[string]struct{} { } // validatePeerDelete checks if the peer can be deleted. -func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, accountId, peerId string) error { +func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, transaction store.Store, accountId, peerId string) error { linkedInIngressPorts, err := am.proxyController.IsPeerInIngressPorts(ctx, accountId, peerId) if err != nil { return err @@ -1553,5 +1554,27 @@ func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, account return status.Errorf(status.PreconditionFailed, "peer is linked to ingress ports: %s", peerId) } + linked, router := isPeerLinkedToNetworkRouter(ctx, transaction, accountId, peerId) + if linked { + return status.Errorf(status.PreconditionFailed, "peer is linked to a network router: %s", router.ID) + } + return nil } + +// isPeerLinkedToNetworkRouter checks if a peer is linked to any network router in the account. +func isPeerLinkedToNetworkRouter(ctx context.Context, transaction store.Store, accountID string, peerID string) (bool, *routerTypes.NetworkRouter) { + routers, err := transaction.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthShare, accountID) + if err != nil { + log.WithContext(ctx).Errorf("error retrieving network routers while checking peer linkage: %v", err) + return false, nil + } + + for _, router := range routers { + if router.Peer == peerID { + return true, router + } + } + + return false, nil +} From cfb2d8235205f2d5db9e81d52677702a11176218 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Fri, 30 May 2025 16:54:49 +0300 Subject: [PATCH 039/213] [client] Refactor exclude list handling to use a map for permanent connections (#3901) [client] Refactor exclude list handling to use a map for permanent connections (#3901) --- client/internal/conn_mgr.go | 4 ++-- client/internal/engine.go | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index 119ddc1bd..f7b1f6a05 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -98,14 +98,14 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er } // SetExcludeList sets the list of peer IDs that should always have permanent connections. -func (e *ConnMgr) SetExcludeList(peerIDs []string) { +func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) { if e.lazyConnMgr == nil { return } excludedPeers := make([]lazyconn.PeerConfig, 0, len(peerIDs)) - for _, peerID := range peerIDs { + for peerID := range peerIDs { var peerConn *peer.Conn var exists bool if peerConn, exists = e.peerStore.PeerConn(peerID); !exists { diff --git a/client/internal/engine.go b/client/internal/engine.go index d6bcc66f6..e04ccc9b8 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1927,14 +1927,16 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewal return forwardingRules, nberrors.FormatErrorOrNil(merr) } -func (e *Engine) toExcludedLazyPeers(routes []*route.Route, rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) []string { - excludedPeers := make([]string, 0) +func (e *Engine) toExcludedLazyPeers(routes []*route.Route, rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool { + excludedPeers := make(map[string]bool) for _, r := range routes { if r.Peer == "" { continue } - log.Infof("exclude router peer from lazy connection: %s", r.Peer) - excludedPeers = append(excludedPeers, r.Peer) + if !excludedPeers[r.Peer] { + log.Infof("exclude router peer from lazy connection: %s", r.Peer) + excludedPeers[r.Peer] = true + } } for _, r := range rules { @@ -1945,7 +1947,7 @@ func (e *Engine) toExcludedLazyPeers(routes []*route.Route, rules []firewallMana continue } log.Infof("exclude forwarder peer from lazy connection: %s", p.GetWgPubKey()) - excludedPeers = append(excludedPeers, p.GetWgPubKey()) + excludedPeers[p.GetWgPubKey()] = true } } } From 2bef214cc07ff7f641a102f326da0bdf6d9d4cb8 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 30 May 2025 18:12:30 +0300 Subject: [PATCH 040/213] [management] Fix user groups propagation (#3902) --- management/server/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/user.go b/management/server/user.go index 2c762a8eb..5c162c50b 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -676,7 +676,7 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact return false, nil, nil, nil, fmt.Errorf("error modifying user peers in groups: %w", err) } - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, update.AccountID, updatedGroups); err != nil { + if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, updatedGroups); err != nil { return false, nil, nil, nil, fmt.Errorf("error saving groups: %w", err) } } From aa07b3b87b262cbf3eb262d939d8c9e2d70ee6de Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 30 May 2025 23:38:02 +0200 Subject: [PATCH 041/213] Fix deadlock (#3904) --- client/internal/peer/conn.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 5037a0bd0..b33023873 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -691,8 +691,7 @@ func (conn *Conn) evalStatus() ConnStatus { } func (conn *Conn) isConnectedOnAllWay() (connected bool) { - conn.mu.Lock() - defer conn.mu.Unlock() + // would be better to protect this with a mutex, but it could cause deadlock with Close function defer func() { if !connected { From f16f0c78318add10cef54f9b70d58ee2f2d97376 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 1 Jun 2025 16:08:27 +0200 Subject: [PATCH 042/213] [client] Fix HA router switch (#3889) * Fix HA router switch. - Simplify the notification filter logic. Always send notification if a state has been changed - Remove IP changes check because we never modify * Notify only the proper listeners * Fix test * Fix TestGetPeerStateChangeNotifierLogic test * Before lazy connection, when the peer disconnected, the status switched to disconnected. After implementing lazy connection, the peer state is connecting, so we did not decrease the reference counters on the routes. * When switch to idle notify the route mgr --- client/internal/peer/status.go | 85 +++++++++++++------------- client/internal/peer/status_test.go | 27 ++++---- client/internal/routemanager/client.go | 2 +- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 69e333bf1..40956e68e 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -289,11 +289,7 @@ func (d *Status) UpdatePeerState(receivedState State) error { return errors.New("peer doesn't exist") } - if receivedState.IP != "" { - peerState.IP = receivedState.IP - } - - skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState) + oldState := peerState.ConnStatus if receivedState.ConnStatus != peerState.ConnStatus { peerState.ConnStatus = receivedState.ConnStatus @@ -309,11 +305,15 @@ func (d *Status) UpdatePeerState(receivedState State) error { d.peers[receivedState.PubKey] = peerState - if skipNotification { - return nil + if hasConnStatusChanged(oldState, receivedState.ConnStatus) { + d.notifyPeerListChanged() } - d.notifyPeerListChanged() + // when we close the connection we will not notify the router manager + if receivedState.ConnStatus == StatusIdle { + d.notifyPeerStateChangeListeners(receivedState.PubKey) + + } return nil } @@ -380,11 +380,8 @@ func (d *Status) UpdatePeerICEState(receivedState State) error { return errors.New("peer doesn't exist") } - if receivedState.IP != "" { - peerState.IP = receivedState.IP - } - - skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState) + oldState := peerState.ConnStatus + oldIsRelayed := peerState.Relayed peerState.ConnStatus = receivedState.ConnStatus peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate @@ -397,12 +394,13 @@ func (d *Status) UpdatePeerICEState(receivedState State) error { d.peers[receivedState.PubKey] = peerState - if skipNotification { - return nil + if hasConnStatusChanged(oldState, receivedState.ConnStatus) { + d.notifyPeerListChanged() } - d.notifyPeerStateChangeListeners(receivedState.PubKey) - d.notifyPeerListChanged() + if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) { + d.notifyPeerStateChangeListeners(receivedState.PubKey) + } return nil } @@ -415,7 +413,8 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error { return errors.New("peer doesn't exist") } - skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState) + oldState := peerState.ConnStatus + oldIsRelayed := peerState.Relayed peerState.ConnStatus = receivedState.ConnStatus peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate @@ -425,12 +424,13 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error { d.peers[receivedState.PubKey] = peerState - if skipNotification { - return nil + if hasConnStatusChanged(oldState, receivedState.ConnStatus) { + d.notifyPeerListChanged() } - d.notifyPeerStateChangeListeners(receivedState.PubKey) - d.notifyPeerListChanged() + if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) { + d.notifyPeerStateChangeListeners(receivedState.PubKey) + } return nil } @@ -443,7 +443,8 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error return errors.New("peer doesn't exist") } - skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState) + oldState := peerState.ConnStatus + oldIsRelayed := peerState.Relayed peerState.ConnStatus = receivedState.ConnStatus peerState.Relayed = receivedState.Relayed @@ -452,12 +453,13 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error d.peers[receivedState.PubKey] = peerState - if skipNotification { - return nil + if hasConnStatusChanged(oldState, receivedState.ConnStatus) { + d.notifyPeerListChanged() } - d.notifyPeerStateChangeListeners(receivedState.PubKey) - d.notifyPeerListChanged() + if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) { + d.notifyPeerStateChangeListeners(receivedState.PubKey) + } return nil } @@ -470,7 +472,8 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error { return errors.New("peer doesn't exist") } - skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState) + oldState := peerState.ConnStatus + oldIsRelayed := peerState.Relayed peerState.ConnStatus = receivedState.ConnStatus peerState.Relayed = receivedState.Relayed @@ -482,12 +485,13 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error { d.peers[receivedState.PubKey] = peerState - if skipNotification { - return nil + if hasConnStatusChanged(oldState, receivedState.ConnStatus) { + d.notifyPeerListChanged() } - d.notifyPeerStateChangeListeners(receivedState.PubKey) - d.notifyPeerListChanged() + if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) { + d.notifyPeerStateChangeListeners(receivedState.PubKey) + } return nil } @@ -510,17 +514,12 @@ func (d *Status) UpdateWireGuardPeerState(pubKey string, wgStats configurer.WGSt return nil } -func shouldSkipNotify(receivedConnStatus ConnStatus, curr State) bool { - switch { - case receivedConnStatus == StatusConnecting: - return true - case receivedConnStatus == StatusIdle && curr.ConnStatus == StatusConnecting: - return true - case receivedConnStatus == StatusIdle && curr.ConnStatus == StatusIdle: - return curr.IP != "" - default: - return false - } +func hasStatusOrRelayedChange(oldConnStatus, newConnStatus ConnStatus, oldRelayed, newRelayed bool) bool { + return oldRelayed != newRelayed || hasConnStatusChanged(newConnStatus, oldConnStatus) +} + +func hasConnStatusChanged(oldStatus, newStatus ConnStatus) bool { + return newStatus != oldStatus } // UpdatePeerFQDN update peer's state fqdn only diff --git a/client/internal/peer/status_test.go b/client/internal/peer/status_test.go index bdf8f087a..8f28a9862 100644 --- a/client/internal/peer/status_test.go +++ b/client/internal/peer/status_test.go @@ -4,6 +4,7 @@ import ( "errors" "sync" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -42,16 +43,16 @@ func TestGetPeer(t *testing.T) { func TestUpdatePeerState(t *testing.T) { key := "abc" ip := "10.10.10.10" + fqdn := "peer-a.netbird.local" status := NewRecorder("https://mgm") + _ = status.AddPeer(key, fqdn, ip) + peerState := State{ - PubKey: key, - Mux: new(sync.RWMutex), + PubKey: key, + ConnStatusUpdate: time.Now(), + ConnStatus: StatusConnecting, } - status.peers[key] = peerState - - peerState.IP = ip - err := status.UpdatePeerState(peerState) assert.NoError(t, err, "shouldn't return error") @@ -83,17 +84,17 @@ func TestGetPeerStateChangeNotifierLogic(t *testing.T) { key := "abc" ip := "10.10.10.10" status := NewRecorder("https://mgm") - peerState := State{ - PubKey: key, - Mux: new(sync.RWMutex), - } - - status.peers[key] = peerState + _ = status.AddPeer(key, "abc.netbird", ip) ch := status.GetPeerStateChangeNotifier(key) assert.NotNil(t, ch, "channel shouldn't be nil") - peerState.IP = ip + peerState := State{ + PubKey: key, + ConnStatus: StatusConnecting, + Relayed: false, + ConnStatusUpdate: time.Now(), + } err := status.UpdatePeerRelayedStateToDisconnected(peerState) assert.NoError(t, err, "shouldn't return error") diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 847949a53..137e00d31 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -232,7 +232,7 @@ func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey stri return case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey): state, err := c.statusRecorder.GetPeer(peerKey) - if err != nil || state.ConnStatus == peer.StatusConnecting { + if err != nil { continue } peerStateUpdate <- struct{}{} From 41cd4952f17d4e117bddf078d6ab56704f73b809 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:11:54 +0200 Subject: [PATCH 043/213] [client] Apply return traffic rules only if firewall is stateless (#3895) --- client/firewall/iptables/manager_linux.go | 4 + client/firewall/manager/firewall.go | 2 + client/firewall/nftables/manager_linux.go | 4 + client/firewall/uspfilter/uspfilter.go | 9 +- client/internal/acl/manager.go | 6 +- client/internal/acl/manager_test.go | 187 +++++++++++-------- client/internal/routemanager/static/route.go | 1 - 7 files changed, 130 insertions(+), 83 deletions(-) diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index b229688fc..0897f831f 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -147,6 +147,10 @@ func (m *Manager) IsServerRouteSupported() bool { return true } +func (m *Manager) IsStateful() bool { + return true +} + func (m *Manager) AddNatRule(pair firewall.RouterPair) error { m.mutex.Lock() defer m.mutex.Unlock() diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index 084d19423..3b3164823 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -116,6 +116,8 @@ type Manager interface { // IsServerRouteSupported returns true if the firewall supports server side routing operations IsServerRouteSupported() bool + IsStateful() bool + AddRouteFiltering( id []byte, sources []netip.Prefix, diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index e6b3a031b..2f8ee81a4 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -170,6 +170,10 @@ func (m *Manager) IsServerRouteSupported() bool { return true } +func (m *Manager) IsStateful() bool { + return true +} + func (m *Manager) AddNatRule(pair firewall.RouterPair) error { m.mutex.Lock() defer m.mutex.Unlock() diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 11730dbb3..287e52773 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -326,6 +326,10 @@ func (m *Manager) IsServerRouteSupported() bool { return true } +func (m *Manager) IsStateful() bool { + return m.stateful +} + func (m *Manager) AddNatRule(pair firewall.RouterPair) error { if m.nativeRouter.Load() && m.nativeFirewall != nil { return m.nativeFirewall.AddNatRule(pair) @@ -606,9 +610,8 @@ func (m *Manager) processOutgoingHooks(packetData []byte, size int) bool { return true } - if m.stateful { - m.trackOutbound(d, srcIP, dstIP, size) - } + // for netflow we keep track even if the firewall is stateless + m.trackOutbound(d, srcIP, dstIP, size) return false } diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index a6316d7a2..5caf2b770 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -285,8 +285,10 @@ func (d *DefaultManager) protoRuleToFirewallRule( case mgmProto.RuleDirection_IN: rules, err = d.addInRules(r.PolicyID, ip, protocol, port, action, ipsetName) case mgmProto.RuleDirection_OUT: - // TODO: Remove this soon. Outbound rules are obsolete. - // We only maintain this for return traffic (inbound dir) which is now handled by the stateful firewall already + if d.firewall.IsStateful() { + return "", nil, nil + } + // return traffic for outbound connections if firewall is stateless rules, err = d.addOutRules(r.PolicyID, ip, protocol, port, action, ipsetName) default: return "", nil, fmt.Errorf("invalid direction, skipping firewall rule") diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 3595ca600..532d70a24 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -5,9 +5,10 @@ import ( "testing" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/client/firewall" - "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/acl/mocks" "github.com/netbirdio/netbird/client/internal/netflow" @@ -43,9 +44,7 @@ func TestDefaultManager(t *testing.T) { ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes() ifaceMock.EXPECT().SetFilter(gomock.Any()) ip, network, err := net.ParseCIDR("172.0.0.1/32") - if err != nil { - t.Fatalf("failed to parse IP address: %v", err) - } + require.NoError(t, err) ifaceMock.EXPECT().Name().Return("lo").AnyTimes() ifaceMock.EXPECT().Address().Return(wgaddr.Address{ @@ -54,23 +53,22 @@ func TestDefaultManager(t *testing.T) { }).AnyTimes() ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() - // we receive one rule from the management so for testing purposes ignore it fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false) - if err != nil { - t.Errorf("create firewall: %v", err) - return - } - defer func(fw manager.Manager) { - _ = fw.Close(nil) - }(fw) + require.NoError(t, err) + defer func() { + err = fw.Close(nil) + require.NoError(t, err) + }() + acl := NewDefaultManager(fw) t.Run("apply firewall rules", func(t *testing.T) { acl.ApplyFiltering(networkMap, false) - if len(acl.peerRulesPairs) != 2 { - t.Errorf("firewall rules not applied: %v", acl.peerRulesPairs) - return + if fw.IsStateful() { + assert.Equal(t, 0, len(acl.peerRulesPairs)) + } else { + assert.Equal(t, 2, len(acl.peerRulesPairs)) } }) @@ -94,12 +92,13 @@ func TestDefaultManager(t *testing.T) { acl.ApplyFiltering(networkMap, false) - // we should have one old and one new rule in the existed rules - if len(acl.peerRulesPairs) != 2 { - t.Errorf("firewall rules not applied") - return + expectedRules := 2 + if fw.IsStateful() { + expectedRules = 1 // only the inbound rule } + assert.Equal(t, expectedRules, len(acl.peerRulesPairs)) + // check that old rule was removed previousCount := 0 for id := range acl.peerRulesPairs { @@ -107,26 +106,87 @@ func TestDefaultManager(t *testing.T) { previousCount++ } } - if previousCount != 1 { - t.Errorf("old rule was not removed") + + expectedPreviousCount := 0 + if !fw.IsStateful() { + expectedPreviousCount = 1 } + assert.Equal(t, expectedPreviousCount, previousCount) }) t.Run("handle default rules", func(t *testing.T) { networkMap.FirewallRules = networkMap.FirewallRules[:0] networkMap.FirewallRulesIsEmpty = true - if acl.ApplyFiltering(networkMap, false); len(acl.peerRulesPairs) != 0 { - t.Errorf("rules should be empty if FirewallRulesIsEmpty is set, got: %v", len(acl.peerRulesPairs)) - return - } + acl.ApplyFiltering(networkMap, false) + assert.Equal(t, 0, len(acl.peerRulesPairs)) networkMap.FirewallRulesIsEmpty = false acl.ApplyFiltering(networkMap, false) - if len(acl.peerRulesPairs) != 1 { - t.Errorf("rules should contain 1 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs)) - return + + expectedRules := 1 + if fw.IsStateful() { + expectedRules = 1 // only inbound allow-all rule } + assert.Equal(t, expectedRules, len(acl.peerRulesPairs)) + }) +} + +func TestDefaultManagerStateless(t *testing.T) { + // stateless currently only in userspace, so we have to disable kernel + t.Setenv("NB_WG_KERNEL_DISABLED", "true") + t.Setenv("NB_DISABLE_CONNTRACK", "true") + + networkMap := &mgmProto.NetworkMap{ + FirewallRules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_OUT, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "80", + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_UDP, + Port: "53", + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ifaceMock := mocks.NewMockIFaceMapper(ctrl) + ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes() + ifaceMock.EXPECT().SetFilter(gomock.Any()) + ip, network, err := net.ParseCIDR("172.0.0.1/32") + require.NoError(t, err) + + ifaceMock.EXPECT().Name().Return("lo").AnyTimes() + ifaceMock.EXPECT().Address().Return(wgaddr.Address{ + IP: ip, + Network: network, + }).AnyTimes() + ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() + + fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false) + require.NoError(t, err) + defer func() { + err = fw.Close(nil) + require.NoError(t, err) + }() + + acl := NewDefaultManager(fw) + + t.Run("stateless firewall creates outbound rules", func(t *testing.T) { + acl.ApplyFiltering(networkMap, false) + + // In stateless mode, we should have both inbound and outbound rules + assert.False(t, fw.IsStateful()) + assert.Equal(t, 2, len(acl.peerRulesPairs)) }) } @@ -192,42 +252,19 @@ func TestDefaultManagerSquashRules(t *testing.T) { manager := &DefaultManager{} rules, _ := manager.squashAcceptRules(networkMap) - if len(rules) != 2 { - t.Errorf("rules should contain 2, got: %v", rules) - return - } + assert.Equal(t, 2, len(rules)) r := rules[0] - switch { - case r.PeerIP != "0.0.0.0": - t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP) - return - case r.Direction != mgmProto.RuleDirection_IN: - t.Errorf("direction should be IN, got: %v", r.Direction) - return - case r.Protocol != mgmProto.RuleProtocol_ALL: - t.Errorf("protocol should be ALL, got: %v", r.Protocol) - return - case r.Action != mgmProto.RuleAction_ACCEPT: - t.Errorf("action should be ACCEPT, got: %v", r.Action) - return - } + assert.Equal(t, "0.0.0.0", r.PeerIP) + assert.Equal(t, mgmProto.RuleDirection_IN, r.Direction) + assert.Equal(t, mgmProto.RuleProtocol_ALL, r.Protocol) + assert.Equal(t, mgmProto.RuleAction_ACCEPT, r.Action) r = rules[1] - switch { - case r.PeerIP != "0.0.0.0": - t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP) - return - case r.Direction != mgmProto.RuleDirection_OUT: - t.Errorf("direction should be OUT, got: %v", r.Direction) - return - case r.Protocol != mgmProto.RuleProtocol_ALL: - t.Errorf("protocol should be ALL, got: %v", r.Protocol) - return - case r.Action != mgmProto.RuleAction_ACCEPT: - t.Errorf("action should be ACCEPT, got: %v", r.Action) - return - } + assert.Equal(t, "0.0.0.0", r.PeerIP) + assert.Equal(t, mgmProto.RuleDirection_OUT, r.Direction) + assert.Equal(t, mgmProto.RuleProtocol_ALL, r.Protocol) + assert.Equal(t, mgmProto.RuleAction_ACCEPT, r.Action) } func TestDefaultManagerSquashRulesNoAffect(t *testing.T) { @@ -291,9 +328,8 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) { } manager := &DefaultManager{} - if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) { - t.Errorf("we should get the same amount of rules as output, got %v", len(rules)) - } + rules, _ := manager.squashAcceptRules(networkMap) + assert.Equal(t, len(networkMap.FirewallRules), len(rules)) } func TestDefaultManagerEnableSSHRules(t *testing.T) { @@ -337,9 +373,7 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes() ifaceMock.EXPECT().SetFilter(gomock.Any()) ip, network, err := net.ParseCIDR("172.0.0.1/32") - if err != nil { - t.Fatalf("failed to parse IP address: %v", err) - } + require.NoError(t, err) ifaceMock.EXPECT().Name().Return("lo").AnyTimes() ifaceMock.EXPECT().Address().Return(wgaddr.Address{ @@ -348,21 +382,20 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { }).AnyTimes() ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() - // we receive one rule from the management so for testing purposes ignore it fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false) - if err != nil { - t.Errorf("create firewall: %v", err) - return - } - defer func(fw manager.Manager) { - _ = fw.Close(nil) - }(fw) + require.NoError(t, err) + defer func() { + err = fw.Close(nil) + require.NoError(t, err) + }() + acl := NewDefaultManager(fw) acl.ApplyFiltering(networkMap, false) - if len(acl.peerRulesPairs) != 3 { - t.Errorf("expect 3 rules (last must be SSH), got: %d", len(acl.peerRulesPairs)) - return + expectedRules := 3 + if fw.IsStateful() { + expectedRules = 3 // 2 inbound rules + SSH rule } + assert.Equal(t, expectedRules, len(acl.peerRulesPairs)) } diff --git a/client/internal/routemanager/static/route.go b/client/internal/routemanager/static/route.go index 98c34dbee..681c192fb 100644 --- a/client/internal/routemanager/static/route.go +++ b/client/internal/routemanager/static/route.go @@ -24,7 +24,6 @@ func NewRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allo } } -// Route route methods func (r *Route) String() string { return r.route.Network.String() } From 07b220d91ba77248dbf320ea943e83ea6da18e69 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:11:28 +0100 Subject: [PATCH 044/213] [management] REST client impersonation (#3879) --- management/client/rest/client.go | 2 + management/client/rest/impersonation.go | 48 ++++++++++++ management/client/rest/impersonation_test.go | 77 ++++++++++++++++++++ management/client/rest/options.go | 9 +++ management/server/types/settings.go | 2 +- 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 management/client/rest/impersonation.go create mode 100644 management/client/rest/impersonation_test.go diff --git a/management/client/rest/client.go b/management/client/rest/client.go index 25e8ad0da..8bf11caae 100644 --- a/management/client/rest/client.go +++ b/management/client/rest/client.go @@ -86,6 +86,7 @@ func NewWithBearerToken(managementURL, token string) *Client { ) } +// NewWithOptions initialize new Client instance with options func NewWithOptions(opts ...option) *Client { client := &Client{ httpClient: http.DefaultClient, @@ -115,6 +116,7 @@ func (c *Client) initialize() { c.Events = &EventsAPI{c} } +// NewRequest creates and executes new management API request func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body) if err != nil { diff --git a/management/client/rest/impersonation.go b/management/client/rest/impersonation.go new file mode 100644 index 000000000..4d47c9373 --- /dev/null +++ b/management/client/rest/impersonation.go @@ -0,0 +1,48 @@ +package rest + +import ( + "net/http" + "net/url" +) + +// Impersonate returns a Client impersonated for a specific account +func (c *Client) Impersonate(account string) *Client { + client := NewWithOptions( + WithManagementURL(c.managementURL), + WithAuthHeader(c.authHeader), + WithHttpClient(newImpersonatedHttpClient(c, account)), + ) + return client +} + +type impersonatedHttpClient struct { + baseClient HttpClient + account string +} + +func newImpersonatedHttpClient(c *Client, account string) *impersonatedHttpClient { + if hc, ok := c.httpClient.(*impersonatedHttpClient); ok { + hc.account = account + return hc + } + + return &impersonatedHttpClient{ + baseClient: c.httpClient, + account: account, + } +} + +func (c *impersonatedHttpClient) Do(req *http.Request) (*http.Response, error) { + parsedURL, err := url.Parse(req.URL.String()) + if err != nil { + return nil, err + } + + query := parsedURL.Query() + query.Set("account", c.account) + parsedURL.RawQuery = query.Encode() + + req.URL = parsedURL + + return c.baseClient.Do(req) +} diff --git a/management/client/rest/impersonation_test.go b/management/client/rest/impersonation_test.go new file mode 100644 index 000000000..69c0f9728 --- /dev/null +++ b/management/client/rest/impersonation_test.go @@ -0,0 +1,77 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" +) + +var ( + testImpersonatedAccount = api.Account{ + Id: "ImpersonatedTest", + Settings: api.AccountSettings{ + Extra: &api.AccountExtraSettings{ + PeerApprovalEnabled: false, + }, + GroupsPropagationEnabled: ptr(true), + JwtGroupsEnabled: ptr(false), + PeerInactivityExpiration: 7, + PeerInactivityExpirationEnabled: true, + PeerLoginExpiration: 24, + PeerLoginExpirationEnabled: true, + RegularUsersViewBlocked: false, + RoutingPeerDnsResolutionEnabled: ptr(false), + }, + } +) + +func TestImpersonation_Peers_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + impersonatedClient := c.Impersonate(testImpersonatedAccount.Id) + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, r.URL.Query().Get("account"), testImpersonatedAccount.Id) + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := impersonatedClient.Peers.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestImpersonation_Change_Account(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + impersonatedClient := c.Impersonate(testImpersonatedAccount.Id) + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, r.URL.Query().Get("account"), testImpersonatedAccount.Id) + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + _, err := impersonatedClient.Peers.List(context.Background()) + require.NoError(t, err) + + impersonatedClient = impersonatedClient.Impersonate("another-test-account") + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, r.URL.Query().Get("account"), "another-test-account") + retBytes, _ := json.Marshal(testPeer) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + + _, err = impersonatedClient.Peers.Get(context.Background(), "Test") + require.NoError(t, err) + }) +} diff --git a/management/client/rest/options.go b/management/client/rest/options.go index 5aad7dd7e..21f2394e9 100644 --- a/management/client/rest/options.go +++ b/management/client/rest/options.go @@ -2,32 +2,41 @@ package rest import "net/http" +// option modifier for creation of Client type option func(*Client) +// HTTPClient interface for HTTP client type HttpClient interface { Do(req *http.Request) (*http.Response, error) } +// WithHTTPClient overrides HTTPClient used func WithHttpClient(client HttpClient) option { return func(c *Client) { c.httpClient = client } } +// WithBearerToken uses provided bearer token acquired from SSO for authentication func WithBearerToken(token string) option { return WithAuthHeader("Bearer " + token) } +// WithPAT uses provided Personal Access Token +// (created from NetBird Management Dashboard) for authentication func WithPAT(token string) option { return WithAuthHeader("Token " + token) } +// WithManagementURL overrides target NetBird Management server func WithManagementURL(url string) option { return func(c *Client) { c.managementURL = url } } +// WithAuthHeader overrides auth header completely, this should generally not be used +// and WithBearerToken or WithPAT should be used instead func WithAuthHeader(value string) option { return func(c *Client) { c.authHeader = value diff --git a/management/server/types/settings.go b/management/server/types/settings.go index bd361f3ff..a22a36b03 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -45,7 +45,7 @@ type Settings struct { // Extra is a dictionary of Account settings Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` - // LazyConnectionEnabled indicates wether the experimental feature is enabled or disabled + // LazyConnectionEnabled indicates if the experimental feature is enabled or disabled LazyConnectionEnabled bool `gorm:"default:false"` } From 35287f8241f4bf3582ed9d1049c5da5c2e6bde2e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Mon, 2 Jun 2025 23:37:51 +0100 Subject: [PATCH 045/213] [misc] Fail linter workflows on codespell failures (#3913) * Fail linter workflows on codespell failures * testing workflow * remove test --- .github/workflows/golangci-lint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index bdd508e9b..7e6583cc6 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,7 +21,6 @@ jobs: with: ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe skip: go.mod,go.sum - only_warn: 1 golangci: strategy: fail-fast: false From af27aaf9af28124198fcf0b1a90ef3443f6aeb5d Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 3 Jun 2025 09:20:33 +0200 Subject: [PATCH 046/213] [client] Refactor peer state change subscription mechanism (#3910) * Refactor peer state change subscription mechanism Because the code generated new channel for every single event, was easy to miss notification. Use single channel. * Fix lint * Avoid potential deadlock * Fix test * Add context * Fix test --- client/internal/peer/status.go | 81 +++++++++++++++++++++----- client/internal/peer/status_test.go | 13 +++-- client/internal/routemanager/client.go | 11 ++-- 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 40956e68e..0c6aac372 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -1,6 +1,7 @@ package peer import ( + "context" "errors" "net/netip" "slices" @@ -146,11 +147,31 @@ type FullStatus struct { LazyConnectionEnabled bool } +type StatusChangeSubscription struct { + peerID string + id string + eventsChan chan struct{} + ctx context.Context +} + +func newStatusChangeSubscription(ctx context.Context, peerID string) *StatusChangeSubscription { + return &StatusChangeSubscription{ + ctx: ctx, + peerID: peerID, + id: uuid.New().String(), + eventsChan: make(chan struct{}, 1), + } +} + +func (s *StatusChangeSubscription) Events() chan struct{} { + return s.eventsChan +} + // Status holds a state of peers, signal, management connections and relays type Status struct { mux sync.Mutex peers map[string]State - changeNotify map[string]chan struct{} + changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription signalState bool signalError error managementState bool @@ -187,7 +208,7 @@ type Status struct { func NewRecorder(mgmAddress string) *Status { return &Status{ peers: make(map[string]State), - changeNotify: make(map[string]chan struct{}), + changeNotify: make(map[string]map[string]*StatusChangeSubscription), eventStreams: make(map[string]chan *proto.SystemEvent), eventQueue: NewEventQueue(eventQueueSize), offlinePeers: make([]State, 0), @@ -312,7 +333,6 @@ func (d *Status) UpdatePeerState(receivedState State) error { // when we close the connection we will not notify the router manager if receivedState.ConnStatus == StatusIdle { d.notifyPeerStateChangeListeners(receivedState.PubKey) - } return nil } @@ -552,19 +572,41 @@ func (d *Status) FinishPeerListModifications() { d.notifyPeerListChanged() } -// GetPeerStateChangeNotifier returns a change notifier channel for a peer -func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} { +func (d *Status) SubscribeToPeerStateChanges(ctx context.Context, peerID string) *StatusChangeSubscription { d.mux.Lock() defer d.mux.Unlock() - ch, found := d.changeNotify[peer] - if found { - return ch + sub := newStatusChangeSubscription(ctx, peerID) + if _, ok := d.changeNotify[peerID]; !ok { + d.changeNotify[peerID] = make(map[string]*StatusChangeSubscription) + } + d.changeNotify[peerID][sub.id] = sub + + return sub +} + +func (d *Status) UnsubscribePeerStateChanges(subscription *StatusChangeSubscription) { + d.mux.Lock() + defer d.mux.Unlock() + + if subscription == nil { + return } - ch = make(chan struct{}) - d.changeNotify[peer] = ch - return ch + channels, ok := d.changeNotify[subscription.peerID] + if !ok { + return + } + + sub, exists := channels[subscription.id] + if !exists { + return + } + + delete(channels, subscription.id) + if len(channels) == 0 { + delete(d.changeNotify, sub.peerID) + } } // GetLocalPeerState returns the local peer state @@ -939,13 +981,20 @@ func (d *Status) onConnectionChanged() { // notifyPeerStateChangeListeners notifies route manager about the change in peer state func (d *Status) notifyPeerStateChangeListeners(peerID string) { - ch, found := d.changeNotify[peerID] - if !found { + subs, ok := d.changeNotify[peerID] + if !ok { return } - - close(ch) - delete(d.changeNotify, peerID) + for _, sub := range subs { + // block the write because we do not want to miss notification + // must have to be sure we will run the GetPeerState() on separated thread + go func() { + select { + case sub.eventsChan <- struct{}{}: + case <-sub.ctx.Done(): + } + }() + } } func (d *Status) notifyPeerListChanged() { diff --git a/client/internal/peer/status_test.go b/client/internal/peer/status_test.go index 8f28a9862..272638750 100644 --- a/client/internal/peer/status_test.go +++ b/client/internal/peer/status_test.go @@ -1,6 +1,7 @@ package peer import ( + "context" "errors" "sync" "testing" @@ -86,8 +87,8 @@ func TestGetPeerStateChangeNotifierLogic(t *testing.T) { status := NewRecorder("https://mgm") _ = status.AddPeer(key, "abc.netbird", ip) - ch := status.GetPeerStateChangeNotifier(key) - assert.NotNil(t, ch, "channel shouldn't be nil") + sub := status.SubscribeToPeerStateChanges(context.Background(), key) + assert.NotNil(t, sub, "channel shouldn't be nil") peerState := State{ PubKey: key, @@ -99,10 +100,12 @@ func TestGetPeerStateChangeNotifierLogic(t *testing.T) { err := status.UpdatePeerRelayedStateToDisconnected(peerState) assert.NoError(t, err, "shouldn't return error") + timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() select { - case <-ch: - default: - t.Errorf("channel wasn't closed after update") + case <-sub.eventsChan: + case <-timeoutCtx.Done(): + t.Errorf("timed out waiting for event") } } diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 137e00d31..bff954c27 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -224,19 +224,18 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID] } func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) { + subscription := c.statusRecorder.SubscribeToPeerStateChanges(ctx, peerKey) + defer c.statusRecorder.UnsubscribePeerStateChanges(subscription) + for { select { case <-ctx.Done(): return case <-closer: return - case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey): - state, err := c.statusRecorder.GetPeer(peerKey) - if err != nil { - continue - } + case <-subscription.Events(): peerStateUpdate <- struct{}{} - log.Debugf("triggered route state update for Peer %s, state: %s", peerKey, state.ConnStatus) + log.Debugf("triggered route state update for Peer: %s", peerKey) } } } From 616b19c0644be0d1b0b5ccf4632653f6402e4faf Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:49:13 +0300 Subject: [PATCH 047/213] [client] Add "Deselect All" Menu Item to Exit Node Menu (#3877) * [client] Enhance exit node menu functionality with deselect all option * Hide exit nodes before removal in recreateExitNodeMenu * recreateExitNodeMenu adding mutex locks * Refetch exit nodes after deselecting all in exit node menu --- client/ui/client_ui.go | 18 +++++---- client/ui/network.go | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index c23b78582..c0c8692c6 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -235,9 +235,11 @@ type serviceClient struct { eventManager *event.Manager - exitNodeMu sync.Mutex - mExitNodeItems []menuHandler - logFile string + exitNodeMu sync.Mutex + mExitNodeItems []menuHandler + exitNodeStates []exitNodeState + mExitNodeDeselectAll *systray.MenuItem + logFile string } type menuHandler struct { @@ -1035,11 +1037,11 @@ func (s *serviceClient) updateConfig() error { lazyConnectionEnabled := s.mLazyConnEnabled.Checked() loginRequest := proto.LoginRequest{ - IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", - ServerSSHAllowed: &sshAllowed, - RosenpassEnabled: &rosenpassEnabled, - DisableAutoConnect: &disableAutoStart, - DisableNotifications: ¬ificationsDisabled, + IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", + ServerSSHAllowed: &sshAllowed, + RosenpassEnabled: &rosenpassEnabled, + DisableAutoConnect: &disableAutoStart, + DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, } diff --git a/client/ui/network.go b/client/ui/network.go index 435917f30..b3748a89d 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "runtime" + "slices" "sort" "strings" "time" @@ -33,6 +34,11 @@ const ( type filter string +type exitNodeState struct { + id string + selected bool +} + func (s *serviceClient) showNetworksUI() { s.wNetworks = s.app.NewWindow("Networks") s.wNetworks.SetOnClosed(s.cancel) @@ -357,18 +363,45 @@ func (s *serviceClient) updateExitNodes() { } func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { + var exitNodeIDs []exitNodeState + for _, node := range exitNodes { + exitNodeIDs = append(exitNodeIDs, exitNodeState{ + id: node.ID, + selected: node.Selected, + }) + } + + sort.Slice(exitNodeIDs, func(i, j int) bool { + return exitNodeIDs[i].id < exitNodeIDs[j].id + }) + if slices.Equal(s.exitNodeStates, exitNodeIDs) { + log.Debug("Exit node menu already up to date") + return + } + for _, node := range s.mExitNodeItems { node.cancel() + node.Hide() node.Remove() } s.mExitNodeItems = nil + if s.mExitNodeDeselectAll != nil { + s.mExitNodeDeselectAll.Remove() + s.mExitNodeDeselectAll = nil + } if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { s.mExitNode.Remove() s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) } + var showDeselectAll bool + for _, node := range exitNodes { + if node.Selected { + showDeselectAll = true + } + menuItem := s.mExitNode.AddSubMenuItemCheckbox( node.ID, fmt.Sprintf("Use exit node %s", node.ID), @@ -383,6 +416,32 @@ func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { go s.handleChecked(ctx, node.ID, menuItem) } + s.exitNodeStates = exitNodeIDs + + if showDeselectAll { + s.mExitNode.AddSeparator() + deselectAllItem := s.mExitNode.AddSubMenuItem("Deselect All", "Deselect All") + s.mExitNodeDeselectAll = deselectAllItem + go func() { + for { + _, ok := <-deselectAllItem.ClickedCh + if !ok { + // channel closed: exit the goroutine + return + } + exitNodes, err := s.handleExitNodeMenuDeselectAll() + if err != nil { + log.Warnf("failed to handle deselect all exit nodes: %v", err) + } else { + s.exitNodeMu.Lock() + s.recreateExitNodeMenu(exitNodes) + s.exitNodeMu.Unlock() + } + } + + }() + } + } func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) { @@ -420,6 +479,37 @@ func (s *serviceClient) handleChecked(ctx context.Context, id string, item *syst } } +func (s *serviceClient) handleExitNodeMenuDeselectAll() ([]*proto.Network, error) { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return nil, fmt.Errorf("get client: %v", err) + } + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + return nil, fmt.Errorf("get exit nodes: %v", err) + } + + var ids []string + for _, e := range exitNodes { + if e.Selected { + ids = append(ids, e.ID) + } + } + + // deselect selected exit nodes + if err := s.deselectOtherExitNodes(conn, ids); err != nil { + return nil, err + } + + updatedExitNodes, err := s.getExitNodes(conn) + if err != nil { + return nil, fmt.Errorf("re-fetch exit nodes: %v", err) + } + + return updatedExitNodes, nil +} + // Add function to toggle exit node selection func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error { conn, err := s.getSrvClient(defaultFailTimeout) From f367925496136074a4f5911e965de4fd12a5ef87 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:52:10 +0200 Subject: [PATCH 048/213] [client] Log duplicate client ui pid (#3915) --- client/ui/client_ui.go | 4 ++-- client/ui/process/process.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index c0c8692c6..92289a8a3 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -89,13 +89,13 @@ func main() { } // Check for another running process. - running, err := process.IsAnotherProcessRunning() + pid, running, err := process.IsAnotherProcessRunning() if err != nil { log.Errorf("error while checking process: %v", err) return } if running { - log.Warn("another process is running") + log.Warnf("another process is running with pid %d, exiting", pid) return } diff --git a/client/ui/process/process.go b/client/ui/process/process.go index f9a8a4fe9..d0ef54896 100644 --- a/client/ui/process/process.go +++ b/client/ui/process/process.go @@ -8,10 +8,10 @@ import ( "github.com/shirou/gopsutil/v3/process" ) -func IsAnotherProcessRunning() (bool, error) { +func IsAnotherProcessRunning() (int32, bool, error) { processes, err := process.Processes() if err != nil { - return false, err + return 0, false, err } pid := os.Getpid() @@ -29,9 +29,9 @@ func IsAnotherProcessRunning() (bool, error) { } if strings.Contains(strings.ToLower(runningProcessPath), processName) && isProcessOwnedByCurrentUser(p) { - return true, nil + return p.Pid, true, nil } } - return false, nil + return 0, false, nil } From 1ce4ee0cef6a5cae84b3daf1ea8fc534bfbc5ffb Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:53:27 +0200 Subject: [PATCH 049/213] [client] Add block inbound flag to disallow inbound connections of any kind (#3897) --- client/cmd/root.go | 2 - client/cmd/system.go | 11 + client/cmd/up.go | 266 +++--- client/firewall/iptables/manager_linux.go | 8 +- client/firewall/iptables/router_linux.go | 8 - client/firewall/nftables/manager_linux.go | 6 + client/firewall/nftables/router_linux.go | 8 - client/firewall/uspfilter/uspfilter.go | 2 +- client/internal/acl/manager.go | 11 +- client/internal/config.go | 18 +- client/internal/connect.go | 5 +- client/internal/debug/debug.go | 26 +- client/internal/engine.go | 118 +-- client/proto/daemon.pb.go | 1064 +++++++++++---------- client/proto/daemon.proto | 7 +- client/server/server.go | 31 +- client/ui/client_ui.go | 29 +- client/ui/const.go | 3 +- 18 files changed, 878 insertions(+), 745 deletions(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 9bcf65df9..16e445f4d 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -39,7 +39,6 @@ const ( extraIFaceBlackListFlag = "extra-iface-blacklist" dnsRouteIntervalFlag = "dns-router-interval" systemInfoFlag = "system-info" - blockLANAccessFlag = "block-lan-access" enableLazyConnectionFlag = "enable-lazy-connection" uploadBundle = "upload-bundle" uploadBundleURL = "upload-bundle-url" @@ -78,7 +77,6 @@ var ( anonymizeFlag bool debugSystemInfoFlag bool dnsRouteInterval time.Duration - blockLANAccess bool debugUploadBundle bool debugUploadBundleURL string lazyConnEnabled bool diff --git a/client/cmd/system.go b/client/cmd/system.go index f628867a7..83ce8d215 100644 --- a/client/cmd/system.go +++ b/client/cmd/system.go @@ -6,6 +6,8 @@ const ( disableServerRoutesFlag = "disable-server-routes" disableDNSFlag = "disable-dns" disableFirewallFlag = "disable-firewall" + blockLANAccessFlag = "block-lan-access" + blockInboundFlag = "block-inbound" ) var ( @@ -13,6 +15,8 @@ var ( disableServerRoutes bool disableDNS bool disableFirewall bool + blockLANAccess bool + blockInbound bool ) func init() { @@ -28,4 +32,11 @@ func init() { upCmd.PersistentFlags().BoolVar(&disableFirewall, disableFirewallFlag, false, "Disable firewall configuration. If enabled, the client won't modify firewall rules.") + + upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, + "Block access to local networks (LAN) when using this peer as a router or exit node") + + upCmd.PersistentFlags().BoolVar(&blockInbound, blockInboundFlag, false, + "Block inbound connections. If enabled, the client will not allow any inbound connections to the local machine nor routed networks.\n"+ + "This overrides any policies received from the management service.") } diff --git a/client/cmd/up.go b/client/cmd/up.go index 2dcf2282b..b9781c0df 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -55,12 +55,11 @@ func init() { upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name") upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port") upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor, - `Manage network monitoring. Defaults to true on Windows and macOS, false on Linux. `+ + `Manage network monitoring. Defaults to true on Windows and macOS, false on Linux and FreeBSD. `+ `E.g. --network-monitor=false to disable or --network-monitor=true to enable.`, ) upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening") upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval") - upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node") upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil, `Sets DNS labels`+ @@ -119,83 +118,9 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { return err } - ic := internal.ConfigInput{ - ManagementURL: managementURL, - AdminURL: adminURL, - ConfigPath: configPath, - NATExternalIPs: natExternalIPs, - CustomDNSAddress: customDNSAddressConverted, - ExtraIFaceBlackList: extraIFaceBlackList, - DNSLabels: dnsLabelsValidated, - } - - if cmd.Flag(enableRosenpassFlag).Changed { - ic.RosenpassEnabled = &rosenpassEnabled - } - - if cmd.Flag(rosenpassPermissiveFlag).Changed { - ic.RosenpassPermissive = &rosenpassPermissive - } - - if cmd.Flag(serverSSHAllowedFlag).Changed { - ic.ServerSSHAllowed = &serverSSHAllowed - } - - if cmd.Flag(interfaceNameFlag).Changed { - if err := parseInterfaceName(interfaceName); err != nil { - return err - } - ic.InterfaceName = &interfaceName - } - - if cmd.Flag(wireguardPortFlag).Changed { - p := int(wireguardPort) - ic.WireguardPort = &p - } - - if cmd.Flag(networkMonitorFlag).Changed { - ic.NetworkMonitor = &networkMonitor - } - - if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { - ic.PreSharedKey = &preSharedKey - } - - if cmd.Flag(disableAutoConnectFlag).Changed { - ic.DisableAutoConnect = &autoConnectDisabled - - if autoConnectDisabled { - cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.") - } - - if !autoConnectDisabled { - cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.") - } - } - - if cmd.Flag(dnsRouteIntervalFlag).Changed { - ic.DNSRouteInterval = &dnsRouteInterval - } - - if cmd.Flag(disableClientRoutesFlag).Changed { - ic.DisableClientRoutes = &disableClientRoutes - } - if cmd.Flag(disableServerRoutesFlag).Changed { - ic.DisableServerRoutes = &disableServerRoutes - } - if cmd.Flag(disableDNSFlag).Changed { - ic.DisableDNS = &disableDNS - } - if cmd.Flag(disableFirewallFlag).Changed { - ic.DisableFirewall = &disableFirewall - } - - if cmd.Flag(blockLANAccessFlag).Changed { - ic.BlockLANAccess = &blockLANAccess - } - - if cmd.Flag(enableLazyConnectionFlag).Changed { - ic.LazyConnectionEnabled = &lazyConnEnabled + ic, err := setupConfig(customDNSAddressConverted, cmd) + if err != nil { + return fmt.Errorf("setup config: %v", err) } providedSetupKey, err := getSetupKey() @@ -203,7 +128,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { return err } - config, err := internal.UpdateOrCreateConfig(ic) + config, err := internal.UpdateOrCreateConfig(*ic) if err != nil { return fmt.Errorf("get config file: %v", err) } @@ -262,9 +187,141 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { providedSetupKey, err := getSetupKey() if err != nil { - return err + return fmt.Errorf("get setup key: %v", err) } + loginRequest, err := setupLoginRequest(providedSetupKey, customDNSAddressConverted, cmd) + if err != nil { + return fmt.Errorf("setup login request: %v", err) + } + + var loginErr error + var loginResp *proto.LoginResponse + + err = WithBackOff(func() error { + var backOffErr error + loginResp, backOffErr = client.Login(ctx, loginRequest) + if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument || + s.Code() == codes.PermissionDenied || + s.Code() == codes.NotFound || + s.Code() == codes.Unimplemented) { + loginErr = backOffErr + return nil + } + return backOffErr + }) + if err != nil { + return fmt.Errorf("login backoff cycle failed: %v", err) + } + + if loginErr != nil { + return fmt.Errorf("login failed: %v", loginErr) + } + + if loginResp.NeedsSSOLogin { + + openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser) + + _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) + if err != nil { + return fmt.Errorf("waiting sso login failed with: %v", err) + } + } + + if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil { + return fmt.Errorf("call service up method: %v", err) + } + cmd.Println("Connected") + return nil +} + +func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) { + ic := internal.ConfigInput{ + ManagementURL: managementURL, + AdminURL: adminURL, + ConfigPath: configPath, + NATExternalIPs: natExternalIPs, + CustomDNSAddress: customDNSAddressConverted, + ExtraIFaceBlackList: extraIFaceBlackList, + DNSLabels: dnsLabelsValidated, + } + + if cmd.Flag(enableRosenpassFlag).Changed { + ic.RosenpassEnabled = &rosenpassEnabled + } + + if cmd.Flag(rosenpassPermissiveFlag).Changed { + ic.RosenpassPermissive = &rosenpassPermissive + } + + if cmd.Flag(serverSSHAllowedFlag).Changed { + ic.ServerSSHAllowed = &serverSSHAllowed + } + + if cmd.Flag(interfaceNameFlag).Changed { + if err := parseInterfaceName(interfaceName); err != nil { + return nil, err + } + ic.InterfaceName = &interfaceName + } + + if cmd.Flag(wireguardPortFlag).Changed { + p := int(wireguardPort) + ic.WireguardPort = &p + } + + if cmd.Flag(networkMonitorFlag).Changed { + ic.NetworkMonitor = &networkMonitor + } + + if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { + ic.PreSharedKey = &preSharedKey + } + + if cmd.Flag(disableAutoConnectFlag).Changed { + ic.DisableAutoConnect = &autoConnectDisabled + + if autoConnectDisabled { + cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.") + } + + if !autoConnectDisabled { + cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.") + } + } + + if cmd.Flag(dnsRouteIntervalFlag).Changed { + ic.DNSRouteInterval = &dnsRouteInterval + } + + if cmd.Flag(disableClientRoutesFlag).Changed { + ic.DisableClientRoutes = &disableClientRoutes + } + if cmd.Flag(disableServerRoutesFlag).Changed { + ic.DisableServerRoutes = &disableServerRoutes + } + if cmd.Flag(disableDNSFlag).Changed { + ic.DisableDNS = &disableDNS + } + if cmd.Flag(disableFirewallFlag).Changed { + ic.DisableFirewall = &disableFirewall + } + + if cmd.Flag(blockLANAccessFlag).Changed { + ic.BlockLANAccess = &blockLANAccess + } + + if cmd.Flag(blockInboundFlag).Changed { + ic.BlockInbound = &blockInbound + } + + if cmd.Flag(enableLazyConnectionFlag).Changed { + ic.LazyConnectionEnabled = &lazyConnEnabled + } + return &ic, nil +} + +func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) { loginRequest := proto.LoginRequest{ SetupKey: providedSetupKey, ManagementUrl: managementURL, @@ -301,7 +358,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { if cmd.Flag(interfaceNameFlag).Changed { if err := parseInterfaceName(interfaceName); err != nil { - return err + return nil, err } loginRequest.InterfaceName = &interfaceName } @@ -336,49 +393,14 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { loginRequest.BlockLanAccess = &blockLANAccess } + if cmd.Flag(blockInboundFlag).Changed { + loginRequest.BlockInbound = &blockInbound + } + if cmd.Flag(enableLazyConnectionFlag).Changed { loginRequest.LazyConnectionEnabled = &lazyConnEnabled } - - var loginErr error - - var loginResp *proto.LoginResponse - - err = WithBackOff(func() error { - var backOffErr error - loginResp, backOffErr = client.Login(ctx, &loginRequest) - if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument || - s.Code() == codes.PermissionDenied || - s.Code() == codes.NotFound || - s.Code() == codes.Unimplemented) { - loginErr = backOffErr - return nil - } - return backOffErr - }) - if err != nil { - return fmt.Errorf("login backoff cycle failed: %v", err) - } - - if loginErr != nil { - return fmt.Errorf("login failed: %v", loginErr) - } - - if loginResp.NeedsSSOLogin { - - openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser) - - _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) - if err != nil { - return fmt.Errorf("waiting sso login failed with: %v", err) - } - } - - if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil { - return fmt.Errorf("call service up method: %v", err) - } - cmd.Println("Connected") - return nil + return &loginRequest, nil } func validateNATExternalIPs(list []string) error { diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 0897f831f..81f7a9125 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -202,7 +202,7 @@ func (m *Manager) AllowNetbird() error { _, err := m.AddPeerFiltering( nil, net.IP{0, 0, 0, 0}, - "all", + firewall.ProtocolALL, nil, nil, firewall.ActionAccept, @@ -223,10 +223,16 @@ func (m *Manager) SetLogLevel(log.Level) { } func (m *Manager) EnableRouting() error { + if err := m.router.ipFwdState.RequestForwarding(); err != nil { + return fmt.Errorf("enable IP forwarding: %w", err) + } return nil } func (m *Manager) DisableRouting() error { + if err := m.router.ipFwdState.ReleaseForwarding(); err != nil { + return fmt.Errorf("disable IP forwarding: %w", err) + } return nil } diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go index bb799b99b..1e44c7a4d 100644 --- a/client/firewall/iptables/router_linux.go +++ b/client/firewall/iptables/router_linux.go @@ -248,10 +248,6 @@ func (r *router) deleteIpSet(setName string) error { // AddNatRule inserts an iptables rule pair into the nat chain func (r *router) AddNatRule(pair firewall.RouterPair) error { - if err := r.ipFwdState.RequestForwarding(); err != nil { - return err - } - if r.legacyManagement { log.Warnf("This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s", pair.Destination) if err := r.addLegacyRouteRule(pair); err != nil { @@ -278,10 +274,6 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error { // RemoveNatRule removes an iptables rule pair from forwarding and nat chains func (r *router) RemoveNatRule(pair firewall.RouterPair) error { - if err := r.ipFwdState.ReleaseForwarding(); err != nil { - log.Errorf("%v", err) - } - if pair.Masquerade { if err := r.removeNatRule(pair); err != nil { return fmt.Errorf("remove nat rule: %w", err) diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 2f8ee81a4..560f224f5 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -328,10 +328,16 @@ func (m *Manager) SetLogLevel(log.Level) { } func (m *Manager) EnableRouting() error { + if err := m.router.ipFwdState.RequestForwarding(); err != nil { + return fmt.Errorf("enable IP forwarding: %w", err) + } return nil } func (m *Manager) DisableRouting() error { + if err := m.router.ipFwdState.ReleaseForwarding(); err != nil { + return fmt.Errorf("disable IP forwarding: %w", err) + } return nil } diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go index 0f6c5bdf6..f8fed4d80 100644 --- a/client/firewall/nftables/router_linux.go +++ b/client/firewall/nftables/router_linux.go @@ -573,10 +573,6 @@ func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error { // AddNatRule appends a nftables rule pair to the nat chain func (r *router) AddNatRule(pair firewall.RouterPair) error { - if err := r.ipFwdState.RequestForwarding(); err != nil { - return err - } - if err := r.refreshRulesMap(); err != nil { return fmt.Errorf(refreshRulesMapError, err) } @@ -1006,10 +1002,6 @@ func (r *router) removeAcceptForwardRulesIptables(ipt *iptables.IPTables) error // RemoveNatRule removes the prerouting mark rule func (r *router) RemoveNatRule(pair firewall.RouterPair) error { - if err := r.ipFwdState.ReleaseForwarding(); err != nil { - log.Errorf("%v", err) - } - if err := r.refreshRulesMap(); err != nil { return fmt.Errorf(refreshRulesMapError, err) } diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 287e52773..8e0a955ca 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -269,7 +269,7 @@ func (m *Manager) determineRouting() error { log.Info("userspace routing is forced") - case !m.netstack && m.nativeFirewall != nil && m.nativeFirewall.IsServerRouteSupported(): + case !m.netstack && m.nativeFirewall != nil: // if the OS supports routing natively, then we don't need to filter/route ourselves // netstack mode won't support native routing as there is no interface diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 5caf2b770..c8bc9123b 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -58,6 +58,11 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRout d.mutex.Lock() defer d.mutex.Unlock() + if d.firewall == nil { + log.Debug("firewall manager is not supported, skipping firewall rules") + return + } + start := time.Now() defer func() { total := 0 @@ -69,14 +74,8 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRout time.Since(start), total) }() - if d.firewall == nil { - log.Debug("firewall manager is not supported, skipping firewall rules") - return - } - d.applyPeerACLs(networkMap) - if err := d.applyRouteACLs(networkMap.RoutesFirewallRules, dnsRouteFeatureFlag); err != nil { log.Errorf("Failed to apply route ACLs: %v", err) } diff --git a/client/internal/config.go b/client/internal/config.go index 86dd7ebb1..45a7620e1 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -68,8 +68,8 @@ type ConfigInput struct { DisableServerRoutes *bool DisableDNS *bool DisableFirewall *bool - - BlockLANAccess *bool + BlockLANAccess *bool + BlockInbound *bool DisableNotifications *bool @@ -98,8 +98,8 @@ type Config struct { DisableServerRoutes bool DisableDNS bool DisableFirewall bool - - BlockLANAccess bool + BlockLANAccess bool + BlockInbound bool DisableNotifications *bool @@ -483,6 +483,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.BlockInbound != nil && *input.BlockInbound != config.BlockInbound { + if *input.BlockInbound { + log.Infof("blocking inbound connections") + } else { + log.Infof("allowing inbound connections") + } + config.BlockInbound = *input.BlockInbound + updated = true + } + if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications { if *input.DisableNotifications { log.Infof("disabling notifications") diff --git a/client/internal/connect.go b/client/internal/connect.go index 1428d2656..1cfef77f2 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -436,11 +436,12 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe DNSRouteInterval: config.DNSRouteInterval, DisableClientRoutes: config.DisableClientRoutes, - DisableServerRoutes: config.DisableServerRoutes, + DisableServerRoutes: config.DisableServerRoutes || config.BlockInbound, DisableDNS: config.DisableDNS, DisableFirewall: config.DisableFirewall, + BlockLANAccess: config.BlockLANAccess, + BlockInbound: config.BlockInbound, - BlockLANAccess: config.BlockLANAccess, LazyConnectionEnabled: config.LazyConnectionEnabled, } diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 2192872df..a753ece0c 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -366,17 +366,33 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) configContent.WriteString(fmt.Sprintf("RosenpassEnabled: %v\n", g.internalConfig.RosenpassEnabled)) configContent.WriteString(fmt.Sprintf("RosenpassPermissive: %v\n", g.internalConfig.RosenpassPermissive)) if g.internalConfig.ServerSSHAllowed != nil { - configContent.WriteString(fmt.Sprintf("BundleGeneratorSSHAllowed: %v\n", *g.internalConfig.ServerSSHAllowed)) + configContent.WriteString(fmt.Sprintf("ServerSSHAllowed: %v\n", *g.internalConfig.ServerSSHAllowed)) } - configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", g.internalConfig.DisableAutoConnect)) - configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", g.internalConfig.DNSRouteInterval)) configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", g.internalConfig.DisableClientRoutes)) - configContent.WriteString(fmt.Sprintf("DisableBundleGeneratorRoutes: %v\n", g.internalConfig.DisableServerRoutes)) + configContent.WriteString(fmt.Sprintf("DisableServerRoutes: %v\n", g.internalConfig.DisableServerRoutes)) configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", g.internalConfig.DisableDNS)) configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", g.internalConfig.DisableFirewall)) - configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess)) + configContent.WriteString(fmt.Sprintf("BlockInbound: %v\n", g.internalConfig.BlockInbound)) + + if g.internalConfig.DisableNotifications != nil { + configContent.WriteString(fmt.Sprintf("DisableNotifications: %v\n", *g.internalConfig.DisableNotifications)) + } + + configContent.WriteString(fmt.Sprintf("DNSLabels: %v\n", g.internalConfig.DNSLabels)) + + configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", g.internalConfig.DisableAutoConnect)) + + configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", g.internalConfig.DNSRouteInterval)) + + if g.internalConfig.ClientCertPath != "" { + configContent.WriteString(fmt.Sprintf("ClientCertPath: %s\n", g.internalConfig.ClientCertPath)) + } + if g.internalConfig.ClientCertKeyPath != "" { + configContent.WriteString(fmt.Sprintf("ClientCertKeyPath: %s\n", g.internalConfig.ClientCertKeyPath)) + } + configContent.WriteString(fmt.Sprintf("LazyConnectionEnabled: %v\n", g.internalConfig.LazyConnectionEnabled)) } diff --git a/client/internal/engine.go b/client/internal/engine.go index e04ccc9b8..5efc0b92b 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -121,8 +121,8 @@ type EngineConfig struct { DisableServerRoutes bool DisableDNS bool DisableFirewall bool - - BlockLANAccess bool + BlockLANAccess bool + BlockInbound bool LazyConnectionEnabled bool } @@ -431,7 +431,8 @@ func (e *Engine) Start() error { return fmt.Errorf("up wg interface: %w", err) } - if e.firewall != nil { + // if inbound conns are blocked there is no need to create the ACL manager + if e.firewall != nil && !e.config.BlockInbound { e.acl = acl.NewDefaultManager(e.firewall) } @@ -487,11 +488,9 @@ func (e *Engine) createFirewall() error { } func (e *Engine) initFirewall() error { - if e.firewall.IsServerRouteSupported() { - if err := e.routeManager.EnableServerRouter(e.firewall); err != nil { - e.close() - return fmt.Errorf("enable server router: %w", err) - } + if err := e.routeManager.EnableServerRouter(e.firewall); err != nil { + e.close() + return fmt.Errorf("enable server router: %w", err) } if e.config.BlockLANAccess { @@ -525,6 +524,11 @@ func (e *Engine) initFirewall() error { } func (e *Engine) blockLanAccess() { + if e.config.BlockInbound { + // no need to set up extra deny rules if inbound is already blocked in general + return + } + var merr *multierror.Error // TODO: keep this updated @@ -796,56 +800,58 @@ func isNil(server nbssh.Server) bool { } func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error { + if e.config.BlockInbound { + log.Infof("SSH server is disabled because inbound connections are blocked") + return nil + } if !e.config.ServerSSHAllowed { - log.Warnf("running SSH server is not permitted") + log.Info("SSH server is not enabled") return nil - } else { - - if sshConf.GetSshEnabled() { - if runtime.GOOS == "windows" { - log.Warnf("running SSH server on %s is not supported", runtime.GOOS) - return nil - } - // start SSH server if it wasn't running - if isNil(e.sshServer) { - listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort) - if nbnetstack.IsEnabled() { - listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort) - } - // nil sshServer means it has not yet been started - var err error - e.sshServer, err = e.sshServerFunc(e.config.SSHKey, listenAddr) - - if err != nil { - return fmt.Errorf("create ssh server: %w", err) - } - go func() { - // blocking - err = e.sshServer.Start() - if err != nil { - // will throw error when we stop it even if it is a graceful stop - log.Debugf("stopped SSH server with error %v", err) - } - e.syncMsgMux.Lock() - defer e.syncMsgMux.Unlock() - e.sshServer = nil - log.Infof("stopped SSH server") - }() - } else { - log.Debugf("SSH server is already running") - } - } else if !isNil(e.sshServer) { - // Disable SSH server request, so stop it if it was running - err := e.sshServer.Stop() - if err != nil { - log.Warnf("failed to stop SSH server %v", err) - } - e.sshServer = nil - } - return nil - } + + if sshConf.GetSshEnabled() { + if runtime.GOOS == "windows" { + log.Warnf("running SSH server on %s is not supported", runtime.GOOS) + return nil + } + // start SSH server if it wasn't running + if isNil(e.sshServer) { + listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort) + if nbnetstack.IsEnabled() { + listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort) + } + // nil sshServer means it has not yet been started + var err error + e.sshServer, err = e.sshServerFunc(e.config.SSHKey, listenAddr) + + if err != nil { + return fmt.Errorf("create ssh server: %w", err) + } + go func() { + // blocking + err = e.sshServer.Start() + if err != nil { + // will throw error when we stop it even if it is a graceful stop + log.Debugf("stopped SSH server with error %v", err) + } + e.syncMsgMux.Lock() + defer e.syncMsgMux.Unlock() + e.sshServer = nil + log.Infof("stopped SSH server") + }() + } else { + log.Debugf("SSH server is already running") + } + } else if !isNil(e.sshServer) { + // Disable SSH server request, so stop it if it was running + err := e.sshServer.Stop() + if err != nil { + log.Warnf("failed to stop SSH server %v", err) + } + e.sshServer = nil + } + return nil } func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { @@ -1796,6 +1802,10 @@ func (e *Engine) updateDNSForwarder( enabled bool, fwdEntries []*dnsfwd.ForwarderEntry, ) { + if e.config.DisableServerRoutes { + return + } + if !enabled { if e.dnsForwardMgr == nil { return diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 402dd2f9a..b88d0aa31 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -279,6 +279,7 @@ type LoginRequest struct { // omits initialized empty slices due to omitempty tags CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` LazyConnectionEnabled *bool `protobuf:"varint,28,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"` + BlockInbound *bool `protobuf:"varint,29,opt,name=block_inbound,json=blockInbound,proto3,oneof" json:"block_inbound,omitempty"` } func (x *LoginRequest) Reset() { @@ -510,6 +511,13 @@ func (x *LoginRequest) GetLazyConnectionEnabled() bool { return false } +func (x *LoginRequest) GetBlockInbound() bool { + if x != nil && x.BlockInbound != nil { + return *x.BlockInbound + } + return false +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -990,14 +998,16 @@ type GetConfigResponse struct { // preSharedKey settings value. PreSharedKey string `protobuf:"bytes,4,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"` // adminURL settings value. - AdminURL string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"` - InterfaceName string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"` - WireguardPort int64 `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"` - DisableAutoConnect bool `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"` - ServerSSHAllowed bool `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` - RosenpassEnabled bool `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - RosenpassPermissive bool `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` - DisableNotifications bool `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"` + AdminURL string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"` + InterfaceName string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"` + WireguardPort int64 `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"` + DisableAutoConnect bool `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"` + ServerSSHAllowed bool `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` + RosenpassEnabled bool `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + DisableNotifications bool `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"` + LazyConnectionEnabled bool `protobuf:"varint,14,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` + BlockInbound bool `protobuf:"varint,15,opt,name=blockInbound,proto3" json:"blockInbound,omitempty"` } func (x *GetConfigResponse) Reset() { @@ -1116,6 +1126,20 @@ func (x *GetConfigResponse) GetDisableNotifications() bool { return false } +func (x *GetConfigResponse) GetLazyConnectionEnabled() bool { + if x != nil { + return x.LazyConnectionEnabled + } + return false +} + +func (x *GetConfigResponse) GetBlockInbound() bool { + if x != nil { + return x.BlockInbound + } + return false +} + // PeerState contains the latest state of a peer type PeerState struct { state protoimpl.MessageState @@ -3625,7 +3649,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0e, 0x0a, 0x0c, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x83, 0x0d, 0x0a, 0x0c, 0x4c, 0x6f, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x0d, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, @@ -3707,516 +3731,526 @@ var file_daemon_proto_rawDesc = []byte{ 0x62, 0x65, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0f, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, - 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, - 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, - 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, - 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, - 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, - 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, - 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, - 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, - 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, - 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, - 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, - 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, - 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, - 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, - 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, - 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, - 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, - 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, - 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, - 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, - 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, - 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, - 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, - 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0xee, 0x03, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, - 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, - 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, - 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, - 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, - 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, - 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, - 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, - 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, - 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, + 0x28, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x18, 0x1d, 0x20, 0x01, 0x28, 0x08, 0x48, 0x10, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, + 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, + 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, + 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, + 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, - 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, - 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, - 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, - 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, - 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, - 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, - 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, - 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, - 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, - 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, - 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, - 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, - 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, - 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, - 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, - 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, - 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, - 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, + 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, + 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, + 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, + 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, + 0x18, 0x0a, 0x16, 0x5f, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xb5, 0x01, 0x0a, 0x0d, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, + 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, + 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, + 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, + 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc8, 0x04, 0x0a, + 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, + 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, + 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, + 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, + 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, + 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, + 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, + 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, + 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, + 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, + 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, + 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, - 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, - 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, - 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xef, 0x03, 0x0a, 0x0a, - 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, - 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, - 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, - 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, - 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, - 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, - 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x15, 0x0a, - 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, - 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, - 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, - 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, - 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, - 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, - 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, - 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, - 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, - 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, - 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, - 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, - 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, - 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, - 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, - 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, - 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x88, 0x01, 0x0a, 0x12, - 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, - 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, - 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, - 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, - 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, - 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, - 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, - 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, - 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, - 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, - 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, - 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, - 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, - 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, - 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, - 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, - 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, - 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, - 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, - 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, - 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, - 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, - 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, - 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, - 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, - 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, - 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, - 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, - 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, - 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, - 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, - 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, - 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, - 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, - 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, - 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, - 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, - 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, - 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, - 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, - 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, - 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, 0x0a, 0x0d, 0x44, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, - 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, - 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, - 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, - 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, - 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, - 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, - 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, - 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, + 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, + 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, + 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, + 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, + 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, + 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, + 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, + 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, + 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, + 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, + 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, + 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, + 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, + 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, + 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, + 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, + 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, + 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, + 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, + 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, + 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, + 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, + 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, + 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, + 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, + 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, + 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, + 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, + 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x22, 0xef, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, + 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, + 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, + 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, + 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, + 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, + 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, + 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, + 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, + 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, + 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, + 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, + 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, + 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, + 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, + 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, + 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, + 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, + 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, + 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, + 0x73, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, + 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, + 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, + 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, + 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, + 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, + 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, + 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, + 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, + 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, + 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, + 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, - 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, - 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, - 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, - 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, - 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, - 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, + 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, + 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, + 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, + 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, + 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, + 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, + 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, + 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, + 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, + 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, + 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, + 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, + 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, + 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, + 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, + 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, + 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, + 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, + 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, + 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, + 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, + 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, + 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, + 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, + 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, + 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, + 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, + 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, + 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, + 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, + 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, + 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, + 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, + 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, + 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, + 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, + 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, + 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, + 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, + 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, + 0x0b, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, + 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, + 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, + 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, + 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, + 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, + 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, + 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, + 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, + 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 6fa391c8e..a46ba554a 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -122,7 +122,6 @@ message LoginRequest { optional bool disable_server_routes = 21; optional bool disable_dns = 22; optional bool disable_firewall = 23; - optional bool block_lan_access = 24; optional bool disable_notifications = 25; @@ -135,6 +134,8 @@ message LoginRequest { bool cleanDNSLabels = 27; optional bool lazyConnectionEnabled = 28; + + optional bool block_inbound = 29; } message LoginResponse { @@ -202,6 +203,10 @@ message GetConfigResponse { bool rosenpassPermissive = 12; bool disable_notifications = 13; + + bool lazyConnectionEnabled = 14; + + bool blockInbound = 15; } // PeerState contains the latest state of a peer diff --git a/client/server/server.go b/client/server/server.go index 43b3eb3b7..2025a89ec 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -398,11 +398,14 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro inputConfig.DisableFirewall = msg.DisableFirewall s.latestConfigInput.DisableFirewall = msg.DisableFirewall } - if msg.BlockLanAccess != nil { inputConfig.BlockLANAccess = msg.BlockLanAccess s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess } + if msg.BlockInbound != nil { + inputConfig.BlockInbound = msg.BlockInbound + s.latestConfigInput.BlockInbound = msg.BlockInbound + } if msg.CleanDNSLabels { inputConfig.DNSLabels = domain.List{} @@ -756,18 +759,20 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto } return &proto.GetConfigResponse{ - ManagementUrl: managementURL, - ConfigFile: s.latestConfigInput.ConfigPath, - LogFile: s.logFile, - PreSharedKey: preSharedKey, - AdminURL: adminURL, - InterfaceName: s.config.WgIface, - WireguardPort: int64(s.config.WgPort), - DisableAutoConnect: s.config.DisableAutoConnect, - ServerSSHAllowed: *s.config.ServerSSHAllowed, - RosenpassEnabled: s.config.RosenpassEnabled, - RosenpassPermissive: s.config.RosenpassPermissive, - DisableNotifications: disableNotifications, + ManagementUrl: managementURL, + ConfigFile: s.latestConfigInput.ConfigPath, + LogFile: s.logFile, + PreSharedKey: preSharedKey, + AdminURL: adminURL, + InterfaceName: s.config.WgIface, + WireguardPort: int64(s.config.WgPort), + DisableAutoConnect: s.config.DisableAutoConnect, + ServerSSHAllowed: *s.config.ServerSSHAllowed, + RosenpassEnabled: s.config.RosenpassEnabled, + RosenpassPermissive: s.config.RosenpassPermissive, + LazyConnectionEnabled: s.config.LazyConnectionEnabled, + BlockInbound: s.config.BlockInbound, + DisableNotifications: disableNotifications, }, nil } diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 92289a8a3..f0202b8e7 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -194,6 +194,7 @@ type serviceClient struct { mAutoConnect *systray.MenuItem mEnableRosenpass *systray.MenuItem mLazyConnEnabled *systray.MenuItem + mBlockInbound *systray.MenuItem mNotifications *systray.MenuItem mAdvancedSettings *systray.MenuItem mCreateDebugBundle *systray.MenuItem @@ -635,7 +636,8 @@ func (s *serviceClient) onTrayReady() { s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false) - s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable lazy connection", lazyConnMenuDescr, false) + s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable Lazy Connections", lazyConnMenuDescr, false) + s.mBlockInbound = s.mSettings.AddSubMenuItemCheckbox("Block Inbound Connections", blockInboundMenuDescr, false) s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", notificationsMenuDescr, false) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", advancedSettingsMenuDescr) s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr) @@ -757,6 +759,15 @@ func (s *serviceClient) listenEvents() { if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) } + case <-s.mBlockInbound.ClickedCh: + if s.mBlockInbound.Checked() { + s.mBlockInbound.Uncheck() + } else { + s.mBlockInbound.Check() + } + if err := s.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } case <-s.mAdvancedSettings.ClickedCh: s.mAdvancedSettings.Disable() go func() { @@ -1017,6 +1028,18 @@ func (s *serviceClient) loadSettings() { s.mEnableRosenpass.Uncheck() } + if cfg.LazyConnectionEnabled { + s.mLazyConnEnabled.Check() + } else { + s.mLazyConnEnabled.Uncheck() + } + + if cfg.BlockInbound { + s.mBlockInbound.Check() + } else { + s.mBlockInbound.Uncheck() + } + if cfg.DisableNotifications { s.mNotifications.Uncheck() } else { @@ -1033,8 +1056,9 @@ func (s *serviceClient) updateConfig() error { disableAutoStart := !s.mAutoConnect.Checked() sshAllowed := s.mAllowSSH.Checked() rosenpassEnabled := s.mEnableRosenpass.Checked() - notificationsDisabled := !s.mNotifications.Checked() lazyConnectionEnabled := s.mLazyConnEnabled.Checked() + blockInbound := s.mBlockInbound.Checked() + notificationsDisabled := !s.mNotifications.Checked() loginRequest := proto.LoginRequest{ IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", @@ -1043,6 +1067,7 @@ func (s *serviceClient) updateConfig() error { DisableAutoConnect: &disableAutoStart, DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, + BlockInbound: &blockInbound, } if err := s.restartClient(&loginRequest); err != nil { diff --git a/client/ui/const.go b/client/ui/const.go index cd4e7db8e..5a4b27f32 100644 --- a/client/ui/const.go +++ b/client/ui/const.go @@ -5,7 +5,8 @@ const ( allowSSHMenuDescr = "Allow SSH connections" autoConnectMenuDescr = "Connect automatically when the service starts" quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass" - lazyConnMenuDescr = "[Experimental] Enable lazy connect" + lazyConnMenuDescr = "[Experimental] Enable lazy connections" + blockInboundMenuDescr = "Block inbound connections to the local machine and routed networks" notificationsMenuDescr = "Enable notifications" advancedSettingsMenuDescr = "Advanced settings of the application" debugBundleMenuDescr = "Create and open debug information bundle" From 06980e7fa0a30b7d6d14129c980a4f3edf37cc25 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:53:39 +0200 Subject: [PATCH 050/213] [client] Apply routes right away instead of on peer connection (#3907) --- client/internal/dns/server.go | 2 +- client/internal/engine.go | 18 +- .../routemanager/{ => client}/client.go | 343 +++++++++--------- .../routemanager/{ => client}/client_test.go | 4 +- client/internal/routemanager/manager.go | 146 ++++++-- client/internal/routemanager/manager_test.go | 5 +- .../routemanager/{ => server}/server.go | 78 ++-- client/internal/routemanager/static/route.go | 18 +- 8 files changed, 349 insertions(+), 265 deletions(-) rename client/internal/routemanager/{ => client}/client.go (52%) rename client/internal/routemanager/{ => client}/client_test.go (99%) rename client/internal/routemanager/{ => server}/server.go (63%) diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 3f49c23fd..7b845235c 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -489,7 +489,7 @@ func (s *DefaultServer) applyHostConfig() { } } - log.Debugf("extra match domains: %v", s.extraDomains) + log.Debugf("extra match domains: %v", maps.Keys(s.extraDomains)) if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil { log.Errorf("failed to apply DNS host manager update: %v", err) diff --git a/client/internal/engine.go b/client/internal/engine.go index 5efc0b92b..d015c1d6c 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -994,6 +994,15 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { } } + protoDNSConfig := networkMap.GetDNSConfig() + if protoDNSConfig == nil { + protoDNSConfig = &mgmProto.DNSConfig{} + } + + if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil { + log.Errorf("failed to update dns server, err: %v", err) + } + dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap) // apply routes first, route related actions might depend on routing being enabled @@ -1061,15 +1070,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { excludedLazyPeers := e.toExcludedLazyPeers(routes, forwardingRules, networkMap.GetRemotePeers()) e.connMgr.SetExcludeList(excludedLazyPeers) - protoDNSConfig := networkMap.GetDNSConfig() - if protoDNSConfig == nil { - protoDNSConfig = &mgmProto.DNSConfig{} - } - - if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil { - log.Errorf("failed to update dns server, err: %v", err) - } - e.networkSerial = serial // Test received (upstream) servers for availability right away instead of upon usage. diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client/client.go similarity index 52% rename from client/internal/routemanager/client.go rename to client/internal/routemanager/client/client.go index bff954c27..5582591a9 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client/client.go @@ -1,4 +1,4 @@ -package routemanager +package client import ( "context" @@ -7,10 +7,8 @@ import ( "runtime" "time" - "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" - nberrors "github.com/netbirdio/netbird/client/errors" nbdns "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peerstore" @@ -36,6 +34,7 @@ const ( reasonRouteUpdate reasonPeerUpdate reasonShutdown + reasonHA ) type routerPeerStatus struct { @@ -44,9 +43,9 @@ type routerPeerStatus struct { latency time.Duration } -type routesUpdate struct { - updateSerial uint64 - routes []*route.Route +type RoutesUpdate struct { + UpdateSerial uint64 + Routes []*route.Route } // RouteHandler defines the interface for handling routes @@ -58,64 +57,54 @@ type RouteHandler interface { RemoveAllowedIPs() error } -type clientNetwork struct { +type WatcherConfig struct { + Context context.Context + DNSRouteInterval time.Duration + WGInterface iface.WGIface + StatusRecorder *peer.Status + Route *route.Route + Handler RouteHandler +} + +// Watcher watches route and peer changes and updates allowed IPs accordingly. +// Once stopped, it cannot be reused. +type Watcher struct { ctx context.Context cancel context.CancelFunc statusRecorder *peer.Status wgInterface iface.WGIface routes map[route.ID]*route.Route - routeUpdate chan routesUpdate + routeUpdate chan RoutesUpdate peerStateUpdate chan struct{} - routePeersNotifiers map[string]chan struct{} + routePeersNotifiers map[string]chan struct{} // map of peer key to channel for peer state changes currentChosen *route.Route handler RouteHandler updateSerial uint64 } -func newClientNetworkWatcher( - ctx context.Context, - dnsRouteInterval time.Duration, - wgInterface iface.WGIface, - statusRecorder *peer.Status, - rt *route.Route, - routeRefCounter *refcounter.RouteRefCounter, - allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, - dnsServer nbdns.Server, - peerStore *peerstore.Store, - useNewDNSRoute bool, -) *clientNetwork { - ctx, cancel := context.WithCancel(ctx) +func NewWatcher(config WatcherConfig) *Watcher { + ctx, cancel := context.WithCancel(config.Context) - client := &clientNetwork{ + client := &Watcher{ ctx: ctx, cancel: cancel, - statusRecorder: statusRecorder, - wgInterface: wgInterface, + statusRecorder: config.StatusRecorder, + wgInterface: config.WGInterface, routes: make(map[route.ID]*route.Route), routePeersNotifiers: make(map[string]chan struct{}), - routeUpdate: make(chan routesUpdate), + routeUpdate: make(chan RoutesUpdate), peerStateUpdate: make(chan struct{}), - handler: handlerFromRoute( - rt, - routeRefCounter, - allowedIPsRefCounter, - dnsRouteInterval, - statusRecorder, - wgInterface, - dnsServer, - peerStore, - useNewDNSRoute, - ), + handler: config.Handler, } return client } -func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus { +func (w *Watcher) getRouterPeerStatuses() map[route.ID]routerPeerStatus { routePeerStatuses := make(map[route.ID]routerPeerStatus) - for _, r := range c.routes { - peerStatus, err := c.statusRecorder.GetPeer(r.Peer) + for _, r := range w.routes { + peerStatus, err := w.statusRecorder.GetPeer(r.Peer) if err != nil { - log.Debugf("couldn't fetch peer state: %v", err) + log.Debugf("couldn't fetch peer state %v: %v", r.Peer, err) continue } routePeerStatuses[r.ID] = routerPeerStatus{ @@ -128,7 +117,7 @@ func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus { } // getBestRouteFromStatuses determines the most optimal route from the available routes -// within a clientNetwork, taking into account peer connection status, route metrics, and +// within a Watcher, taking into account peer connection status, route metrics, and // preference for non-relayed and direct connections. // // It follows these prioritization rules: @@ -140,17 +129,17 @@ func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus { // * Stability: In case of equal scores, the currently active route (if any) is maintained. // // It returns the ID of the selected optimal route. -func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID { - chosen := route.ID("") +func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID { + var chosen route.ID chosenScore := float64(0) currScore := float64(0) - currID := route.ID("") - if c.currentChosen != nil { - currID = c.currentChosen.ID + var currID route.ID + if w.currentChosen != nil { + currID = w.currentChosen.ID } - for _, r := range c.routes { + for _, r := range w.routes { tempScore := float64(0) peerStatus, found := routePeerStatuses[r.ID] if !found || !peerStatus.connected { @@ -167,7 +156,7 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID] if peerStatus.latency != 0 { latency = peerStatus.latency } else { - log.Tracef("peer %s has 0 latency, range %s", r.Peer, c.handler) + log.Tracef("peer %s has 0 latency, range %s", r.Peer, w.handler) } // avoid negative tempScore on the higher latency calculation @@ -197,35 +186,45 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID] } } - log.Debugf("chosen route: %s, chosen score: %f, current route: %s, current score: %f", chosen, chosenScore, currID, currScore) + chosenID := chosen + if chosen == "" { + chosenID = "" + } + currentID := currID + if currID == "" { + currentID = "" + } + + log.Debugf("chosen route: %s, chosen score: %f, current route: %s, current score: %f", chosenID, chosenScore, currentID, currScore) switch { case chosen == "": var peers []string - for _, r := range c.routes { + for _, r := range w.routes { peers = append(peers, r.Peer) } - log.Warnf("The network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", c.handler, peers) + log.Infof("network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", w.handler, peers) case chosen != currID: // we compare the current score + 10ms to the chosen score to avoid flapping between routes if currScore != 0 && currScore+0.01 > chosenScore { - log.Debugf("Keeping current routing peer because the score difference with latency is less than 0.01(10ms), current: %f, new: %f", currScore, chosenScore) + log.Debugf("keeping current routing peer %s for [%v]: the score difference with latency is less than 0.01(10ms): current: %f, new: %f", + w.currentChosen.Peer, w.handler, currScore, chosenScore) return currID } var p string - if rt := c.routes[chosen]; rt != nil { + if rt := w.routes[chosen]; rt != nil { p = rt.Peer } - log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, c.handler) + log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, w.handler) } return chosen } -func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) { - subscription := c.statusRecorder.SubscribeToPeerStateChanges(ctx, peerKey) - defer c.statusRecorder.UnsubscribePeerStateChanges(subscription) +func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) { + subscription := w.statusRecorder.SubscribeToPeerStateChanges(ctx, peerKey) + defer w.statusRecorder.UnsubscribePeerStateChanges(subscription) for { select { @@ -240,105 +239,92 @@ func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey stri } } -func (c *clientNetwork) startPeersStatusChangeWatcher() { - for _, r := range c.routes { - _, found := c.routePeersNotifiers[r.Peer] - if found { +func (w *Watcher) startNewPeerStatusWatchers() { + for _, r := range w.routes { + if _, found := w.routePeersNotifiers[r.Peer]; found { continue } closerChan := make(chan struct{}) - c.routePeersNotifiers[r.Peer] = closerChan - go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, closerChan) + w.routePeersNotifiers[r.Peer] = closerChan + go w.watchPeerStatusChanges(w.ctx, r.Peer, w.peerStateUpdate, closerChan) } } -func (c *clientNetwork) removeRouteFromWireGuardPeer() error { - if err := c.statusRecorder.RemovePeerStateRoute(c.currentChosen.Peer, c.handler.String()); err != nil { +// addAllowedIPs adds the allowed IPs for the current chosen route to the handler. +func (w *Watcher) addAllowedIPs(route *route.Route) error { + if err := w.handler.AddAllowedIPs(route.Peer); err != nil { + return fmt.Errorf("add allowed IPs for peer %s: %w", route.Peer, err) + } + + if err := w.statusRecorder.AddPeerStateRoute(route.Peer, w.handler.String(), route.GetResourceID()); err != nil { log.Warnf("Failed to update peer state: %v", err) } - if err := c.handler.RemoveAllowedIPs(); err != nil { - return fmt.Errorf("remove allowed IPs: %w", err) - } + w.connectEvent(route) return nil } -func (c *clientNetwork) removeRouteFromPeerAndSystem(rsn reason) error { - if c.currentChosen == nil { - return nil +func (w *Watcher) removeAllowedIPs(route *route.Route, rsn reason) error { + if err := w.statusRecorder.RemovePeerStateRoute(route.Peer, w.handler.String()); err != nil { + log.Warnf("Failed to update peer state: %v", err) } - var merr *multierror.Error - - if err := c.removeRouteFromWireGuardPeer(); err != nil { - merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)) - } - if err := c.handler.RemoveRoute(); err != nil { - merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err)) + if err := w.handler.RemoveAllowedIPs(); err != nil { + return fmt.Errorf("remove allowed IPs: %w", err) } - c.disconnectEvent(rsn) + w.disconnectEvent(route, rsn) - return nberrors.FormatErrorOrNil(merr) + return nil } -func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error { - routerPeerStatuses := c.getRouterPeerStatuses() +func (w *Watcher) recalculateRoutes(rsn reason) error { + routerPeerStatuses := w.getRouterPeerStatuses() - newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses) + newChosenID := w.getBestRouteFromStatuses(routerPeerStatuses) - // If no route is chosen, remove the route from the peer and system + // If no route is chosen, remove the route from the peer if newChosenID == "" { - if err := c.removeRouteFromPeerAndSystem(rsn); err != nil { - return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err) + if w.currentChosen == nil { + return nil } - c.currentChosen = nil + if err := w.removeAllowedIPs(w.currentChosen, rsn); err != nil { + return fmt.Errorf("remove obsolete: %w", err) + } + + w.currentChosen = nil return nil } // If the chosen route is the same as the current route, do nothing - if c.currentChosen != nil && c.currentChosen.ID == newChosenID && - c.currentChosen.Equal(c.routes[newChosenID]) { + if w.currentChosen != nil && w.currentChosen.ID == newChosenID && + w.currentChosen.Equal(w.routes[newChosenID]) { return nil } - var isNew bool - if c.currentChosen == nil { - // If they were not previously assigned to another peer, add routes to the system first - if err := c.handler.AddRoute(c.ctx); err != nil { - return fmt.Errorf("add route: %w", err) - } - isNew = true - } else { - // Otherwise, remove the allowed IPs from the previous peer first - if err := c.removeRouteFromWireGuardPeer(); err != nil { - return fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err) + // If the chosen route was assigned to a different peer, remove the allowed IPs first + if isNew := w.currentChosen == nil; !isNew { + if err := w.removeAllowedIPs(w.currentChosen, reasonHA); err != nil { + return fmt.Errorf("remove old: %w", err) } } - c.currentChosen = c.routes[newChosenID] - - if err := c.handler.AddAllowedIPs(c.currentChosen.Peer); err != nil { - return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err) + newChosenRoute := w.routes[newChosenID] + if err := w.addAllowedIPs(newChosenRoute); err != nil { + return fmt.Errorf("add new: %w", err) } - if isNew { - c.connectEvent() - } + w.currentChosen = newChosenRoute - err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String(), c.currentChosen.GetResourceID()) - if err != nil { - return fmt.Errorf("add peer state route: %w", err) - } return nil } -func (c *clientNetwork) connectEvent() { +func (w *Watcher) connectEvent(route *route.Route) { var defaultRoute bool - for _, r := range c.routes { + for _, r := range w.routes { if r.Network.Bits() == 0 { defaultRoute = true break @@ -350,13 +336,13 @@ func (c *clientNetwork) connectEvent() { } meta := map[string]string{ - "network": c.handler.String(), + "network": w.handler.String(), } - if c.currentChosen != nil { - meta["id"] = string(c.currentChosen.NetID) - meta["peer"] = c.currentChosen.Peer + if route != nil { + meta["id"] = string(route.NetID) + meta["peer"] = route.Peer } - c.statusRecorder.PublishEvent( + w.statusRecorder.PublishEvent( proto.SystemEvent_INFO, proto.SystemEvent_NETWORK, "Default route added", @@ -365,9 +351,9 @@ func (c *clientNetwork) connectEvent() { ) } -func (c *clientNetwork) disconnectEvent(rsn reason) { +func (w *Watcher) disconnectEvent(route *route.Route, rsn reason) { var defaultRoute bool - for _, r := range c.routes { + for _, r := range w.routes { if r.Network.Bits() == 0 { defaultRoute = true break @@ -383,11 +369,11 @@ func (c *clientNetwork) disconnectEvent(rsn reason) { var userMessage string meta := make(map[string]string) - if c.currentChosen != nil { - meta["id"] = string(c.currentChosen.NetID) - meta["peer"] = c.currentChosen.Peer + if route != nil { + meta["id"] = string(route.NetID) + meta["peer"] = route.Peer } - meta["network"] = c.handler.String() + meta["network"] = w.handler.String() switch rsn { case reasonShutdown: severity = proto.SystemEvent_INFO @@ -400,13 +386,17 @@ func (c *clientNetwork) disconnectEvent(rsn reason) { severity = proto.SystemEvent_WARNING message = "Default route disconnected due to peer unreachability" userMessage = "Exit node connection lost. Your internet access might be affected." + case reasonHA: + severity = proto.SystemEvent_INFO + message = "Default route disconnected due to high availability change" + userMessage = "Exit node disconnected due to high availability change." default: severity = proto.SystemEvent_ERROR message = "Default route disconnected for unknown reasons" userMessage = "Exit node disconnected for unknown reasons." } - c.statusRecorder.PublishEvent( + w.statusRecorder.PublishEvent( severity, proto.SystemEvent_NETWORK, message, @@ -415,86 +405,101 @@ func (c *clientNetwork) disconnectEvent(rsn reason) { ) } -func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) { +func (w *Watcher) SendUpdate(update RoutesUpdate) { go func() { - c.routeUpdate <- update + select { + case w.routeUpdate <- update: + case <-w.ctx.Done(): + } }() } -func (c *clientNetwork) handleUpdate(update routesUpdate) bool { +func (w *Watcher) classifyUpdate(update RoutesUpdate) bool { isUpdateMapDifferent := false updateMap := make(map[route.ID]*route.Route) - for _, r := range update.routes { + for _, r := range update.Routes { updateMap[r.ID] = r } - if len(c.routes) != len(updateMap) { + if len(w.routes) != len(updateMap) { isUpdateMapDifferent = true } - for id, r := range c.routes { + for id, r := range w.routes { _, found := updateMap[id] if !found { - close(c.routePeersNotifiers[r.Peer]) - delete(c.routePeersNotifiers, r.Peer) + close(w.routePeersNotifiers[r.Peer]) + delete(w.routePeersNotifiers, r.Peer) isUpdateMapDifferent = true continue } - if !reflect.DeepEqual(c.routes[id], updateMap[id]) { + if !reflect.DeepEqual(w.routes[id], updateMap[id]) { isUpdateMapDifferent = true } } - c.routes = updateMap + w.routes = updateMap return isUpdateMapDifferent } -// peersStateAndUpdateWatcher is the main point of reacting on client network routing events. +// Start is the main point of reacting on client network routing events. // All the processing related to the client network should be done here. Thread-safe. -func (c *clientNetwork) peersStateAndUpdateWatcher() { +func (w *Watcher) Start() { for { select { - case <-c.ctx.Done(): - log.Debugf("Stopping watcher for network [%v]", c.handler) - if err := c.removeRouteFromPeerAndSystem(reasonShutdown); err != nil { - log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err) - } + case <-w.ctx.Done(): return - case <-c.peerStateUpdate: - err := c.recalculateRouteAndUpdatePeerAndSystem(reasonPeerUpdate) - if err != nil { - log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err) + case <-w.peerStateUpdate: + if err := w.recalculateRoutes(reasonPeerUpdate); err != nil { + log.Errorf("Failed to recalculate routes for network [%v]: %v", w.handler, err) } - case update := <-c.routeUpdate: - if update.updateSerial < c.updateSerial { - log.Warnf("Received a routes update with smaller serial number (%d -> %d), ignoring it", c.updateSerial, update.updateSerial) + case update := <-w.routeUpdate: + if update.UpdateSerial < w.updateSerial { + log.Warnf("Received a routes update with smaller serial number (%d -> %d), ignoring it", w.updateSerial, update.UpdateSerial) continue } - log.Debugf("Received a new client network route update for [%v]", c.handler) - - // hash update somehow - isTrueRouteUpdate := c.handleUpdate(update) - - c.updateSerial = update.updateSerial - - if isTrueRouteUpdate { - log.Debug("Client network update contains different routes, recalculating routes") - err := c.recalculateRouteAndUpdatePeerAndSystem(reasonRouteUpdate) - if err != nil { - log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err) - } - } else { - log.Debug("Route update is not different, skipping route recalculation") - } - - c.startPeersStatusChangeWatcher() + w.handleRouteUpdate(update) } } } -func handlerFromRoute( +func (w *Watcher) handleRouteUpdate(update RoutesUpdate) { + log.Debugf("Received a new client network route update for [%v]", w.handler) + + // hash update somehow + isTrueRouteUpdate := w.classifyUpdate(update) + + w.updateSerial = update.UpdateSerial + + if isTrueRouteUpdate { + log.Debugf("client network update %v for [%v] contains different routes, recalculating routes", update.UpdateSerial, w.handler) + if err := w.recalculateRoutes(reasonRouteUpdate); err != nil { + log.Errorf("failed to recalculate routes for network [%v]: %v", w.handler, err) + } + } else { + log.Debugf("route update %v for [%v] is not different, skipping route recalculation", update.UpdateSerial, w.handler) + } + + w.startNewPeerStatusWatchers() +} + +// Stop stops the watcher and cleans up resources. +func (w *Watcher) Stop() { + log.Debugf("Stopping watcher for network [%v]", w.handler) + + w.cancel() + + if w.currentChosen == nil { + return + } + if err := w.removeAllowedIPs(w.currentChosen, reasonShutdown); err != nil { + log.Errorf("Failed to remove routes for [%v]: %v", w.handler, err) + } +} + +func HandlerFromRoute( rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, diff --git a/client/internal/routemanager/client_test.go b/client/internal/routemanager/client/client_test.go similarity index 99% rename from client/internal/routemanager/client_test.go rename to client/internal/routemanager/client/client_test.go index 56fcf1613..48a9495bf 100644 --- a/client/internal/routemanager/client_test.go +++ b/client/internal/routemanager/client/client_test.go @@ -1,4 +1,4 @@ -package routemanager +package client import ( "fmt" @@ -395,7 +395,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) { } // create new clientNetwork - client := &clientNetwork{ + client := &Watcher{ handler: static.NewRoute(&route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, nil, nil), routes: tc.existingRoutes, currentChosen: currentRoute, diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 078206ab9..afb74c23e 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -11,9 +11,11 @@ import ( "sync" "time" + "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" + nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/netstack" @@ -21,9 +23,11 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/client/internal/routemanager/client" "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/client/internal/routemanager/server" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/routemanager/vars" "github.com/netbirdio/netbird/client/internal/routeselector" @@ -68,9 +72,9 @@ type DefaultManager struct { ctx context.Context stop context.CancelFunc mux sync.Mutex - clientNetworks map[route.HAUniqueID]*clientNetwork + clientNetworks map[route.HAUniqueID]*client.Watcher routeSelector *routeselector.RouteSelector - serverRouter *serverRouter + serverRouter *server.Router sysOps *systemops.SysOps statusRecorder *peer.Status relayMgr *relayClient.Manager @@ -88,6 +92,7 @@ type DefaultManager struct { useNewDNSRoute bool disableClientRoutes bool disableServerRoutes bool + activeRoutes map[route.HAUniqueID]client.RouteHandler } func NewManager(config ManagerConfig) *DefaultManager { @@ -99,7 +104,7 @@ func NewManager(config ManagerConfig) *DefaultManager { ctx: mCTX, stop: cancel, dnsRouteInterval: config.DNSRouteInterval, - clientNetworks: make(map[route.HAUniqueID]*clientNetwork), + clientNetworks: make(map[route.HAUniqueID]*client.Watcher), relayMgr: config.RelayManager, sysOps: sysOps, statusRecorder: config.StatusRecorder, @@ -111,6 +116,7 @@ func NewManager(config ManagerConfig) *DefaultManager { peerStore: config.PeerStore, disableClientRoutes: config.DisableClientRoutes, disableServerRoutes: config.DisableServerRoutes, + activeRoutes: make(map[route.HAUniqueID]client.RouteHandler), } useNoop := netstack.IsEnabled() || config.DisableClientRoutes @@ -226,7 +232,7 @@ func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error { } var err error - m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder) + m.serverRouter, err = server.NewRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder) if err != nil { return err } @@ -237,7 +243,7 @@ func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error { func (m *DefaultManager) Stop(stateManager *statemanager.Manager) { m.stop() if m.serverRouter != nil { - m.serverRouter.cleanUp() + m.serverRouter.CleanUp() } if m.routeRefCounter != nil { @@ -265,6 +271,54 @@ func (m *DefaultManager) Stop(stateManager *statemanager.Manager) { } // UpdateRoutes compares received routes with existing routes and removes, updates or adds them to the client and server maps +func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error { + toAdd := make(map[route.HAUniqueID]*route.Route) + toRemove := make(map[route.HAUniqueID]client.RouteHandler) + + for id, routes := range newRoutes { + if len(routes) > 0 { + toAdd[id] = routes[0] + } + } + + for id, activeHandler := range m.activeRoutes { + if _, exists := toAdd[id]; exists { + delete(toAdd, id) + } else { + toRemove[id] = activeHandler + } + } + + var merr *multierror.Error + for id, handler := range toRemove { + if err := handler.RemoveRoute(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove route %s: %w", handler.String(), err)) + } + delete(m.activeRoutes, id) + } + + for id, route := range toAdd { + handler := client.HandlerFromRoute( + route, + m.routeRefCounter, + m.allowedIPsRefCounter, + m.dnsRouteInterval, + m.statusRecorder, + m.wgInterface, + m.dnsServer, + m.peerStore, + m.useNewDNSRoute, + ) + if err := handler.AddRoute(m.ctx); err != nil { + merr = multierror.Append(merr, fmt.Errorf("add route %s: %w", handler.String(), err)) + continue + } + m.activeRoutes[id] = handler + } + + return nberrors.FormatErrorOrNil(merr) +} + func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, useNewDNSRoute bool) error { select { case <-m.ctx.Done(): @@ -281,6 +335,11 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro if !m.disableClientRoutes { filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap) + + if err := m.updateSystemRoutes(filteredClientRoutes); err != nil { + log.Errorf("Failed to update system routes: %v", err) + } + m.updateClientNetworks(updateSerial, filteredClientRoutes) m.notifier.OnNewRoutes(filteredClientRoutes) } @@ -290,7 +349,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro return nil } - if err := m.serverRouter.updateRoutes(newServerRoutesMap, useNewDNSRoute); err != nil { + if err := m.serverRouter.UpdateRoutes(newServerRoutesMap, useNewDNSRoute); err != nil { return fmt.Errorf("update routes: %w", err) } @@ -341,6 +400,10 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) { m.notifier.OnNewRoutes(networks) + if err := m.updateSystemRoutes(networks); err != nil { + log.Errorf("failed to update system routes during selection: %v", err) + } + m.stopObsoleteClients(networks) for id, routes := range networks { @@ -349,21 +412,24 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) { continue } - clientNetworkWatcher := newClientNetworkWatcher( - m.ctx, - m.dnsRouteInterval, - m.wgInterface, - m.statusRecorder, - routes[0], - m.routeRefCounter, - m.allowedIPsRefCounter, - m.dnsServer, - m.peerStore, - m.useNewDNSRoute, - ) + handler := m.activeRoutes[id] + if handler == nil { + log.Warnf("no active handler found for route %s", id) + continue + } + + config := client.WatcherConfig{ + Context: m.ctx, + DNSRouteInterval: m.dnsRouteInterval, + WGInterface: m.wgInterface, + StatusRecorder: m.statusRecorder, + Route: routes[0], + Handler: handler, + } + clientNetworkWatcher := client.NewWatcher(config) m.clientNetworks[id] = clientNetworkWatcher - go clientNetworkWatcher.peersStateAndUpdateWatcher() - clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes}) + go clientNetworkWatcher.Start() + clientNetworkWatcher.SendUpdate(client.RoutesUpdate{Routes: routes}) } if err := m.stateManager.UpdateState((*SelectorState)(m.routeSelector)); err != nil { @@ -375,8 +441,7 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) { func (m *DefaultManager) stopObsoleteClients(networks route.HAMap) { for id, client := range m.clientNetworks { if _, ok := networks[id]; !ok { - log.Debugf("Stopping client network watcher, %s", id) - client.cancel() + client.Stop() delete(m.clientNetworks, id) } } @@ -389,26 +454,29 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks rout for id, routes := range networks { clientNetworkWatcher, found := m.clientNetworks[id] if !found { - clientNetworkWatcher = newClientNetworkWatcher( - m.ctx, - m.dnsRouteInterval, - m.wgInterface, - m.statusRecorder, - routes[0], - m.routeRefCounter, - m.allowedIPsRefCounter, - m.dnsServer, - m.peerStore, - m.useNewDNSRoute, - ) + handler := m.activeRoutes[id] + if handler == nil { + log.Errorf("No active handler found for route %s", id) + continue + } + + config := client.WatcherConfig{ + Context: m.ctx, + DNSRouteInterval: m.dnsRouteInterval, + WGInterface: m.wgInterface, + StatusRecorder: m.statusRecorder, + Route: routes[0], + Handler: handler, + } + clientNetworkWatcher = client.NewWatcher(config) m.clientNetworks[id] = clientNetworkWatcher - go clientNetworkWatcher.peersStateAndUpdateWatcher() + go clientNetworkWatcher.Start() } - update := routesUpdate{ - updateSerial: updateSerial, - routes: routes, + update := client.RoutesUpdate{ + UpdateSerial: updateSerial, + Routes: routes, } - clientNetworkWatcher.sendUpdateToClientNetworkWatcher(update) + clientNetworkWatcher.SendUpdate(update) } } diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 318ef5ae5..680bd813f 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/netip" - "runtime" "testing" "github.com/pion/transport/v3/stdnet" @@ -454,8 +453,8 @@ func TestManagerUpdateRoutes(t *testing.T) { } require.Len(t, routeManager.clientNetworks, expectedWatchers, "client networks size should match") - if runtime.GOOS == "linux" && routeManager.serverRouter != nil { - require.Len(t, routeManager.serverRouter.routes, testCase.serverRoutesExpected, "server networks size should match") + if routeManager.serverRouter != nil { + require.Equal(t, testCase.serverRoutesExpected, routeManager.serverRouter.RoutesCount(), "server networks size should match") } }) } diff --git a/client/internal/routemanager/server.go b/client/internal/routemanager/server/server.go similarity index 63% rename from client/internal/routemanager/server.go rename to client/internal/routemanager/server/server.go index 5bacb856c..e674c80cd 100644 --- a/client/internal/routemanager/server.go +++ b/client/internal/routemanager/server/server.go @@ -1,4 +1,4 @@ -package routemanager +package server import ( "context" @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/route" ) -type serverRouter struct { +type Router struct { mux sync.Mutex ctx context.Context routes map[route.ID]*route.Route @@ -23,8 +23,8 @@ type serverRouter struct { statusRecorder *peer.Status } -func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) { - return &serverRouter{ +func NewRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*Router, error) { + return &Router{ ctx: ctx, routes: make(map[route.ID]*route.Route), firewall: firewall, @@ -33,104 +33,110 @@ func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall fi }, nil } -func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route, useNewDNSRoute bool) error { - m.mux.Lock() - defer m.mux.Unlock() +func (r *Router) UpdateRoutes(routesMap map[route.ID]*route.Route, useNewDNSRoute bool) error { + r.mux.Lock() + defer r.mux.Unlock() serverRoutesToRemove := make([]route.ID, 0) - for routeID := range m.routes { + for routeID := range r.routes { update, found := routesMap[routeID] - if !found || !update.Equal(m.routes[routeID]) { + if !found || !update.Equal(r.routes[routeID]) { serverRoutesToRemove = append(serverRoutesToRemove, routeID) } } for _, routeID := range serverRoutesToRemove { - oldRoute := m.routes[routeID] - err := m.removeFromServerNetwork(oldRoute) + oldRoute := r.routes[routeID] + err := r.removeFromServerNetwork(oldRoute) if err != nil { log.Errorf("Unable to remove route id: %s, network %s, from server, got: %v", oldRoute.ID, oldRoute.Network, err) } - delete(m.routes, routeID) + delete(r.routes, routeID) } // If routing is to be disabled, do it after routes have been removed // If routing is to be enabled, do it before adding new routes; addToServerNetwork needs routing to be enabled if len(routesMap) > 0 { - if err := m.firewall.EnableRouting(); err != nil { + if err := r.firewall.EnableRouting(); err != nil { return fmt.Errorf("enable routing: %w", err) } } else { - if err := m.firewall.DisableRouting(); err != nil { + if err := r.firewall.DisableRouting(); err != nil { return fmt.Errorf("disable routing: %w", err) } } for id, newRoute := range routesMap { - _, found := m.routes[id] + _, found := r.routes[id] if found { continue } - err := m.addToServerNetwork(newRoute, useNewDNSRoute) + err := r.addToServerNetwork(newRoute, useNewDNSRoute) if err != nil { log.Errorf("Unable to add route %s from server, got: %v", newRoute.ID, err) continue } - m.routes[id] = newRoute + r.routes[id] = newRoute } return nil } -func (m *serverRouter) removeFromServerNetwork(route *route.Route) error { - if m.ctx.Err() != nil { +func (r *Router) removeFromServerNetwork(route *route.Route) error { + if r.ctx.Err() != nil { log.Infof("Not removing from server network because context is done") - return m.ctx.Err() + return r.ctx.Err() } routerPair := routeToRouterPair(route, false) - if err := m.firewall.RemoveNatRule(routerPair); err != nil { + if err := r.firewall.RemoveNatRule(routerPair); err != nil { return fmt.Errorf("remove routing rules: %w", err) } - delete(m.routes, route.ID) - m.statusRecorder.RemoveLocalPeerStateRoute(route.NetString()) + delete(r.routes, route.ID) + r.statusRecorder.RemoveLocalPeerStateRoute(route.NetString()) return nil } -func (m *serverRouter) addToServerNetwork(route *route.Route, useNewDNSRoute bool) error { - if m.ctx.Err() != nil { +func (r *Router) addToServerNetwork(route *route.Route, useNewDNSRoute bool) error { + if r.ctx.Err() != nil { log.Infof("Not adding to server network because context is done") - return m.ctx.Err() + return r.ctx.Err() } routerPair := routeToRouterPair(route, useNewDNSRoute) - if err := m.firewall.AddNatRule(routerPair); err != nil { + if err := r.firewall.AddNatRule(routerPair); err != nil { return fmt.Errorf("insert routing rules: %w", err) } - m.routes[route.ID] = route - m.statusRecorder.AddLocalPeerStateRoute(route.NetString(), route.GetResourceID()) + r.routes[route.ID] = route + r.statusRecorder.AddLocalPeerStateRoute(route.NetString(), route.GetResourceID()) return nil } -func (m *serverRouter) cleanUp() { - m.mux.Lock() - defer m.mux.Unlock() +func (r *Router) CleanUp() { + r.mux.Lock() + defer r.mux.Unlock() - for _, r := range m.routes { - routerPair := routeToRouterPair(r, false) - if err := m.firewall.RemoveNatRule(routerPair); err != nil { + for _, route := range r.routes { + routerPair := routeToRouterPair(route, false) + if err := r.firewall.RemoveNatRule(routerPair); err != nil { log.Errorf("Failed to remove cleanup route: %v", err) } } - m.statusRecorder.CleanLocalPeerStateRoutes() + r.statusRecorder.CleanLocalPeerStateRoutes() +} + +func (r *Router) RoutesCount() int { + r.mux.Lock() + defer r.mux.Unlock() + return len(r.routes) } func routeToRouterPair(route *route.Route, useNewDNSRoute bool) firewall.RouterPair { diff --git a/client/internal/routemanager/static/route.go b/client/internal/routemanager/static/route.go index 681c192fb..c8b9338e0 100644 --- a/client/internal/routemanager/static/route.go +++ b/client/internal/routemanager/static/route.go @@ -29,13 +29,17 @@ func (r *Route) String() string { } func (r *Route) AddRoute(context.Context) error { - _, err := r.routeRefCounter.Increment(r.route.Network, struct{}{}) - return err + if _, err := r.routeRefCounter.Increment(r.route.Network, struct{}{}); err != nil { + return err + } + return nil } func (r *Route) RemoveRoute() error { - _, err := r.routeRefCounter.Decrement(r.route.Network) - return err + if _, err := r.routeRefCounter.Decrement(r.route.Network); err != nil { + return err + } + return nil } func (r *Route) AddAllowedIPs(peerKey string) error { @@ -51,6 +55,8 @@ func (r *Route) AddAllowedIPs(peerKey string) error { } func (r *Route) RemoveAllowedIPs() error { - _, err := r.allowedIPsRefcounter.Decrement(r.route.Network) - return err + if _, err := r.allowedIPsRefcounter.Decrement(r.route.Network); err != nil { + return err + } + return nil } From 0cd36baf67daf068c6e9194c96a4835a56f68a08 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:09:39 +0200 Subject: [PATCH 051/213] [client] Allow the netbird service to log to console (#3916) --- client/cmd/service.go | 2 +- client/cmd/service_installer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/cmd/service.go b/client/cmd/service.go index 3560088a7..005479306 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -30,7 +30,7 @@ func newSVCConfig() *service.Config { return &service.Config{ Name: serviceName, DisplayName: "Netbird", - Description: "A WireGuard-based mesh network that connects your devices into a single private network.", + Description: "Netbird mesh network client", Option: make(service.KeyValue), } } diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index 99a4821b0..c1d6308c6 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -39,7 +39,7 @@ var installCmd = &cobra.Command{ svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL) } - if logFile != "console" { + if logFile != "" { svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile) } From 87148c503f8c236c39492d05bba7681473811d43 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:21:31 +0100 Subject: [PATCH 052/213] [management] support account retrieval and creation by private domain (#3825) * [management] sys initiator save user (#3911) * [management] activity events with multiple external account users (#3914) --- management/server/account.go | 60 ++++++++++++------- management/server/account/manager.go | 2 +- management/server/account_test.go | 40 ++++++++++--- management/server/event.go | 43 ++++++------- management/server/mock_server/account_mock.go | 11 ++-- management/server/user.go | 38 ++++++++---- 6 files changed, 121 insertions(+), 73 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 033ec5fa1..63879802a 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1730,23 +1730,26 @@ func (am *DefaultAccountManager) GetStore() store.Store { return am.Store } -// Creates account by private domain. -// Expects domain value to be a valid and a private dns domain. -func (am *DefaultAccountManager) CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error) { +func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) { cancel := am.Store.AcquireGlobalLock(ctx) defer cancel() - domain = strings.ToLower(domain) - - count, err := am.Store.CountAccountsByPrivateDomain(ctx, domain) - if err != nil { - return nil, err + existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain) + if handleNotFound(err) != nil { + return nil, false, err } - if count > 0 { - return nil, status.Errorf(status.InvalidArgument, "account with private domain already exists") + // a primary account already exists for this private domain + if err == nil { + existingAccount, err := am.Store.GetAccount(ctx, existingPrimaryAccountID) + if err != nil { + return nil, false, err + } + + return existingAccount, false, nil } + // create a new account for this private domain // retry twice for new ID clashes for range 2 { accountId := xid.New().String() @@ -1776,7 +1779,7 @@ func (am *DefaultAccountManager) CreateAccountByPrivateDomain(ctx context.Contex Users: users, // @todo check if using the MSP owner id here is ok CreatedBy: initiatorId, - Domain: domain, + Domain: strings.ToLower(domain), DomainCategory: types.PrivateCategory, IsDomainPrimaryAccount: false, Routes: routes, @@ -1795,19 +1798,22 @@ func (am *DefaultAccountManager) CreateAccountByPrivateDomain(ctx context.Contex } if err := newAccount.AddAllGroup(); err != nil { - return nil, status.Errorf(status.Internal, "failed to add all group to new account by private domain") + return nil, false, status.Errorf(status.Internal, "failed to add all group to new account by private domain") } if err := am.Store.SaveAccount(ctx, newAccount); err != nil { - log.WithContext(ctx).Errorf("failed to save new account %s by private domain: %v", newAccount.Id, err) - return nil, err + log.WithContext(ctx).WithFields(log.Fields{ + "accountId": newAccount.Id, + "domain": domain, + }).Errorf("failed to create new account: %v", err) + return nil, false, err } am.StoreEvent(ctx, initiatorId, newAccount.Id, accountId, activity.AccountCreated, nil) - return newAccount, nil + return newAccount, true, nil } - return nil, status.Errorf(status.Internal, "failed to create new account by private domain") + return nil, false, status.Errorf(status.Internal, "failed to get or create new account by private domain") } func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) { @@ -1820,21 +1826,29 @@ func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, acc return account, nil } - // additional check to ensure there is only one account for this domain at the time of update - count, err := am.Store.CountAccountsByPrivateDomain(ctx, account.Domain) - if err != nil { + existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, account.Domain) + + // error is not a not found error + if handleNotFound(err) != nil { return nil, err } - if count > 1 { - return nil, status.Errorf(status.Internal, "more than one account exists with the same private domain") + // a primary account already exists for this private domain + if err == nil { + log.WithContext(ctx).WithFields(log.Fields{ + "accountId": accountId, + "existingAccountId": existingPrimaryAccountID, + }).Errorf("cannot update account to primary, another account already exists as primary for the same domain") + return nil, status.Errorf(status.Internal, "cannot update account to primary") } account.IsDomainPrimaryAccount = true if err := am.Store.SaveAccount(ctx, account); err != nil { - log.WithContext(ctx).Errorf("failed to update primary account %s by private domain: %v", account.Id, err) - return nil, status.Errorf(status.Internal, "failed to update primary account %s by private domain", account.Id) + log.WithContext(ctx).WithFields(log.Fields{ + "accountId": accountId, + }).Errorf("failed to update account to primary: %v", err) + return nil, status.Errorf(status.Internal, "failed to update account to primary") } return account, nil diff --git a/management/server/account/manager.go b/management/server/account/manager.go index 9bc4f9605..030bd94ef 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -113,7 +113,7 @@ type Manager interface { BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error GetStore() store.Store - CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error) + GetOrCreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) GetOwnerInfo(ctx context.Context, accountId string) (*types.UserInfo, error) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) diff --git a/management/server/account_test.go b/management/server/account_test.go index c5583d226..5ada28ca3 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -14,7 +14,6 @@ import ( "time" "github.com/golang/mock/gomock" - "github.com/netbirdio/netbird/management/server/idp" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,6 +24,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/cache" nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" @@ -3198,7 +3198,7 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { } } -func Test_CreateAccountByPrivateDomain(t *testing.T) { +func Test_GetCreateAccountByPrivateDomain(t *testing.T) { manager, err := createManager(t) if err != nil { t.Fatal(err) @@ -3209,9 +3209,10 @@ func Test_CreateAccountByPrivateDomain(t *testing.T) { initiatorId := "test-user" domain := "example.com" - account, err := manager.CreateAccountByPrivateDomain(ctx, initiatorId, domain) + account, created, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain) assert.NoError(t, err) + assert.True(t, created) assert.False(t, account.IsDomainPrimaryAccount) assert.Equal(t, domain, account.Domain) assert.Equal(t, types.PrivateCategory, account.DomainCategory) @@ -3220,9 +3221,25 @@ func Test_CreateAccountByPrivateDomain(t *testing.T) { assert.Equal(t, 0, len(account.Users)) assert.Equal(t, 0, len(account.SetupKeys)) - // retry should fail - _, err = manager.CreateAccountByPrivateDomain(ctx, initiatorId, domain) - assert.Error(t, err) + // should return a new account because the previous one is not primary + account2, created2, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain) + assert.NoError(t, err) + + assert.True(t, created2) + assert.False(t, account2.IsDomainPrimaryAccount) + assert.Equal(t, domain, account2.Domain) + assert.Equal(t, types.PrivateCategory, account2.DomainCategory) + assert.Equal(t, initiatorId, account2.CreatedBy) + assert.Equal(t, 1, len(account2.Groups)) + assert.Equal(t, 0, len(account2.Users)) + assert.Equal(t, 0, len(account2.SetupKeys)) + + account, err = manager.UpdateToPrimaryAccount(ctx, account.Id) + assert.NoError(t, err) + assert.True(t, account.IsDomainPrimaryAccount) + + _, err = manager.UpdateToPrimaryAccount(ctx, account2.Id) + assert.Error(t, err, "should not be able to update a second account to primary") } func Test_UpdateToPrimaryAccount(t *testing.T) { @@ -3236,14 +3253,21 @@ func Test_UpdateToPrimaryAccount(t *testing.T) { initiatorId := "test-user" domain := "example.com" - account, err := manager.CreateAccountByPrivateDomain(ctx, initiatorId, domain) + account, created, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain) assert.NoError(t, err) + assert.True(t, created) assert.False(t, account.IsDomainPrimaryAccount) + assert.Equal(t, domain, account.Domain) - // retry should fail account, err = manager.UpdateToPrimaryAccount(ctx, account.Id) assert.NoError(t, err) assert.True(t, account.IsDomainPrimaryAccount) + + account2, created2, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain) + assert.NoError(t, err) + assert.False(t, created2) + assert.True(t, account.IsDomainPrimaryAccount) + assert.Equal(t, account.Id, account2.Id) } func TestDefaultAccountManager_IsCacheCold(t *testing.T) { diff --git a/management/server/event.go b/management/server/event.go index 2952edc8c..d94714e2c 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -143,11 +143,10 @@ func (am *DefaultAccountManager) getEventsUserInfo(ctx context.Context, events [ return eventUserInfos, nil } - return am.getEventsExternalUserInfo(ctx, externalUserIds, eventUserInfos, userId) + return am.getEventsExternalUserInfo(ctx, externalUserIds, eventUserInfos) } -func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, externalUserIds []string, eventUserInfos map[string]eventUserInfo, userId string) (map[string]eventUserInfo, error) { - externalAccountId := "" +func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, externalUserIds []string, eventUserInfos map[string]eventUserInfo) (map[string]eventUserInfo, error) { fetched := make(map[string]struct{}) externalUsers := []*types.User{} for _, id := range externalUserIds { @@ -161,34 +160,30 @@ func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, continue } - if externalAccountId != "" && externalAccountId != externalUser.AccountID { - return nil, fmt.Errorf("multiple external user accounts in events") - } - - if externalAccountId == "" { - externalAccountId = externalUser.AccountID - } - fetched[id] = struct{}{} externalUsers = append(externalUsers, externalUser) } - // if we couldn't determine an account, return what we have - if externalAccountId == "" { - log.WithContext(ctx).Warnf("failed to determine external user account from users: %v", externalUserIds) - return eventUserInfos, nil + usersByExternalAccount := map[string][]*types.User{} + for _, u := range externalUsers { + if _, ok := usersByExternalAccount[u.AccountID]; !ok { + usersByExternalAccount[u.AccountID] = make([]*types.User, 0) + } + usersByExternalAccount[u.AccountID] = append(usersByExternalAccount[u.AccountID], u) } - externalUserInfos, err := am.BuildUserInfosForAccount(ctx, externalAccountId, userId, externalUsers) - if err != nil { - return nil, err - } + for externalAccountId, externalUsers := range usersByExternalAccount { + externalUserInfos, err := am.BuildUserInfosForAccount(ctx, externalAccountId, "", externalUsers) + if err != nil { + return nil, err + } - for i, k := range externalUserInfos { - eventUserInfos[i] = eventUserInfo{ - email: k.Email, - name: k.Name, - accountId: externalAccountId, + for i, k := range externalUserInfos { + eventUserInfos[i] = eventUserInfo{ + email: k.Email, + name: k.Name, + accountId: externalAccountId, + } } } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 0dd3f927e..ed47d3914 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -113,11 +113,12 @@ type MockAccountManager struct { DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) GetStoreFunc func() store.Store - CreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, error) UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error) GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error) GetCurrentUserInfoFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error) + + GetOrCreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { @@ -862,11 +863,11 @@ func (am *MockAccountManager) GetStore() store.Store { return nil } -func (am *MockAccountManager) CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error) { - if am.CreateAccountByPrivateDomainFunc != nil { - return am.CreateAccountByPrivateDomainFunc(ctx, initiatorId, domain) +func (am *MockAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) { + if am.GetOrCreateAccountByPrivateDomainFunc != nil { + return am.GetOrCreateAccountByPrivateDomainFunc(ctx, initiatorId, domain) } - return nil, status.Errorf(codes.Unimplemented, "method CreateAccountByPrivateDomain is not implemented") + return nil, false, status.Errorf(codes.Unimplemented, "method GetOrCreateAccountByPrivateDomainFunc is not implemented") } func (am *MockAccountManager) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) { diff --git a/management/server/user.go b/management/server/user.go index 5c162c50b..6d780cda3 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -531,9 +531,13 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, groupsMap[group.ID] = group } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) - if err != nil { - return nil, err + var initiatorUser *types.User + if initiatorUserID != activity.SystemInitiator { + result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + if err != nil { + return nil, err + } + initiatorUser = result } err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { @@ -543,7 +547,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, } userHadPeers, updatedUser, userPeersToExpire, userEvents, err := am.processUserUpdate( - ctx, transaction, groupsMap, accountID, initiatorUser, update, addIfNotExists, settings, + ctx, transaction, groupsMap, accountID, initiatorUserID, initiatorUser, update, addIfNotExists, settings, ) if err != nil { return fmt.Errorf("failed to process user update: %w", err) @@ -629,7 +633,7 @@ func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, ac } func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transaction store.Store, groupsMap map[string]*types.Group, - accountID string, initiatorUser, update *types.User, addIfNotExists bool, settings *types.Settings) (bool, *types.User, []*nbpeer.Peer, []func(), error) { + accountID, initiatorUserId string, initiatorUser, update *types.User, addIfNotExists bool, settings *types.Settings) (bool, *types.User, []*nbpeer.Peer, []func(), error) { if update == nil { return false, nil, nil, nil, status.Errorf(status.InvalidArgument, "provided user update is nil") @@ -653,10 +657,12 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact updatedUser.Issued = update.Issued updatedUser.IntegrationReference = update.IntegrationReference - transferredOwnerRole, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update) + var transferredOwnerRole bool + result, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update) if err != nil { return false, nil, nil, nil, err } + transferredOwnerRole = result userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthUpdate, updatedUser.AccountID, update.Id) if err != nil { @@ -682,7 +688,7 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact } updateAccountPeers := len(userPeers) > 0 - userEventsToAdd := am.prepareUserUpdateEvents(ctx, updatedUser.AccountID, initiatorUser.Id, oldUser, updatedUser, transferredOwnerRole) + userEventsToAdd := am.prepareUserUpdateEvents(ctx, updatedUser.AccountID, initiatorUserId, oldUser, updatedUser, transferredOwnerRole) return updateAccountPeers, updatedUser, peersToExpire, userEventsToAdd, nil } @@ -709,7 +715,7 @@ func getUserOrCreateIfNotExists(ctx context.Context, transaction store.Store, ac } func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initiatorUser, update *types.User) (bool, error) { - if initiatorUser.Role == types.UserRoleOwner && initiatorUser.Id != update.Id && update.Role == types.UserRoleOwner { + if initiatorUser != nil && initiatorUser.Role == types.UserRoleOwner && initiatorUser.Id != update.Id && update.Role == types.UserRoleOwner { newInitiatorUser := initiatorUser.Copy() newInitiatorUser.Role = types.UserRoleAdmin @@ -737,6 +743,10 @@ func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.Us // validateUserUpdate validates the update operation for a user. func validateUserUpdate(groupsMap map[string]*types.Group, initiatorUser, oldUser, update *types.User) error { + if initiatorUser == nil { + return nil + } + // @todo double check these if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked { return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") @@ -818,9 +828,13 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun return nil, status.NewPermissionValidationError(err) } - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) - if err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) + var user *types.User + if initiatorUserID != activity.SystemInitiator { + result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + user = result } accountUsers := []*types.User{} @@ -830,7 +844,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun if err != nil { return nil, err } - case user.AccountID == accountID: + case user != nil && user.AccountID == accountID: accountUsers = append(accountUsers, user) default: return map[string]*types.UserInfo{}, nil From ea4d13e96d79665bc0e4b489af5a0d492e104515 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:28:58 +0200 Subject: [PATCH 053/213] [client] Use platform-native routing APIs for freeBSD, macOS and Windows --- client/cmd/trace.go | 2 +- .../firewall/iptables/manager_linux_test.go | 39 +- .../firewall/nftables/manager_linux_test.go | 31 +- .../firewall/uspfilter/forwarder/forwarder.go | 12 +- client/firewall/uspfilter/localip.go | 48 +- client/firewall/uspfilter/localip_test.go | 49 +- client/firewall/uspfilter/tracer_test.go | 7 +- client/firewall/uspfilter/uspfilter.go | 6 - .../uspfilter/uspfilter_bench_test.go | 74 +- .../uspfilter/uspfilter_filter_test.go | 22 +- client/firewall/uspfilter/uspfilter_test.go | 20 +- client/iface/bind/udp_mux_universal.go | 2 +- client/iface/device/device_filter.go | 4 - client/iface/device/device_netstack.go | 6 +- client/iface/device/wg_link_freebsd.go | 10 +- client/iface/iface.go | 1 - client/iface/mocks/filter.go | 13 - client/iface/netstack/tun.go | 22 +- client/iface/wgaddr/address.go | 15 +- client/internal/acl/manager_test.go | 17 +- client/internal/dns.go | 34 +- client/internal/dns/server_test.go | 12 +- client/internal/dns/service_memory.go | 8 +- client/internal/dns/service_memory_test.go | 33 - client/internal/dns/upstream_android.go | 5 +- client/internal/dns/upstream_general.go | 6 +- client/internal/dns/upstream_ios.go | 20 +- client/internal/dns/upstream_test.go | 4 +- client/internal/engine.go | 17 +- client/internal/engine_test.go | 7 +- .../internal/netflow/conntrack/conntrack.go | 12 +- client/internal/netflow/logger/logger.go | 13 +- client/internal/netflow/logger/logger_test.go | 4 +- client/internal/netflow/manager.go | 8 +- client/internal/netflow/manager_test.go | 12 +- .../routemanager/dnsinterceptor/handler.go | 2 +- client/internal/routemanager/manager.go | 9 +- client/internal/routemanager/manager_test.go | 30 +- .../routemanager/sysctl/sysctl_linux.go | 9 +- .../routemanager/systemops/systemops.go | 30 +- .../systemops/systemops_bsd_test.go | 78 ++- .../systemops/systemops_generic.go | 121 +--- .../systemops/systemops_generic_test.go | 635 ++++++++++-------- .../routemanager/systemops/systemops_linux.go | 14 +- .../systemops/systemops_linux_test.go | 7 - .../systemops/systemops_nonlinux.go | 6 + .../routemanager/systemops/systemops_test.go | 268 ++++++++ .../routemanager/systemops/systemops_unix.go | 204 ++++-- .../systemops/systemops_windows.go | 244 +++++-- .../systemops/systemops_windows_test.go | 64 +- client/server/trace.go | 169 +++-- util/net/net.go | 19 +- util/net/net_test.go | 94 +++ 53 files changed, 1552 insertions(+), 1046 deletions(-) delete mode 100644 client/internal/dns/service_memory_test.go create mode 100644 client/internal/routemanager/systemops/systemops_test.go create mode 100644 util/net/net_test.go diff --git a/client/cmd/trace.go b/client/cmd/trace.go index b2ff1f1b5..abb73b646 100644 --- a/client/cmd/trace.go +++ b/client/cmd/trace.go @@ -17,7 +17,7 @@ var traceCmd = &cobra.Command{ Example: ` netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53 - netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0 + netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --icmp-type 8 --icmp-code 0 netbird debug trace in 100.64.1.1 self -p tcp --dport 80`, Args: cobra.ExactArgs(3), RunE: tracePacket, diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index af9f5dd23..30f391a6d 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -2,7 +2,7 @@ package iptables import ( "fmt" - "net" + "net/netip" "testing" "time" @@ -19,11 +19,8 @@ var ifaceMock = &iFaceMock{ }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("10.20.0.1"), - Network: &net.IPNet{ - IP: net.ParseIP("10.20.0.0"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IP: netip.MustParseAddr("10.20.0.1"), + Network: netip.MustParsePrefix("10.20.0.0/24"), } }, } @@ -70,12 +67,12 @@ func TestIptablesManager(t *testing.T) { var rule2 []fw.Rule t.Run("add second rule", func(t *testing.T) { - ip := net.ParseIP("10.20.0.3") + ip := netip.MustParseAddr("10.20.0.3") port := &fw.Port{ IsRange: true, Values: []uint16{8043, 8046}, } - rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "") + rule2, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", port, nil, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") for _, r := range rule2 { @@ -95,9 +92,9 @@ func TestIptablesManager(t *testing.T) { t.Run("reset check", func(t *testing.T) { // add second rule - ip := net.ParseIP("10.20.0.3") + ip := netip.MustParseAddr("10.20.0.3") port := &fw.Port{Values: []uint16{5353}} - _, err = manager.AddPeerFiltering(nil, ip, "udp", nil, port, fw.ActionAccept, "") + _, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "udp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") err = manager.Close(nil) @@ -119,11 +116,8 @@ func TestIptablesManagerIPSet(t *testing.T) { }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("10.20.0.1"), - Network: &net.IPNet{ - IP: net.ParseIP("10.20.0.0"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IP: netip.MustParseAddr("10.20.0.1"), + Network: netip.MustParsePrefix("10.20.0.0/24"), } }, } @@ -144,11 +138,11 @@ func TestIptablesManagerIPSet(t *testing.T) { var rule2 []fw.Rule t.Run("add second rule", func(t *testing.T) { - ip := net.ParseIP("10.20.0.3") + ip := netip.MustParseAddr("10.20.0.3") port := &fw.Port{ Values: []uint16{443}, } - rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "default") + rule2, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", port, nil, fw.ActionAccept, "default") for _, r := range rule2 { require.NoError(t, err, "failed to add rule") require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set") @@ -186,11 +180,8 @@ func TestIptablesCreatePerformance(t *testing.T) { }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("10.20.0.1"), - Network: &net.IPNet{ - IP: net.ParseIP("10.20.0.0"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IP: netip.MustParseAddr("10.20.0.1"), + Network: netip.MustParsePrefix("10.20.0.0/24"), } }, } @@ -212,11 +203,11 @@ func TestIptablesCreatePerformance(t *testing.T) { require.NoError(t, err) - ip := net.ParseIP("10.20.0.100") + ip := netip.MustParseAddr("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []uint16{uint16(1000 + i)}} - _, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "") + _, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") } diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 602a6b8dc..1dd3e9183 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -3,7 +3,6 @@ package nftables import ( "bytes" "fmt" - "net" "net/netip" "os/exec" "testing" @@ -25,11 +24,8 @@ var ifaceMock = &iFaceMock{ }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("100.96.0.1"), - Network: &net.IPNet{ - IP: net.ParseIP("100.96.0.0"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IP: netip.MustParseAddr("100.96.0.1"), + Network: netip.MustParsePrefix("100.96.0.0/16"), } }, } @@ -70,11 +66,11 @@ func TestNftablesManager(t *testing.T) { time.Sleep(time.Second) }() - ip := net.ParseIP("100.96.0.1") + ip := netip.MustParseAddr("100.96.0.1").Unmap() testClient := &nftables.Conn{} - rule, err := manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "") + rule, err := manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "") require.NoError(t, err, "failed to add rule") err = manager.Flush() @@ -109,8 +105,6 @@ func TestNftablesManager(t *testing.T) { } compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1) - ipToAdd, _ := netip.AddrFromSlice(ip) - add := ipToAdd.Unmap() expectedExprs2 := []expr.Any{ &expr.Payload{ DestRegister: 1, @@ -132,7 +126,7 @@ func TestNftablesManager(t *testing.T) { &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, - Data: add.AsSlice(), + Data: ip.AsSlice(), }, &expr.Payload{ DestRegister: 1, @@ -173,11 +167,8 @@ func TestNFtablesCreatePerformance(t *testing.T) { }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("100.96.0.1"), - Network: &net.IPNet{ - IP: net.ParseIP("100.96.0.0"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IP: netip.MustParseAddr("100.96.0.1"), + Network: netip.MustParsePrefix("100.96.0.0/16"), } }, } @@ -197,11 +188,11 @@ func TestNFtablesCreatePerformance(t *testing.T) { time.Sleep(time.Second) }() - ip := net.ParseIP("10.20.0.100") + ip := netip.MustParseAddr("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []uint16{uint16(1000 + i)}} - _, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "") + _, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") if i%100 == 0 { @@ -282,8 +273,8 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { verifyIptablesOutput(t, stdout, stderr) }) - ip := net.ParseIP("100.96.0.1") - _, err = manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "") + ip := netip.MustParseAddr("100.96.0.1") + _, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "") require.NoError(t, err, "failed to add peer filtering rule") _, err = manager.AddRouteFiltering( diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go index 2ae983f6e..42a3e0800 100644 --- a/client/firewall/uspfilter/forwarder/forwarder.go +++ b/client/firewall/uspfilter/forwarder/forwarder.go @@ -41,7 +41,7 @@ type Forwarder struct { udpForwarder *udpForwarder ctx context.Context cancel context.CancelFunc - ip net.IP + ip tcpip.Address netstack bool } @@ -71,12 +71,11 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow return nil, fmt.Errorf("failed to create NIC: %v", err) } - ones, _ := iface.Address().Network.Mask.Size() protoAddr := tcpip.ProtocolAddress{ Protocol: ipv4.ProtocolNumber, AddressWithPrefix: tcpip.AddressWithPrefix{ - Address: tcpip.AddrFromSlice(iface.Address().IP.To4()), - PrefixLen: ones, + Address: tcpip.AddrFromSlice(iface.Address().IP.AsSlice()), + PrefixLen: iface.Address().Network.Bits(), }, } @@ -116,7 +115,7 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow ctx: ctx, cancel: cancel, netstack: netstack, - ip: iface.Address().IP, + ip: tcpip.AddrFromSlice(iface.Address().IP.AsSlice()), } receiveWindow := defaultReceiveWindow @@ -167,7 +166,7 @@ func (f *Forwarder) Stop() { } func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP { - if f.netstack && f.ip.Equal(addr.AsSlice()) { + if f.netstack && f.ip.Equal(addr) { return net.IPv4(127, 0, 0, 1) } return addr.AsSlice() @@ -179,7 +178,6 @@ func (f *Forwarder) RegisterRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uin } func (f *Forwarder) getRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) ([]byte, bool) { - if value, ok := f.ruleIdMap.Load(buildKey(srcIP, dstIP, srcPort, dstPort)); ok { return value.([]byte), true } else if value, ok := f.ruleIdMap.Load(buildKey(dstIP, srcIP, dstPort, srcPort)); ok { diff --git a/client/firewall/uspfilter/localip.go b/client/firewall/uspfilter/localip.go index f093f3429..7f6b52c71 100644 --- a/client/firewall/uspfilter/localip.go +++ b/client/firewall/uspfilter/localip.go @@ -45,24 +45,26 @@ func (m *localIPManager) setBitmapBit(ip net.IP) { m.ipv4Bitmap[high].bitmap[index] |= 1 << bit } -func (m *localIPManager) setBitInBitmap(ip net.IP, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) { - if ipv4 := ip.To4(); ipv4 != nil { - high := uint16(ipv4[0]) - low := (uint16(ipv4[1]) << 8) | (uint16(ipv4[2]) << 4) | uint16(ipv4[3]) +func (m *localIPManager) setBitInBitmap(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) { + if !ip.Is4() { + return + } + ipv4 := ip.AsSlice() - if bitmap[high] == nil { - bitmap[high] = &ipv4LowBitmap{} - } + high := uint16(ipv4[0]) + low := (uint16(ipv4[1]) << 8) | (uint16(ipv4[2]) << 4) | uint16(ipv4[3]) - index := low / 32 - bit := low % 32 - bitmap[high].bitmap[index] |= 1 << bit + if bitmap[high] == nil { + bitmap[high] = &ipv4LowBitmap{} + } - ipStr := ipv4.String() - if _, exists := ipv4Set[ipStr]; !exists { - ipv4Set[ipStr] = struct{}{} - *ipv4Addresses = append(*ipv4Addresses, ipStr) - } + index := low / 32 + bit := low % 32 + bitmap[high].bitmap[index] |= 1 << bit + + if _, exists := ipv4Set[ip]; !exists { + ipv4Set[ip] = struct{}{} + *ipv4Addresses = append(*ipv4Addresses, ip) } } @@ -79,12 +81,12 @@ func (m *localIPManager) checkBitmapBit(ip []byte) bool { return (m.ipv4Bitmap[high].bitmap[index] & (1 << bit)) != 0 } -func (m *localIPManager) processIP(ip net.IP, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) error { +func (m *localIPManager) processIP(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) error { m.setBitInBitmap(ip, bitmap, ipv4Set, ipv4Addresses) return nil } -func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) { +func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) { addrs, err := iface.Addrs() if err != nil { log.Debugf("get addresses for interface %s failed: %v", iface.Name, err) @@ -102,7 +104,13 @@ func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv continue } - if err := m.processIP(ip, bitmap, ipv4Set, ipv4Addresses); err != nil { + addr, ok := netip.AddrFromSlice(ip) + if !ok { + log.Warnf("invalid IP address %s in interface %s", ip.String(), iface.Name) + continue + } + + if err := m.processIP(addr.Unmap(), bitmap, ipv4Set, ipv4Addresses); err != nil { log.Debugf("process IP failed: %v", err) } } @@ -116,8 +124,8 @@ func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) { }() var newIPv4Bitmap [256]*ipv4LowBitmap - ipv4Set := make(map[string]struct{}) - var ipv4Addresses []string + ipv4Set := make(map[netip.Addr]struct{}) + var ipv4Addresses []netip.Addr // 127.0.0.0/8 newIPv4Bitmap[127] = &ipv4LowBitmap{} diff --git a/client/firewall/uspfilter/localip_test.go b/client/firewall/uspfilter/localip_test.go index 0104c9603..45ac912cd 100644 --- a/client/firewall/uspfilter/localip_test.go +++ b/client/firewall/uspfilter/localip_test.go @@ -20,11 +20,8 @@ func TestLocalIPManager(t *testing.T) { { name: "Localhost range", setupAddr: wgaddr.Address{ - IP: net.ParseIP("192.168.1.1"), - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.0"), - Mask: net.CIDRMask(24, 32), - }, + IP: netip.MustParseAddr("192.168.1.1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("127.0.0.2"), expected: true, @@ -32,11 +29,8 @@ func TestLocalIPManager(t *testing.T) { { name: "Localhost standard address", setupAddr: wgaddr.Address{ - IP: net.ParseIP("192.168.1.1"), - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.0"), - Mask: net.CIDRMask(24, 32), - }, + IP: netip.MustParseAddr("192.168.1.1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("127.0.0.1"), expected: true, @@ -44,11 +38,8 @@ func TestLocalIPManager(t *testing.T) { { name: "Localhost range edge", setupAddr: wgaddr.Address{ - IP: net.ParseIP("192.168.1.1"), - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.0"), - Mask: net.CIDRMask(24, 32), - }, + IP: netip.MustParseAddr("192.168.1.1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("127.255.255.255"), expected: true, @@ -56,11 +47,8 @@ func TestLocalIPManager(t *testing.T) { { name: "Local IP matches", setupAddr: wgaddr.Address{ - IP: net.ParseIP("192.168.1.1"), - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.0"), - Mask: net.CIDRMask(24, 32), - }, + IP: netip.MustParseAddr("192.168.1.1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("192.168.1.1"), expected: true, @@ -68,11 +56,8 @@ func TestLocalIPManager(t *testing.T) { { name: "Local IP doesn't match", setupAddr: wgaddr.Address{ - IP: net.ParseIP("192.168.1.1"), - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.0"), - Mask: net.CIDRMask(24, 32), - }, + IP: netip.MustParseAddr("192.168.1.1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("192.168.1.2"), expected: false, @@ -80,11 +65,8 @@ func TestLocalIPManager(t *testing.T) { { name: "Local IP doesn't match - addresses 32 apart", setupAddr: wgaddr.Address{ - IP: net.ParseIP("192.168.1.1"), - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.0"), - Mask: net.CIDRMask(24, 32), - }, + IP: netip.MustParseAddr("192.168.1.1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("192.168.1.33"), expected: false, @@ -92,11 +74,8 @@ func TestLocalIPManager(t *testing.T) { { name: "IPv6 address", setupAddr: wgaddr.Address{ - IP: net.ParseIP("fe80::1"), - Network: &net.IPNet{ - IP: net.ParseIP("fe80::"), - Mask: net.CIDRMask(64, 128), - }, + IP: netip.MustParseAddr("fe80::1"), + Network: netip.MustParsePrefix("192.168.1.0/24"), }, testIP: netip.MustParseAddr("fe80::1"), expected: false, diff --git a/client/firewall/uspfilter/tracer_test.go b/client/firewall/uspfilter/tracer_test.go index bd87879a5..46c115787 100644 --- a/client/firewall/uspfilter/tracer_test.go +++ b/client/firewall/uspfilter/tracer_test.go @@ -38,11 +38,8 @@ func TestTracePacket(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("100.10.0.100"), - Network: &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - }, + IP: netip.MustParseAddr("100.10.0.100"), + Network: netip.MustParsePrefix("100.10.0.0/16"), } }, } diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 8e0a955ca..eede1ab13 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -71,7 +71,6 @@ type Manager struct { // incomingRules is used for filtering and hooks incomingRules map[netip.Addr]RuleSet routeRules RouteRules - wgNetwork *net.IPNet decoders sync.Pool wgIface common.IFaceMapper nativeFirewall firewall.Manager @@ -1091,11 +1090,6 @@ func (m *Manager) ruleMatches(rule *RouteRule, srcAddr, dstAddr netip.Addr, prot return true } -// SetNetwork of the wireguard interface to which filtering applied -func (m *Manager) SetNetwork(network *net.IPNet) { - m.wgNetwork = network -} - // AddUDPPacketHook calls hook when UDP packet from given direction matched // // Hook function returns flag which indicates should be the matched package dropped or not diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go index beb5b9336..c03e60640 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/uspfilter_bench_test.go @@ -174,11 +174,6 @@ func BenchmarkCoreFiltering(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } - // Apply scenario-specific setup sc.setupFunc(manager) @@ -219,11 +214,6 @@ func BenchmarkStateScaling(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } - // Pre-populate connection table srcIPs := generateRandomIPs(count) dstIPs := generateRandomIPs(count) @@ -267,11 +257,6 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } - srcIP := generateRandomIPs(1)[0] dstIP := generateRandomIPs(1)[0] outbound := generatePacket(b, srcIP, dstIP, 1024, 80, layers.IPProtocolTCP) @@ -304,10 +289,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolTCP, state: "new", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } b.Setenv("NB_DISABLE_CONNTRACK", "1") }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -321,10 +302,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolTCP, state: "established", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } b.Setenv("NB_DISABLE_CONNTRACK", "1") }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -339,10 +316,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolUDP, state: "new", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } b.Setenv("NB_DISABLE_CONNTRACK", "1") }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -356,10 +329,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolUDP, state: "established", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - } b.Setenv("NB_DISABLE_CONNTRACK", "1") }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -373,10 +342,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolTCP, state: "new", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - } require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK")) }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -390,10 +355,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolTCP, state: "established", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - } require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK")) }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -408,10 +369,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolTCP, state: "post_handshake", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - } require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK")) }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -426,10 +383,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolUDP, state: "new", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - } require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK")) }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -443,10 +396,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { proto: layers.IPProtocolUDP, state: "established", setupFunc: func(m *Manager) { - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - } require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK")) }, genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) { @@ -593,11 +542,6 @@ func BenchmarkLongLivedConnections(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.SetNetwork(&net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - }) - // Setup initial state based on scenario if sc.rules { // Single rule to allow all return traffic from port 80 @@ -681,11 +625,6 @@ func BenchmarkShortLivedConnections(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.SetNetwork(&net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - }) - // Setup initial state based on scenario if sc.rules { // Single rule to allow all return traffic from port 80 @@ -797,11 +736,6 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.SetNetwork(&net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - }) - // Setup initial state based on scenario if sc.rules { _, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "") @@ -882,11 +816,6 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { require.NoError(b, manager.Close(nil)) }) - manager.SetNetwork(&net.IPNet{ - IP: net.ParseIP("100.64.0.0"), - Mask: net.CIDRMask(10, 32), - }) - if sc.rules { _, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "") require.NoError(b, err) @@ -1032,7 +961,8 @@ func BenchmarkRouteACLs(b *testing.B) { } for _, r := range rules { - _, err := manager.AddRouteFiltering(nil, r.sources, r.dest, r.proto, nil, r.port, fw.ActionAccept) + dst := fw.Network{Prefix: r.dest} + _, err := manager.AddRouteFiltering(nil, r.sources, dst, r.proto, nil, r.port, fw.ActionAccept) if err != nil { b.Fatal(err) } diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go index 04a398d1f..318f86a87 100644 --- a/client/firewall/uspfilter/uspfilter_filter_test.go +++ b/client/firewall/uspfilter/uspfilter_filter_test.go @@ -19,12 +19,8 @@ import ( ) func TestPeerACLFiltering(t *testing.T) { - localIP := net.ParseIP("100.10.0.100") - wgNet := &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - } - + localIP := netip.MustParseAddr("100.10.0.100") + wgNet := netip.MustParsePrefix("100.10.0.0/16") ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, AddressFunc: func() wgaddr.Address { @@ -43,8 +39,6 @@ func TestPeerACLFiltering(t *testing.T) { require.NoError(t, manager.Close(nil)) }) - manager.wgNetwork = wgNet - err = manager.UpdateLocalIPs() require.NoError(t, err) @@ -581,14 +575,13 @@ func setupRoutedManager(tb testing.TB, network string) *Manager { dev := mocks.NewMockDevice(ctrl) dev.EXPECT().MTU().Return(1500, nil).AnyTimes() - localIP, wgNet, err := net.ParseCIDR(network) - require.NoError(tb, err) + wgNet := netip.MustParsePrefix(network) ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: localIP, + IP: wgNet.Addr(), Network: wgNet, } }, @@ -1440,11 +1433,8 @@ func TestRouteACLSet(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("100.10.0.100"), - Network: &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - }, + IP: netip.MustParseAddr("100.10.0.100"), + Network: netip.MustParsePrefix("100.10.0.0/16"), } }, } diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index 24a6a2c40..88de1ddcd 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -271,11 +271,8 @@ func TestNotMatchByIP(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("100.10.0.100"), - Network: &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - }, + IP: netip.MustParseAddr("100.10.0.100"), + Network: netip.MustParsePrefix("100.10.0.0/16"), } }, } @@ -285,10 +282,6 @@ func TestNotMatchByIP(t *testing.T) { t.Errorf("failed to create Manager: %v", err) return } - m.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - } ip := net.ParseIP("0.0.0.0") proto := fw.ProtocolUDP @@ -396,10 +389,6 @@ func TestProcessOutgoingHooks(t *testing.T) { }, false, flowLogger) require.NoError(t, err) - manager.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - } manager.udpTracker.Close() manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger, flowLogger) defer func() { @@ -509,11 +498,6 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { }, false, flowLogger) require.NoError(t, err) - manager.wgNetwork = &net.IPNet{ - IP: net.ParseIP("100.10.0.0"), - Mask: net.CIDRMask(16, 32), - } - manager.udpTracker.Close() // Close the existing tracker manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger, flowLogger) manager.decoders = sync.Pool{ diff --git a/client/iface/bind/udp_mux_universal.go b/client/iface/bind/udp_mux_universal.go index 9fed02bb7..5cc634955 100644 --- a/client/iface/bind/udp_mux_universal.go +++ b/client/iface/bind/udp_mux_universal.go @@ -164,7 +164,7 @@ func (u *udpConn) performFilterCheck(addr net.Addr) error { return nil } - if u.address.Network.Contains(a.AsSlice()) { + if u.address.Network.Contains(a) { log.Warnf("Address %s is part of the NetBird network %s, refusing to write", addr, u.address) return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address) } diff --git a/client/iface/device/device_filter.go b/client/iface/device/device_filter.go index c9b7e2448..5a1a0e96a 100644 --- a/client/iface/device/device_filter.go +++ b/client/iface/device/device_filter.go @@ -1,7 +1,6 @@ package device import ( - "net" "net/netip" "sync" @@ -24,9 +23,6 @@ type PacketFilter interface { // RemovePacketHook removes hook by ID RemovePacketHook(hookID string) error - - // SetNetwork of the wireguard interface to which filtering applied - SetNetwork(*net.IPNet) } // FilteredDevice to override Read or Write of packets diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go index d3c92235e..d2f2c87a1 100644 --- a/client/iface/device/device_netstack.go +++ b/client/iface/device/device_netstack.go @@ -51,7 +51,11 @@ func (t *TunNetstackDevice) Create() (WGConfigurer, error) { log.Info("create nbnetstack tun interface") // TODO: get from service listener runtime IP - dnsAddr := nbnet.GetLastIPFromNetwork(t.address.Network, 1) + dnsAddr, err := nbnet.GetLastIPFromNetwork(t.address.Network, 1) + if err != nil { + return nil, fmt.Errorf("last ip: %w", err) + } + log.Debugf("netstack using address: %s", t.address.IP) t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, t.mtu) log.Debugf("netstack using dns address: %s", dnsAddr) diff --git a/client/iface/device/wg_link_freebsd.go b/client/iface/device/wg_link_freebsd.go index 9067790e4..1b06e0e15 100644 --- a/client/iface/device/wg_link_freebsd.go +++ b/client/iface/device/wg_link_freebsd.go @@ -64,7 +64,15 @@ func (l *wgLink) assignAddr(address wgaddr.Address) error { } ip := address.IP.String() - mask := "0x" + address.Network.Mask.String() + + // Convert prefix length to hex netmask + prefixLen := address.Network.Bits() + if !address.IP.Is4() { + return fmt.Errorf("IPv6 not supported for interface assignment") + } + + maskBits := uint32(0xffffffff) << (32 - prefixLen) + mask := fmt.Sprintf("0x%08x", maskBits) log.Infof("assign addr %s mask %s to %s interface", ip, mask, l.name) diff --git a/client/iface/iface.go b/client/iface/iface.go index c78a252da..1f659af29 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -185,7 +185,6 @@ func (w *WGIface) SetFilter(filter device.PacketFilter) error { } w.filter = filter - w.filter.SetNetwork(w.tun.WgAddress().Network) w.tun.FilteredDevice().SetFilter(filter) return nil diff --git a/client/iface/mocks/filter.go b/client/iface/mocks/filter.go index faac55d68..8cd2a1231 100644 --- a/client/iface/mocks/filter.go +++ b/client/iface/mocks/filter.go @@ -5,7 +5,6 @@ package mocks import ( - net "net" "net/netip" reflect "reflect" @@ -90,15 +89,3 @@ func (mr *MockPacketFilterMockRecorder) RemovePacketHook(arg0 interface{}) *gomo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePacketHook", reflect.TypeOf((*MockPacketFilter)(nil).RemovePacketHook), arg0) } - -// SetNetwork mocks base method. -func (m *MockPacketFilter) SetNetwork(arg0 *net.IPNet) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetNetwork", arg0) -} - -// SetNetwork indicates an expected call of SetNetwork. -func (mr *MockPacketFilterMockRecorder) SetNetwork(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetwork", reflect.TypeOf((*MockPacketFilter)(nil).SetNetwork), arg0) -} diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go index a271a1954..aec9d4faa 100644 --- a/client/iface/netstack/tun.go +++ b/client/iface/netstack/tun.go @@ -1,8 +1,6 @@ package netstack import ( - "fmt" - "net" "net/netip" "os" "strconv" @@ -15,8 +13,8 @@ import ( const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY" type NetStackTun struct { //nolint:revive - address net.IP - dnsAddress net.IP + address netip.Addr + dnsAddress netip.Addr mtu int listenAddress string @@ -24,7 +22,7 @@ type NetStackTun struct { //nolint:revive tundev tun.Device } -func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu int) *NetStackTun { +func NewNetStackTun(listenAddress string, address netip.Addr, dnsAddress netip.Addr, mtu int) *NetStackTun { return &NetStackTun{ address: address, dnsAddress: dnsAddress, @@ -34,19 +32,9 @@ func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu } func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) { - addr, ok := netip.AddrFromSlice(t.address) - if !ok { - return nil, nil, fmt.Errorf("convert address to netip.Addr: %v", t.address) - } - - dnsAddr, ok := netip.AddrFromSlice(t.dnsAddress) - if !ok { - return nil, nil, fmt.Errorf("convert dns address to netip.Addr: %v", t.dnsAddress) - } - nsTunDev, tunNet, err := netstack.CreateNetTUN( - []netip.Addr{addr.Unmap()}, - []netip.Addr{dnsAddr.Unmap()}, + []netip.Addr{t.address}, + []netip.Addr{t.dnsAddress}, t.mtu) if err != nil { return nil, nil, err diff --git a/client/iface/wgaddr/address.go b/client/iface/wgaddr/address.go index e5079258c..078f8be95 100644 --- a/client/iface/wgaddr/address.go +++ b/client/iface/wgaddr/address.go @@ -2,28 +2,27 @@ package wgaddr import ( "fmt" - "net" + "net/netip" ) // Address WireGuard parsed address type Address struct { - IP net.IP - Network *net.IPNet + IP netip.Addr + Network netip.Prefix } // ParseWGAddress parse a string ("1.2.3.4/24") address to WG Address func ParseWGAddress(address string) (Address, error) { - ip, network, err := net.ParseCIDR(address) + prefix, err := netip.ParsePrefix(address) if err != nil { return Address{}, err } return Address{ - IP: ip, - Network: network, + IP: prefix.Addr().Unmap(), + Network: prefix.Masked(), }, nil } func (addr Address) String() string { - maskSize, _ := addr.Network.Mask.Size() - return fmt.Sprintf("%s/%d", addr.IP.String(), maskSize) + return fmt.Sprintf("%s/%d", addr.IP.String(), addr.Network.Bits()) } diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 532d70a24..16620033e 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -1,7 +1,7 @@ package acl import ( - "net" + "net/netip" "testing" "github.com/golang/mock/gomock" @@ -43,12 +43,11 @@ func TestDefaultManager(t *testing.T) { ifaceMock := mocks.NewMockIFaceMapper(ctrl) ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes() ifaceMock.EXPECT().SetFilter(gomock.Any()) - ip, network, err := net.ParseCIDR("172.0.0.1/32") - require.NoError(t, err) + network := netip.MustParsePrefix("172.0.0.1/32") ifaceMock.EXPECT().Name().Return("lo").AnyTimes() ifaceMock.EXPECT().Address().Return(wgaddr.Address{ - IP: ip, + IP: network.Addr(), Network: network, }).AnyTimes() ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() @@ -162,12 +161,11 @@ func TestDefaultManagerStateless(t *testing.T) { ifaceMock := mocks.NewMockIFaceMapper(ctrl) ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes() ifaceMock.EXPECT().SetFilter(gomock.Any()) - ip, network, err := net.ParseCIDR("172.0.0.1/32") - require.NoError(t, err) + network := netip.MustParsePrefix("172.0.0.1/32") ifaceMock.EXPECT().Name().Return("lo").AnyTimes() ifaceMock.EXPECT().Address().Return(wgaddr.Address{ - IP: ip, + IP: network.Addr(), Network: network, }).AnyTimes() ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() @@ -372,12 +370,11 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { ifaceMock := mocks.NewMockIFaceMapper(ctrl) ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes() ifaceMock.EXPECT().SetFilter(gomock.Any()) - ip, network, err := net.ParseCIDR("172.0.0.1/32") - require.NoError(t, err) + network := netip.MustParsePrefix("172.0.0.1/32") ifaceMock.EXPECT().Name().Return("lo").AnyTimes() ifaceMock.EXPECT().Address().Return(wgaddr.Address{ - IP: ip, + IP: network.Addr(), Network: network, }).AnyTimes() ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() diff --git a/client/internal/dns.go b/client/internal/dns.go index 8a73f50f2..5e604bec5 100644 --- a/client/internal/dns.go +++ b/client/internal/dns.go @@ -2,7 +2,7 @@ package internal import ( "fmt" - "net" + "net/netip" "slices" "strings" @@ -12,13 +12,14 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) -func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.SimpleRecord, bool) { - ip := net.ParseIP(aRecord.RData) - if ip == nil || ip.To4() == nil { +func createPTRRecord(aRecord nbdns.SimpleRecord, prefix netip.Prefix) (nbdns.SimpleRecord, bool) { + ip, err := netip.ParseAddr(aRecord.RData) + if err != nil { + log.Warnf("failed to parse IP address %s: %v", aRecord.RData, err) return nbdns.SimpleRecord{}, false } - if !ipNet.Contains(ip) { + if !prefix.Contains(ip) { return nbdns.SimpleRecord{}, false } @@ -36,16 +37,19 @@ func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.Simple } // generateReverseZoneName creates the reverse DNS zone name for a given network -func generateReverseZoneName(ipNet *net.IPNet) (string, error) { - networkIP := ipNet.IP.Mask(ipNet.Mask) - maskOnes, _ := ipNet.Mask.Size() +func generateReverseZoneName(network netip.Prefix) (string, error) { + networkIP := network.Masked().Addr() + + if !networkIP.Is4() { + return "", fmt.Errorf("reverse DNS is only supported for IPv4 networks, got: %s", networkIP) + } // round up to nearest byte - octetsToUse := (maskOnes + 7) / 8 + octetsToUse := (network.Bits() + 7) / 8 octets := strings.Split(networkIP.String(), ".") if octetsToUse > len(octets) { - return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", maskOnes) + return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", network.Bits()) } reverseOctets := make([]string, octetsToUse) @@ -68,7 +72,7 @@ func zoneExists(config *nbdns.Config, zoneName string) bool { } // collectPTRRecords gathers all PTR records for the given network from A records -func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRecord { +func collectPTRRecords(config *nbdns.Config, prefix netip.Prefix) []nbdns.SimpleRecord { var records []nbdns.SimpleRecord for _, zone := range config.CustomZones { @@ -77,7 +81,7 @@ func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRec continue } - if ptrRecord, ok := createPTRRecord(record, ipNet); ok { + if ptrRecord, ok := createPTRRecord(record, prefix); ok { records = append(records, ptrRecord) } } @@ -87,8 +91,8 @@ func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRec } // addReverseZone adds a reverse DNS zone to the configuration for the given network -func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) { - zoneName, err := generateReverseZoneName(ipNet) +func addReverseZone(config *nbdns.Config, network netip.Prefix) { + zoneName, err := generateReverseZoneName(network) if err != nil { log.Warn(err) return @@ -99,7 +103,7 @@ func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) { return } - records := collectPTRRecords(config, ipNet) + records := collectPTRRecords(config, network) reverseZone := nbdns.CustomZone{ Domain: zoneName, diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 1c7c9b117..e55b27910 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -46,10 +46,9 @@ func (w *mocWGIface) Name() string { } func (w *mocWGIface) Address() wgaddr.Address { - ip, network, _ := net.ParseCIDR("100.66.100.0/24") return wgaddr.Address{ - IP: ip, - Network: network, + IP: netip.MustParseAddr("100.66.100.1"), + Network: netip.MustParsePrefix("100.66.100.0/24"), } } @@ -464,17 +463,10 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - _, ipNet, err := net.ParseCIDR("100.66.100.1/32") - if err != nil { - t.Errorf("parse CIDR: %v", err) - return - } - packetfilter := pfmock.NewMockPacketFilter(ctrl) packetfilter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).AnyTimes() packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) packetfilter.EXPECT().RemovePacketHook(gomock.Any()) - packetfilter.EXPECT().SetNetwork(ipNet) if err := wgIface.SetFilter(packetfilter); err != nil { t.Errorf("set packet filter: %v", err) diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 34c563757..226202cf7 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -24,11 +24,15 @@ type ServiceViaMemory struct { } func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory { + lastIP, err := nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1) + if err != nil { + log.Errorf("get last ip from network: %v", err) + } s := &ServiceViaMemory{ wgInterface: wgIface, dnsMux: dns.NewServeMux(), - runtimeIP: nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1).String(), + runtimeIP: lastIP.String(), runtimePort: defaultPort, } return s @@ -91,7 +95,7 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) { } firstLayerDecoder := layers.LayerTypeIPv4 - if s.wgInterface.Address().Network.IP.To4() == nil { + if s.wgInterface.Address().IP.Is6() { firstLayerDecoder = layers.LayerTypeIPv6 } diff --git a/client/internal/dns/service_memory_test.go b/client/internal/dns/service_memory_test.go deleted file mode 100644 index 244adfaef..000000000 --- a/client/internal/dns/service_memory_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package dns - -import ( - "net" - "testing" - - nbnet "github.com/netbirdio/netbird/util/net" -) - -func TestGetLastIPFromNetwork(t *testing.T) { - tests := []struct { - addr string - ip string - }{ - {"2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:fffe"}, - {"192.168.0.0/30", "192.168.0.2"}, - {"192.168.0.0/16", "192.168.255.254"}, - {"192.168.0.0/24", "192.168.0.254"}, - } - - for _, tt := range tests { - _, ipnet, err := net.ParseCIDR(tt.addr) - if err != nil { - t.Errorf("Error parsing CIDR: %v", err) - return - } - - lastIP := nbnet.GetLastIPFromNetwork(ipnet, 1).String() - if lastIP != tt.ip { - t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP) - } - } -} diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go index 06ffcba11..52d2ba58b 100644 --- a/client/internal/dns/upstream_android.go +++ b/client/internal/dns/upstream_android.go @@ -3,6 +3,7 @@ package dns import ( "context" "net" + "net/netip" "syscall" "time" @@ -23,8 +24,8 @@ type upstreamResolver struct { func newUpstreamResolver( ctx context.Context, _ string, - _ net.IP, - _ *net.IPNet, + _ netip.Addr, + _ netip.Prefix, statusRecorder *peer.Status, hostsDNSHolder *hostsDNSHolder, domain string, diff --git a/client/internal/dns/upstream_general.go b/client/internal/dns/upstream_general.go index 9bb5feab0..1bc06a7c1 100644 --- a/client/internal/dns/upstream_general.go +++ b/client/internal/dns/upstream_general.go @@ -4,7 +4,7 @@ package dns import ( "context" - "net" + "net/netip" "time" "github.com/miekg/dns" @@ -19,8 +19,8 @@ type upstreamResolver struct { func newUpstreamResolver( ctx context.Context, _ string, - _ net.IP, - _ *net.IPNet, + _ netip.Addr, + _ netip.Prefix, statusRecorder *peer.Status, _ *hostsDNSHolder, domain string, diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index ca5b31132..648cab176 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net" + "net/netip" "syscall" "time" @@ -18,16 +19,16 @@ import ( type upstreamResolverIOS struct { *upstreamResolverBase - lIP net.IP - lNet *net.IPNet + lIP netip.Addr + lNet netip.Prefix interfaceName string } func newUpstreamResolver( ctx context.Context, interfaceName string, - ip net.IP, - net *net.IPNet, + ip netip.Addr, + net netip.Prefix, statusRecorder *peer.Status, _ *hostsDNSHolder, domain string, @@ -58,8 +59,11 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r * } client.DialTimeout = timeout - upstreamIP := net.ParseIP(upstreamHost) - if u.lNet.Contains(upstreamIP) || net.IP.IsPrivate(upstreamIP) { + upstreamIP, err := netip.ParseAddr(upstreamHost) + if err != nil { + log.Warnf("failed to parse upstream host %s: %s", upstreamHost, err) + } + if u.lNet.Contains(upstreamIP) || upstreamIP.IsPrivate() { log.Debugf("using private client to query upstream: %s", upstream) client, err = GetClientPrivate(u.lIP, u.interfaceName, timeout) if err != nil { @@ -73,7 +77,7 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r * // GetClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface // This method is needed for iOS -func GetClientPrivate(ip net.IP, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) { +func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) { index, err := getInterfaceIndex(interfaceName) if err != nil { log.Debugf("unable to get interface index for %s: %s", interfaceName, err) @@ -82,7 +86,7 @@ func GetClientPrivate(ip net.IP, interfaceName string, dialTimeout time.Duration dialer := &net.Dialer{ LocalAddr: &net.UDPAddr{ - IP: ip, + IP: ip.AsSlice(), Port: 0, // Let the OS pick a free port }, Timeout: dialTimeout, diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go index 13bc91a37..e440995d9 100644 --- a/client/internal/dns/upstream_test.go +++ b/client/internal/dns/upstream_test.go @@ -2,7 +2,7 @@ package dns import ( "context" - "net" + "net/netip" "strings" "testing" "time" @@ -58,7 +58,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) - resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil, ".") + resolver, _ := newUpstreamResolver(ctx, "", netip.Addr{}, netip.Prefix{}, nil, nil, ".") resolver.upstreamServers = testCase.InputServers resolver.upstreamTimeout = testCase.timeout if testCase.cancelCTX { diff --git a/client/internal/engine.go b/client/internal/engine.go index d015c1d6c..0dec799bf 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1008,7 +1008,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { // apply routes first, route related actions might depend on routing being enabled routes := toRoutes(networkMap.GetRoutes()) if err := e.routeManager.UpdateRoutes(serial, routes, dnsRouteFeatureFlag); err != nil { - log.Errorf("failed to update clientRoutes, err: %v", err) + log.Errorf("failed to update routes: %v", err) } if e.acl != nil { @@ -1104,7 +1104,7 @@ func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route { convertedRoute := &route.Route{ ID: route.ID(protoRoute.ID), - Network: prefix, + Network: prefix.Masked(), Domains: domain.FromPunycodeList(protoRoute.Domains), NetID: route.NetID(protoRoute.NetID), NetworkType: route.NetworkType(protoRoute.NetworkType), @@ -1138,7 +1138,7 @@ func toRouteDomains(myPubKey string, routes []*route.Route) []*dnsfwd.ForwarderE return entries } -func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network *net.IPNet) nbdns.Config { +func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network netip.Prefix) nbdns.Config { dnsUpdate := nbdns.Config{ ServiceEnable: protoDNSConfig.GetServiceEnable(), CustomZones: make([]nbdns.CustomZone, 0), @@ -1790,9 +1790,9 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) { } // GetWgAddr returns the wireguard address -func (e *Engine) GetWgAddr() net.IP { +func (e *Engine) GetWgAddr() netip.Addr { if e.wgInterface == nil { - return nil + return netip.Addr{} } return e.wgInterface.Address().IP } @@ -1861,12 +1861,7 @@ func (e *Engine) Address() (netip.Addr, error) { return netip.Addr{}, errors.New("wireguard interface not initialized") } - addr := e.wgInterface.Address() - ip, ok := netip.AddrFromSlice(addr.IP) - if !ok { - return netip.Addr{}, errors.New("failed to convert address to netip.Addr") - } - return ip.Unmap(), nil + return e.wgInterface.Address().IP, nil } func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewallManager.ForwardRule, error) { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 422059bd8..82c1ba0e2 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -371,11 +371,8 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { }, AddressFunc: func() wgaddr.Address { return wgaddr.Address{ - IP: net.ParseIP("10.20.0.1"), - Network: &net.IPNet{ - IP: net.ParseIP("10.20.0.0"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + IP: netip.MustParseAddr("10.20.0.1"), + Network: netip.MustParsePrefix("10.20.0.0/24"), } }, UpdatePeerFunc: func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { diff --git a/client/internal/netflow/conntrack/conntrack.go b/client/internal/netflow/conntrack/conntrack.go index f8440b913..d01adf135 100644 --- a/client/internal/netflow/conntrack/conntrack.go +++ b/client/internal/netflow/conntrack/conntrack.go @@ -232,7 +232,7 @@ func (c *ConnTrack) relevantFlow(mark uint32, srcIP, dstIP netip.Addr) bool { // fallback if mark rules are not in place wgnet := c.iface.Address().Network - return wgnet.Contains(srcIP.AsSlice()) || wgnet.Contains(dstIP.AsSlice()) + return wgnet.Contains(srcIP) || wgnet.Contains(dstIP) } // mapRxPackets maps packet counts to RX based on flow direction @@ -293,17 +293,15 @@ func (c *ConnTrack) inferDirection(mark uint32, srcIP, dstIP netip.Addr) nftypes // fallback if marks are not set wgaddr := c.iface.Address().IP wgnetwork := c.iface.Address().Network - src, dst := srcIP.AsSlice(), dstIP.AsSlice() - switch { - case wgaddr.Equal(src): + case wgaddr == srcIP: return nftypes.Egress - case wgaddr.Equal(dst): + case wgaddr == dstIP: return nftypes.Ingress - case wgnetwork.Contains(src): + case wgnetwork.Contains(srcIP): // netbird network -> resource network return nftypes.Ingress - case wgnetwork.Contains(dst): + case wgnetwork.Contains(dstIP): // resource network -> netbird network return nftypes.Egress } diff --git a/client/internal/netflow/logger/logger.go b/client/internal/netflow/logger/logger.go index a3bd091b6..e28fdf2f4 100644 --- a/client/internal/netflow/logger/logger.go +++ b/client/internal/netflow/logger/logger.go @@ -2,7 +2,7 @@ package logger import ( "context" - "net" + "net/netip" "sync" "sync/atomic" "time" @@ -23,17 +23,16 @@ type Logger struct { rcvChan atomic.Pointer[rcvChan] cancel context.CancelFunc statusRecorder *peer.Status - wgIfaceIPNet net.IPNet + wgIfaceNet netip.Prefix dnsCollection atomic.Bool exitNodeCollection atomic.Bool Store types.Store } -func New(statusRecorder *peer.Status, wgIfaceIPNet net.IPNet) *Logger { - +func New(statusRecorder *peer.Status, wgIfaceIPNet netip.Prefix) *Logger { return &Logger{ statusRecorder: statusRecorder, - wgIfaceIPNet: wgIfaceIPNet, + wgIfaceNet: wgIfaceIPNet, Store: store.NewMemoryStore(), } } @@ -89,11 +88,11 @@ func (l *Logger) startReceiver() { var isSrcExitNode bool var isDestExitNode bool - if !l.wgIfaceIPNet.Contains(net.IP(event.SourceIP.AsSlice())) { + if !l.wgIfaceNet.Contains(event.SourceIP) { event.SourceResourceID, isSrcExitNode = l.statusRecorder.CheckRoutes(event.SourceIP) } - if !l.wgIfaceIPNet.Contains(net.IP(event.DestIP.AsSlice())) { + if !l.wgIfaceNet.Contains(event.DestIP) { event.DestResourceID, isDestExitNode = l.statusRecorder.CheckRoutes(event.DestIP) } diff --git a/client/internal/netflow/logger/logger_test.go b/client/internal/netflow/logger/logger_test.go index 06e10c36c..1144544d8 100644 --- a/client/internal/netflow/logger/logger_test.go +++ b/client/internal/netflow/logger/logger_test.go @@ -1,7 +1,7 @@ package logger_test import ( - "net" + "net/netip" "testing" "time" @@ -12,7 +12,7 @@ import ( ) func TestStore(t *testing.T) { - logger := logger.New(nil, net.IPNet{}) + logger := logger.New(nil, netip.Prefix{}) logger.Enable() event := types.EventFields{ diff --git a/client/internal/netflow/manager.go b/client/internal/netflow/manager.go index bf80e5a9f..e3b188468 100644 --- a/client/internal/netflow/manager.go +++ b/client/internal/netflow/manager.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "net" + "net/netip" "runtime" "sync" "time" @@ -34,11 +34,11 @@ type Manager struct { // NewManager creates a new netflow manager func NewManager(iface nftypes.IFaceMapper, publicKey []byte, statusRecorder *peer.Status) *Manager { - var ipNet net.IPNet + var prefix netip.Prefix if iface != nil { - ipNet = *iface.Address().Network + prefix = iface.Address().Network } - flowLogger := logger.New(statusRecorder, ipNet) + flowLogger := logger.New(statusRecorder, prefix) var ct nftypes.ConnTracker if runtime.GOOS == "linux" && iface != nil && !iface.IsUserspaceBind() { diff --git a/client/internal/netflow/manager_test.go b/client/internal/netflow/manager_test.go index bf7e05f8e..0b5eb3be6 100644 --- a/client/internal/netflow/manager_test.go +++ b/client/internal/netflow/manager_test.go @@ -1,7 +1,7 @@ package netflow import ( - "net" + "net/netip" "testing" "time" @@ -33,10 +33,7 @@ func (m *mockIFaceMapper) IsUserspaceBind() bool { func TestManager_Update(t *testing.T) { mockIFace := &mockIFaceMapper{ address: wgaddr.Address{ - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.1"), - Mask: net.CIDRMask(24, 32), - }, + Network: netip.MustParsePrefix("192.168.1.1/32"), }, isUserspaceBind: true, } @@ -102,10 +99,7 @@ func TestManager_Update(t *testing.T) { func TestManager_Update_TokenPreservation(t *testing.T) { mockIFace := &mockIFaceMapper{ address: wgaddr.Address{ - Network: &net.IPNet{ - IP: net.ParseIP("192.168.1.1"), - Mask: net.CIDRMask(24, 32), - }, + Network: netip.MustParsePrefix("192.168.1.1/32"), }, isUserspaceBind: true, } diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index 6d51c88c0..78d5e3b30 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -264,7 +264,7 @@ func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg) error { continue } - prefix := netip.PrefixFrom(ip, ip.BitLen()) + prefix := netip.PrefixFrom(ip.Unmap(), ip.BitLen()) newPrefixes = append(newPrefixes, prefix) } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index afb74c23e..8dbbb5f77 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -333,11 +333,12 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes) + var merr *multierror.Error if !m.disableClientRoutes { filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap) if err := m.updateSystemRoutes(filteredClientRoutes); err != nil { - log.Errorf("Failed to update system routes: %v", err) + merr = multierror.Append(merr, fmt.Errorf("update system routes: %w", err)) } m.updateClientNetworks(updateSerial, filteredClientRoutes) @@ -346,14 +347,14 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro m.clientRoutes = newClientRoutesIDMap if m.serverRouter == nil { - return nil + return nberrors.FormatErrorOrNil(merr) } if err := m.serverRouter.UpdateRoutes(newServerRoutesMap, useNewDNSRoute); err != nil { - return fmt.Errorf("update routes: %w", err) + merr = multierror.Append(merr, fmt.Errorf("update server routes: %w", err)) } - return nil + return nberrors.FormatErrorOrNil(merr) } // SetRouteChangeListener set RouteListener for route change Notifier diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 680bd813f..a46ae080e 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -44,7 +44,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -71,7 +71,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: localPeerKey, - Network: netip.MustParsePrefix("100.64.252.250/30"), + Network: netip.MustParsePrefix("100.64.252.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -99,7 +99,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: localPeerKey, - Network: netip.MustParsePrefix("100.64.30.250/30"), + Network: netip.MustParsePrefix("100.64.30.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -127,7 +127,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: localPeerKey, - Network: netip.MustParsePrefix("100.64.30.250/30"), + Network: netip.MustParsePrefix("100.64.30.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -211,7 +211,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -233,7 +233,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -250,7 +250,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -272,7 +272,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -282,7 +282,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "b", NetID: "routeA", Peer: remotePeerKey2, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -299,7 +299,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -327,7 +327,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "a", NetID: "routeA", Peer: localPeerKey, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -356,7 +356,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "l1", NetID: "routeA", Peer: localPeerKey, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -376,7 +376,7 @@ func TestManagerUpdateRoutes(t *testing.T) { ID: "r1", NetID: "routeA", Peer: remotePeerKey1, - Network: netip.MustParsePrefix("100.64.251.250/30"), + Network: netip.MustParsePrefix("100.64.251.248/30"), NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -440,11 +440,11 @@ func TestManagerUpdateRoutes(t *testing.T) { } if len(testCase.inputInitRoutes) > 0 { - _ = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes, false) + err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes, false) require.NoError(t, err, "should update routes with init routes") } - _ = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes, false) + err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes, false) require.NoError(t, err, "should update routes") expectedWatchers := testCase.clientNetworkWatchersExpected diff --git a/client/internal/routemanager/sysctl/sysctl_linux.go b/client/internal/routemanager/sysctl/sysctl_linux.go index ea63f02fc..f96a57f37 100644 --- a/client/internal/routemanager/sysctl/sysctl_linux.go +++ b/client/internal/routemanager/sysctl/sysctl_linux.go @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" - "github.com/netbirdio/netbird/client/internal/routemanager/iface" + "github.com/netbirdio/netbird/client/iface/wgaddr" ) const ( @@ -22,8 +22,13 @@ const ( srcValidMarkPath = "net.ipv4.conf.all.src_valid_mark" ) +type iface interface { + Address() wgaddr.Address + Name() string +} + // Setup configures sysctl settings for RP filtering and source validation. -func Setup(wgIface iface.WGIface) (map[string]int, error) { +func Setup(wgIface iface) (map[string]int, error) { keys := map[string]int{} var result *multierror.Error diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index fd511fc20..261567dc3 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -6,9 +6,10 @@ import ( "net/netip" "sync" - "github.com/netbirdio/netbird/client/internal/routemanager/iface" + "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" ) type Nexthop struct { @@ -30,11 +31,16 @@ func (n Nexthop) String() string { return fmt.Sprintf("%s @ %d (%s)", n.IP.String(), n.Intf.Index, n.Intf.Name) } +type wgIface interface { + Address() wgaddr.Address + Name() string +} + type ExclusionCounter = refcounter.Counter[netip.Prefix, struct{}, Nexthop] type SysOps struct { refCounter *ExclusionCounter - wgInterface iface.WGIface + wgInterface wgIface // prefixes is tracking all the current added prefixes im memory // (this is used in iOS as all route updates require a full table update) //nolint @@ -45,9 +51,27 @@ type SysOps struct { notifier *notifier.Notifier } -func NewSysOps(wgInterface iface.WGIface, notifier *notifier.Notifier) *SysOps { +func NewSysOps(wgInterface wgIface, notifier *notifier.Notifier) *SysOps { return &SysOps{ wgInterface: wgInterface, notifier: notifier, } } + +func (r *SysOps) validateRoute(prefix netip.Prefix) error { + addr := prefix.Addr() + + switch { + case + !addr.IsValid(), + addr.IsLoopback(), + addr.IsLinkLocalUnicast(), + addr.IsLinkLocalMulticast(), + addr.IsInterfaceLocalMulticast(), + addr.IsMulticast(), + addr.IsUnspecified() && prefix.Bits() != 0, + r.wgInterface.Address().Network.Contains(addr): + return vars.ErrRouteNotAllowed + } + return nil +} diff --git a/client/internal/routemanager/systemops/systemops_bsd_test.go b/client/internal/routemanager/systemops/systemops_bsd_test.go index a83d7f1de..0d892c162 100644 --- a/client/internal/routemanager/systemops/systemops_bsd_test.go +++ b/client/internal/routemanager/systemops/systemops_bsd_test.go @@ -8,6 +8,8 @@ import ( "net/netip" "os/exec" "regexp" + "runtime" + "strings" "sync" "testing" @@ -33,7 +35,12 @@ func init() { func TestConcurrentRoutes(t *testing.T) { baseIP := netip.MustParseAddr("192.0.2.0") - intf := &net.Interface{Name: "lo0"} + + var intf *net.Interface + var nexthop Nexthop + + _, intf = setupDummyInterface(t) + nexthop = Nexthop{netip.Addr{}, intf} r := NewSysOps(nil, nil) @@ -43,7 +50,7 @@ func TestConcurrentRoutes(t *testing.T) { go func(ip netip.Addr) { defer wg.Done() prefix := netip.PrefixFrom(ip, 32) - if err := r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { + if err := r.addToRouteTable(prefix, nexthop); err != nil { t.Errorf("Failed to add route for %s: %v", prefix, err) } }(baseIP) @@ -59,7 +66,7 @@ func TestConcurrentRoutes(t *testing.T) { go func(ip netip.Addr) { defer wg.Done() prefix := netip.PrefixFrom(ip, 32) - if err := r.removeFromRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { + if err := r.removeFromRouteTable(prefix, nexthop); err != nil { t.Errorf("Failed to remove route for %s: %v", prefix, err) } }(baseIP) @@ -119,18 +126,39 @@ func TestBits(t *testing.T) { func createAndSetupDummyInterface(t *testing.T, intf string, ipAddressCIDR string) string { t.Helper() - err := exec.Command("ifconfig", intf, "alias", ipAddressCIDR).Run() - require.NoError(t, err, "Failed to create loopback alias") + if runtime.GOOS == "darwin" { + err := exec.Command("ifconfig", intf, "alias", ipAddressCIDR).Run() + require.NoError(t, err, "Failed to create loopback alias") + + t.Cleanup(func() { + err := exec.Command("ifconfig", intf, ipAddressCIDR, "-alias").Run() + assert.NoError(t, err, "Failed to remove loopback alias") + }) + + return intf + } + + prefix, err := netip.ParsePrefix(ipAddressCIDR) + require.NoError(t, err, "Failed to parse prefix") + + netIntf, err := net.InterfaceByName(intf) + require.NoError(t, err, "Failed to get interface by name") + + nexthop := Nexthop{netip.Addr{}, netIntf} + + r := NewSysOps(nil, nil) + err = r.addToRouteTable(prefix, nexthop) + require.NoError(t, err, "Failed to add route to table") t.Cleanup(func() { - err := exec.Command("ifconfig", intf, ipAddressCIDR, "-alias").Run() - assert.NoError(t, err, "Failed to remove loopback alias") + err := r.removeFromRouteTable(prefix, nexthop) + assert.NoError(t, err, "Failed to remove route from table") }) - return "lo0" + return intf } -func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, _ string) { +func addDummyRoute(t *testing.T, dstCIDR string, gw netip.Addr, _ string) { t.Helper() var originalNexthop net.IP @@ -176,12 +204,40 @@ func fetchOriginalGateway() (net.IP, error) { return net.ParseIP(matches[1]), nil } +// setupDummyInterface creates a dummy tun interface for FreeBSD route testing +func setupDummyInterface(t *testing.T) (netip.Addr, *net.Interface) { + t.Helper() + + if runtime.GOOS == "darwin" { + return netip.AddrFrom4([4]byte{192, 168, 1, 2}), &net.Interface{Name: "lo0"} + } + + output, err := exec.Command("ifconfig", "tun", "create").CombinedOutput() + require.NoError(t, err, "Failed to create tun interface: %s", string(output)) + + tunName := strings.TrimSpace(string(output)) + + output, err = exec.Command("ifconfig", tunName, "192.168.1.1", "netmask", "255.255.0.0", "192.168.1.2", "up").CombinedOutput() + require.NoError(t, err, "Failed to configure tun interface: %s", string(output)) + + intf, err := net.InterfaceByName(tunName) + require.NoError(t, err, "Failed to get interface by name") + + t.Cleanup(func() { + if err := exec.Command("ifconfig", tunName, "destroy").Run(); err != nil { + t.Logf("Failed to destroy tun interface %s: %v", tunName, err) + } + }) + + return netip.AddrFrom4([4]byte{192, 168, 1, 2}), intf +} + func setupDummyInterfacesAndRoutes(t *testing.T) { t.Helper() defaultDummy := createAndSetupDummyInterface(t, expectedExternalInt, "192.168.0.1/24") - addDummyRoute(t, "0.0.0.0/0", net.IPv4(192, 168, 0, 1), defaultDummy) + addDummyRoute(t, "0.0.0.0/0", netip.AddrFrom4([4]byte{192, 168, 0, 1}), defaultDummy) otherDummy := createAndSetupDummyInterface(t, expectedInternalInt, "192.168.1.1/24") - addDummyRoute(t, "10.0.0.0/8", net.IPv4(192, 168, 1, 1), otherDummy) + addDummyRoute(t, "10.0.0.0/8", netip.AddrFrom4([4]byte{192, 168, 1, 1}), otherDummy) } diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go index eaef01815..d223a27b2 100644 --- a/client/internal/routemanager/systemops/systemops_generic.go +++ b/client/internal/routemanager/systemops/systemops_generic.go @@ -17,7 +17,6 @@ import ( nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/iface/netstack" - "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/util" "github.com/netbirdio/netbird/client/internal/routemanager/vars" @@ -106,59 +105,15 @@ func (r *SysOps) cleanupRefCounter(stateManager *statemanager.Manager) error { return nil } -// TODO: fix: for default our wg address now appears as the default gw -func (r *SysOps) addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { - addr := netip.IPv4Unspecified() - if prefix.Addr().Is6() { - addr = netip.IPv6Unspecified() - } - - nexthop, err := GetNextHop(addr) - if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { - return fmt.Errorf("get existing route gateway: %s", err) - } - - if !prefix.Contains(nexthop.IP) { - log.Debugf("Skipping adding a new route for gateway %s because it is not in the network %s", nexthop.IP, prefix) - return nil - } - - gatewayPrefix := netip.PrefixFrom(nexthop.IP, 32) - if nexthop.IP.Is6() { - gatewayPrefix = netip.PrefixFrom(nexthop.IP, 128) - } - - ok, err := existsInRouteTable(gatewayPrefix) - if err != nil { - return fmt.Errorf("unable to check if there is an existing route for gateway %s. error: %s", gatewayPrefix, err) - } - - if ok { - log.Debugf("Skipping adding a new route for gateway %s because it already exists", gatewayPrefix) - return nil - } - - nexthop, err = GetNextHop(nexthop.IP) - if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { - return fmt.Errorf("unable to get the next hop for the default gateway address. error: %s", err) - } - - log.Debugf("Adding a new route for gateway %s with next hop %s", gatewayPrefix, nexthop.IP) - return r.addToRouteTable(gatewayPrefix, nexthop) -} - // addRouteToNonVPNIntf adds a new route to the routing table for the given prefix and returns the next hop and interface. // If the next hop or interface is pointing to the VPN interface, it will return the initial values. -func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface, initialNextHop Nexthop) (Nexthop, error) { - addr := prefix.Addr() - switch { - case addr.IsLoopback(), - addr.IsLinkLocalUnicast(), - addr.IsLinkLocalMulticast(), - addr.IsInterfaceLocalMulticast(), - addr.IsUnspecified(), - addr.IsMulticast(): +func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf wgIface, initialNextHop Nexthop) (Nexthop, error) { + if err := r.validateRoute(prefix); err != nil { + return Nexthop{}, err + } + addr := prefix.Addr() + if addr.IsUnspecified() { return Nexthop{}, vars.ErrRouteNotAllowed } @@ -179,10 +134,7 @@ func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface Intf: nexthop.Intf, } - vpnAddr, ok := netip.AddrFromSlice(vpnIntf.Address().IP) - if !ok { - return Nexthop{}, fmt.Errorf("failed to convert vpn address to netip.Addr") - } + vpnAddr := vpnIntf.Address().IP // if next hop is the VPN address or the interface is the VPN interface, we should use the initial values if exitNextHop.IP == vpnAddr || exitNextHop.Intf != nil && exitNextHop.Intf.Name == vpnIntf.Name() { @@ -271,32 +223,7 @@ func (r *SysOps) genericAddVPNRoute(prefix netip.Prefix, intf *net.Interface) er return nil } - return r.addNonExistingRoute(prefix, intf) -} - -// addNonExistingRoute adds a new route to the vpn interface if it doesn't exist in the current routing table -func (r *SysOps) addNonExistingRoute(prefix netip.Prefix, intf *net.Interface) error { - ok, err := existsInRouteTable(prefix) - if err != nil { - return fmt.Errorf("exists in route table: %w", err) - } - if ok { - log.Warnf("Skipping adding a new route for network %s because it already exists", prefix) - return nil - } - - ok, err = isSubRange(prefix) - if err != nil { - return fmt.Errorf("sub range: %w", err) - } - - if ok { - if err := r.addRouteForCurrentDefaultGateway(prefix); err != nil { - log.Warnf("Unable to add route for current default gateway route. Will proceed without it. error: %s", err) - } - } - - return r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}) + return r.addToRouteTable(prefix, nextHop) } // genericRemoveVPNRoute removes the route from the vpn interface. If a default prefix is given, @@ -408,12 +335,8 @@ func GetNextHop(ip netip.Addr) (Nexthop, error) { log.Debugf("Route for %s: interface %v nexthop %v, preferred source %v", ip, intf, gateway, preferredSrc) if gateway == nil { - if runtime.GOOS == "freebsd" { - return Nexthop{Intf: intf}, nil - } - if preferredSrc == nil { - return Nexthop{}, vars.ErrRouteNotFound + return Nexthop{Intf: intf}, nil } log.Debugf("No next hop found for IP %s, using preferred source %s", ip, preferredSrc) @@ -457,32 +380,6 @@ func ipToAddr(ip net.IP, intf *net.Interface) (netip.Addr, error) { return addr.Unmap(), nil } -func existsInRouteTable(prefix netip.Prefix) (bool, error) { - routes, err := GetRoutesFromTable() - if err != nil { - return false, fmt.Errorf("get routes from table: %w", err) - } - for _, tableRoute := range routes { - if tableRoute == prefix { - return true, nil - } - } - return false, nil -} - -func isSubRange(prefix netip.Prefix) (bool, error) { - routes, err := GetRoutesFromTable() - if err != nil { - return false, fmt.Errorf("get routes from table: %w", err) - } - for _, tableRoute := range routes { - if tableRoute.Bits() > vars.MinRangeBits && tableRoute.Contains(prefix.Addr()) && tableRoute.Bits() < prefix.Bits() { - return true, nil - } - } - return false, nil -} - // IsAddrRouted checks if the candidate address would route to the vpn, in which case it returns true and the matched prefix. func IsAddrRouted(addr netip.Addr, vpnRoutes []netip.Prefix) (bool, netip.Prefix) { localRoutes, err := hasSeparateRouting() diff --git a/client/internal/routemanager/systemops/systemops_generic_test.go b/client/internal/routemanager/systemops/systemops_generic_test.go index 5b7b13f97..2a57e6044 100644 --- a/client/internal/routemanager/systemops/systemops_generic_test.go +++ b/client/internal/routemanager/systemops/systemops_generic_test.go @@ -3,23 +3,25 @@ package systemops import ( - "bytes" "context" + "errors" "fmt" "net" "net/netip" - "os" + "os/exec" "runtime" + "strconv" "strings" + "syscall" "testing" "github.com/pion/transport/v3/stdnet" - log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" ) type dialer interface { @@ -27,105 +29,370 @@ type dialer interface { DialContext(ctx context.Context, network, address string) (net.Conn, error) } -func TestAddRemoveRoutes(t *testing.T) { +func TestAddVPNRoute(t *testing.T) { testCases := []struct { - name string - prefix netip.Prefix - shouldRouteToWireguard bool - shouldBeRemoved bool + name string + prefix netip.Prefix + expectError bool }{ { - name: "Should Add And Remove Route 100.66.120.0/24", - prefix: netip.MustParsePrefix("100.66.120.0/24"), - shouldRouteToWireguard: true, - shouldBeRemoved: true, + name: "IPv4 - Private network route", + prefix: netip.MustParsePrefix("10.10.100.0/24"), }, { - name: "Should Not Add Or Remove Route 127.0.0.1/32", - prefix: netip.MustParsePrefix("127.0.0.1/32"), - shouldRouteToWireguard: false, - shouldBeRemoved: false, + name: "IPv4 Single host", + prefix: netip.MustParsePrefix("10.111.111.111/32"), + }, + { + name: "IPv4 RFC3927 test range", + prefix: netip.MustParsePrefix("198.51.100.0/24"), + }, + { + name: "IPv4 Default route", + prefix: netip.MustParsePrefix("0.0.0.0/0"), + }, + + { + name: "IPv6 Subnet", + prefix: netip.MustParsePrefix("fdb1:848a:7e16::/48"), + }, + { + name: "IPv6 Single host", + prefix: netip.MustParsePrefix("fdb1:848a:7e16:a::b/128"), + }, + { + name: "IPv6 Default route", + prefix: netip.MustParsePrefix("::/0"), + }, + + // IPv4 addresses that should be rejected (matches validateRoute logic) + { + name: "IPv4 Loopback", + prefix: netip.MustParsePrefix("127.0.0.1/32"), + expectError: true, + }, + { + name: "IPv4 Link-local unicast", + prefix: netip.MustParsePrefix("169.254.1.1/32"), + expectError: true, + }, + { + name: "IPv4 Link-local multicast", + prefix: netip.MustParsePrefix("224.0.0.251/32"), + expectError: true, + }, + { + name: "IPv4 Multicast", + prefix: netip.MustParsePrefix("239.255.255.250/32"), + expectError: true, + }, + { + name: "IPv4 Unspecified with prefix", + prefix: netip.MustParsePrefix("0.0.0.0/32"), + expectError: true, + }, + + // IPv6 addresses that should be rejected (matches validateRoute logic) + { + name: "IPv6 Loopback", + prefix: netip.MustParsePrefix("::1/128"), + expectError: true, + }, + { + name: "IPv6 Link-local unicast", + prefix: netip.MustParsePrefix("fe80::1/128"), + expectError: true, + }, + { + name: "IPv6 Link-local multicast", + prefix: netip.MustParsePrefix("ff02::1/128"), + expectError: true, + }, + { + name: "IPv6 Interface-local multicast", + prefix: netip.MustParsePrefix("ff01::1/128"), + expectError: true, + }, + { + name: "IPv6 Multicast", + prefix: netip.MustParsePrefix("ff00::1/128"), + expectError: true, + }, + { + name: "IPv6 Unspecified with prefix", + prefix: netip.MustParsePrefix("::/128"), + expectError: true, + }, + + { + name: "IPv4 WireGuard interface network overlap", + prefix: netip.MustParsePrefix("100.65.75.0/24"), + expectError: true, + }, + { + name: "IPv4 WireGuard interface network subnet", + prefix: netip.MustParsePrefix("100.65.75.0/32"), + expectError: true, }, } for n, testCase := range testCases { - // todo resolve test execution on freebsd - if runtime.GOOS == "freebsd" { - t.Skip("skipping ", testCase.name, " on freebsd") - } t.Run(testCase.name, func(t *testing.T) { t.Setenv("NB_DISABLE_ROUTE_CACHE", "true") - peerPrivateKey, _ := wgtypes.GeneratePrivateKey() - newNet, err := stdnet.NewNet() - if err != nil { - t.Fatal(err) - } - opts := iface.WGIFaceOpts{ - IFaceName: fmt.Sprintf("utun53%d", n), - Address: "100.65.75.2/24", - WGPrivKey: peerPrivateKey.String(), - MTU: iface.DefaultMTU, - TransportNet: newNet, - } - wgInterface, err := iface.NewWGIFace(opts) - require.NoError(t, err, "should create testing WGIface interface") - defer wgInterface.Close() - - err = wgInterface.Create() - require.NoError(t, err, "should create testing wireguard interface") + wgInterface := createWGInterface(t, fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100+n) r := NewSysOps(wgInterface, nil) - - _, _, err = r.SetupRouting(nil, nil) + _, _, err := r.SetupRouting(nil, nil) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, r.CleanupRouting(nil)) }) - index, err := net.InterfaceByName(wgInterface.Name()) - require.NoError(t, err, "InterfaceByName should not return err") - intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()} + intf, err := net.InterfaceByName(wgInterface.Name()) + require.NoError(t, err) + // add the route err = r.AddVPNRoute(testCase.prefix, intf) - require.NoError(t, err, "genericAddVPNRoute should not return err") + if testCase.expectError { + assert.ErrorIs(t, err, vars.ErrRouteNotAllowed) + return + } - if testCase.shouldRouteToWireguard { - assertWGOutInterface(t, testCase.prefix, wgInterface, false) + // validate it's pointing to the WireGuard interface + require.NoError(t, err) + + nextHop := getNextHop(t, testCase.prefix.Addr()) + assert.Equal(t, wgInterface.Name(), nextHop.Intf.Name, "next hop interface should be WireGuard interface") + + // remove route again + err = r.RemoveVPNRoute(testCase.prefix, intf) + require.NoError(t, err) + + // validate it's gone + nextHop, err = GetNextHop(testCase.prefix.Addr()) + require.True(t, + errors.Is(err, vars.ErrRouteNotFound) || err == nil && nextHop.Intf != nil && nextHop.Intf.Name != wgInterface.Name(), + "err: %v, next hop: %v", err, nextHop) + }) + } +} + +func getNextHop(t *testing.T, addr netip.Addr) Nexthop { + t.Helper() + + if runtime.GOOS == "windows" || runtime.GOOS == "linux" { + nextHop, err := GetNextHop(addr) + + if runtime.GOOS == "windows" && errors.Is(err, vars.ErrRouteNotFound) && addr.Is6() { + // TODO: Fix this test. It doesn't return the route when running in a windows github runner, but it is + // present in the route table. + t.Skip("Skipping windows test") + } + + require.NoError(t, err) + require.NotNil(t, nextHop.Intf, "next hop interface should not be nil for %s", addr) + + return nextHop + } + // GetNextHop for bsd is buggy and returns the wrong interface for the default route. + + if addr.IsUnspecified() { + // On macOS, querying 0.0.0.0 returns the wrong interface + if addr.Is4() { + addr = netip.MustParseAddr("1.2.3.4") + } else { + addr = netip.MustParseAddr("2001:db8::1") + } + } + + cmd := exec.Command("route", "-n", "get", addr.String()) + if addr.Is6() { + cmd = exec.Command("route", "-n", "get", "-inet6", addr.String()) + } + + output, err := cmd.CombinedOutput() + t.Logf("route output: %s", output) + require.NoError(t, err, "%s failed") + + lines := strings.Split(string(output), "\n") + var intf string + var gateway string + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "interface:") { + intf = strings.TrimSpace(strings.TrimPrefix(line, "interface:")) + } else if strings.HasPrefix(line, "gateway:") { + gateway = strings.TrimSpace(strings.TrimPrefix(line, "gateway:")) + } + } + + require.NotEmpty(t, intf, "interface should be found in route output") + + iface, err := net.InterfaceByName(intf) + require.NoError(t, err, "interface %s should exist", intf) + + nexthop := Nexthop{Intf: iface} + + if gateway != "" && gateway != "link#"+strconv.Itoa(iface.Index) { + addr, err := netip.ParseAddr(gateway) + if err == nil { + nexthop.IP = addr + } + } + + return nexthop +} + +func TestAddRouteToNonVPNIntf(t *testing.T) { + testCases := []struct { + name string + prefix netip.Prefix + expectError bool + errorType error + }{ + { + name: "IPv4 RFC3927 test range", + prefix: netip.MustParsePrefix("198.51.100.0/24"), + }, + { + name: "IPv4 Single host", + prefix: netip.MustParsePrefix("8.8.8.8/32"), + }, + { + name: "IPv6 External network route", + prefix: netip.MustParsePrefix("2001:db8:1000::/48"), + }, + { + name: "IPv6 Single host", + prefix: netip.MustParsePrefix("2001:db8::1/128"), + }, + { + name: "IPv6 Subnet", + prefix: netip.MustParsePrefix("2a05:d014:1f8d::/48"), + }, + { + name: "IPv6 Single host", + prefix: netip.MustParsePrefix("2a05:d014:1f8d:7302:ebca:ec15:b24d:d07e/128"), + }, + + // Addresses that should be rejected + { + name: "IPv4 Loopback", + prefix: netip.MustParsePrefix("127.0.0.1/32"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv4 Link-local unicast", + prefix: netip.MustParsePrefix("169.254.1.1/32"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv4 Multicast", + prefix: netip.MustParsePrefix("239.255.255.250/32"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv4 Unspecified", + prefix: netip.MustParsePrefix("0.0.0.0/0"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv6 Loopback", + prefix: netip.MustParsePrefix("::1/128"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv6 Link-local unicast", + prefix: netip.MustParsePrefix("fe80::1/128"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv6 Multicast", + prefix: netip.MustParsePrefix("ff00::1/128"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv6 Unspecified", + prefix: netip.MustParsePrefix("::/0"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + { + name: "IPv4 WireGuard interface network overlap", + prefix: netip.MustParsePrefix("100.65.75.0/24"), + expectError: true, + errorType: vars.ErrRouteNotAllowed, + }, + } + + for n, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Setenv("NB_DISABLE_ROUTE_CACHE", "true") + + wgInterface := createWGInterface(t, fmt.Sprintf("utun54%d", n), "100.65.75.2/24", 33200+n) + + r := NewSysOps(wgInterface, nil) + _, _, err := r.SetupRouting(nil, nil) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, r.CleanupRouting(nil)) + }) + + initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified()) + require.NoError(t, err, "Should be able to get IPv4 default route") + t.Logf("Initial IPv4 next hop: %s", initialNextHopV4) + + initialNextHopV6, err := GetNextHop(netip.IPv6Unspecified()) + if testCase.prefix.Addr().Is6() && + (errors.Is(err, vars.ErrRouteNotFound) || initialNextHopV6.Intf != nil && strings.HasPrefix(initialNextHopV6.Intf.Name, "utun")) { + t.Skip("Skipping test as no ipv6 default route is available") + } + if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { + t.Fatalf("Failed to get IPv6 default route: %v", err) + } + + var initialNextHop Nexthop + if testCase.prefix.Addr().Is6() { + initialNextHop = initialNextHopV6 } else { - assertWGOutInterface(t, testCase.prefix, wgInterface, true) + initialNextHop = initialNextHopV4 } - exists, err := existsInRouteTable(testCase.prefix) - require.NoError(t, err, "existsInRouteTable should not return err") - if exists && testCase.shouldRouteToWireguard { - err = r.RemoveVPNRoute(testCase.prefix, intf) - require.NoError(t, err, "genericRemoveVPNRoute should not return err") - prefixNexthop, err := GetNextHop(testCase.prefix.Addr()) - require.NoError(t, err, "GetNextHop should not return err") + nexthop, err := r.addRouteToNonVPNIntf(testCase.prefix, wgInterface, initialNextHop) - internetNexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) - require.NoError(t, err) - - if testCase.shouldBeRemoved { - require.Equal(t, internetNexthop.IP, prefixNexthop.IP, "route should be pointing to default internet gateway") - } else { - require.NotEqual(t, internetNexthop.IP, prefixNexthop.IP, "route should be pointing to a different gateway than the internet gateway") - } + if testCase.expectError { + require.ErrorIs(t, err, vars.ErrRouteNotAllowed) + return } + require.NoError(t, err) + t.Logf("Next hop for %s: %s", testCase.prefix, nexthop) + + // Verify the route was added and points to non-VPN interface + currentNextHop, err := GetNextHop(testCase.prefix.Addr()) + require.NoError(t, err) + assert.NotEqual(t, wgInterface.Name(), currentNextHop.Intf.Name, "Route should not point to VPN interface") + + err = r.removeFromRouteTable(testCase.prefix, nexthop) + assert.NoError(t, err) }) } } func TestGetNextHop(t *testing.T) { - if runtime.GOOS == "freebsd" { - t.Skip("skipping on freebsd") - } - nexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) + defaultNh, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) if err != nil { t.Fatal("shouldn't return error when fetching the gateway: ", err) } - if !nexthop.IP.IsValid() { + if !defaultNh.IP.IsValid() { t.Fatal("should return a gateway") } addresses, err := net.InterfaceAddrs() @@ -133,7 +400,6 @@ func TestGetNextHop(t *testing.T) { t.Fatal("shouldn't return error when fetching interface addresses: ", err) } - var testingIP string var testingPrefix netip.Prefix for _, address := range addresses { if address.Network() != "ip+net" { @@ -141,213 +407,23 @@ func TestGetNextHop(t *testing.T) { } prefix := netip.MustParsePrefix(address.String()) if !prefix.Addr().IsLoopback() && prefix.Addr().Is4() { - testingIP = prefix.Addr().String() testingPrefix = prefix.Masked() break } } - localIP, err := GetNextHop(testingPrefix.Addr()) + nh, err := GetNextHop(testingPrefix.Addr()) if err != nil { t.Fatal("shouldn't return error: ", err) } - if !localIP.IP.IsValid() { + if nh.Intf == nil { t.Fatal("should return a gateway for local network") } - if localIP.IP.String() == nexthop.IP.String() { - t.Fatal("local IP should not match with gateway IP") + if nh.IP.String() == defaultNh.IP.String() { + t.Fatal("next hop IP should not match with default gateway IP") } - if localIP.IP.String() != testingIP { - t.Fatalf("local IP should match with testing IP: want %s got %s", testingIP, localIP.IP.String()) - } -} - -func TestAddExistAndRemoveRoute(t *testing.T) { - defaultNexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) - t.Log("defaultNexthop: ", defaultNexthop) - if err != nil { - t.Fatal("shouldn't return error when fetching the gateway: ", err) - } - testCases := []struct { - name string - prefix netip.Prefix - preExistingPrefix netip.Prefix - shouldAddRoute bool - }{ - { - name: "Should Add And Remove random Route", - prefix: netip.MustParsePrefix("99.99.99.99/32"), - shouldAddRoute: true, - }, - { - name: "Should Not Add Route if overlaps with default gateway", - prefix: netip.MustParsePrefix(defaultNexthop.IP.String() + "/31"), - shouldAddRoute: false, - }, - { - name: "Should Add Route if bigger network exists", - prefix: netip.MustParsePrefix("100.100.100.0/24"), - preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"), - shouldAddRoute: true, - }, - { - name: "Should Add Route if smaller network exists", - prefix: netip.MustParsePrefix("100.100.0.0/16"), - preExistingPrefix: netip.MustParsePrefix("100.100.100.0/24"), - shouldAddRoute: true, - }, - { - name: "Should Not Add Route if same network exists", - prefix: netip.MustParsePrefix("100.100.0.0/16"), - preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"), - shouldAddRoute: false, - }, - } - - for n, testCase := range testCases { - - var buf bytes.Buffer - log.SetOutput(&buf) - defer func() { - log.SetOutput(os.Stderr) - }() - t.Run(testCase.name, func(t *testing.T) { - t.Setenv("NB_USE_LEGACY_ROUTING", "true") - t.Setenv("NB_DISABLE_ROUTE_CACHE", "true") - - peerPrivateKey, _ := wgtypes.GeneratePrivateKey() - newNet, err := stdnet.NewNet() - if err != nil { - t.Fatal(err) - } - opts := iface.WGIFaceOpts{ - IFaceName: fmt.Sprintf("utun53%d", n), - Address: "100.65.75.2/24", - WGPort: 33100, - WGPrivKey: peerPrivateKey.String(), - MTU: iface.DefaultMTU, - TransportNet: newNet, - } - wgInterface, err := iface.NewWGIFace(opts) - require.NoError(t, err, "should create testing WGIface interface") - defer wgInterface.Close() - - err = wgInterface.Create() - require.NoError(t, err, "should create testing wireguard interface") - - index, err := net.InterfaceByName(wgInterface.Name()) - require.NoError(t, err, "InterfaceByName should not return err") - intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()} - - r := NewSysOps(wgInterface, nil) - - // Prepare the environment - if testCase.preExistingPrefix.IsValid() { - err := r.AddVPNRoute(testCase.preExistingPrefix, intf) - require.NoError(t, err, "should not return err when adding pre-existing route") - } - - // Add the route - err = r.AddVPNRoute(testCase.prefix, intf) - require.NoError(t, err, "should not return err when adding route") - - if testCase.shouldAddRoute { - // test if route exists after adding - ok, err := existsInRouteTable(testCase.prefix) - require.NoError(t, err, "should not return err") - require.True(t, ok, "route should exist") - - // remove route again if added - err = r.RemoveVPNRoute(testCase.prefix, intf) - require.NoError(t, err, "should not return err") - } - - // route should either not have been added or should have been removed - // In case of already existing route, it should not have been added (but still exist) - ok, err := existsInRouteTable(testCase.prefix) - t.Log("Buffer string: ", buf.String()) - require.NoError(t, err, "should not return err") - - if !strings.Contains(buf.String(), "because it already exists") { - require.False(t, ok, "route should not exist") - } - }) - } -} - -func TestIsSubRange(t *testing.T) { - addresses, err := net.InterfaceAddrs() - if err != nil { - t.Fatal("shouldn't return error when fetching interface addresses: ", err) - } - - var subRangeAddressPrefixes []netip.Prefix - var nonSubRangeAddressPrefixes []netip.Prefix - for _, address := range addresses { - p := netip.MustParsePrefix(address.String()) - if !p.Addr().IsLoopback() && p.Addr().Is4() && p.Bits() < 32 { - p2 := netip.PrefixFrom(p.Masked().Addr(), p.Bits()+1) - subRangeAddressPrefixes = append(subRangeAddressPrefixes, p2) - nonSubRangeAddressPrefixes = append(nonSubRangeAddressPrefixes, p.Masked()) - } - } - - for _, prefix := range subRangeAddressPrefixes { - isSubRangePrefix, err := isSubRange(prefix) - if err != nil { - t.Fatal("shouldn't return error when checking if address is sub-range: ", err) - } - if !isSubRangePrefix { - t.Fatalf("address %s should be sub-range of an existing route in the table", prefix) - } - } - - for _, prefix := range nonSubRangeAddressPrefixes { - isSubRangePrefix, err := isSubRange(prefix) - if err != nil { - t.Fatal("shouldn't return error when checking if address is sub-range: ", err) - } - if isSubRangePrefix { - t.Fatalf("address %s should not be sub-range of an existing route in the table", prefix) - } - } -} - -func TestExistsInRouteTable(t *testing.T) { - addresses, err := net.InterfaceAddrs() - if err != nil { - t.Fatal("shouldn't return error when fetching interface addresses: ", err) - } - - var addressPrefixes []netip.Prefix - for _, address := range addresses { - p := netip.MustParsePrefix(address.String()) - - switch { - case p.Addr().Is6(): - continue - // Windows sometimes has hidden interface link local addrs that don't turn up on any interface - case runtime.GOOS == "windows" && p.Addr().IsLinkLocalUnicast(): - continue - // Linux loopback 127/8 is in the local table, not in the main table and always takes precedence - case runtime.GOOS == "linux" && p.Addr().IsLoopback(): - continue - // FreeBSD loopback 127/8 is not added to the routing table - case runtime.GOOS == "freebsd" && p.Addr().IsLoopback(): - continue - default: - addressPrefixes = append(addressPrefixes, p.Masked()) - } - } - - for _, prefix := range addressPrefixes { - exists, err := existsInRouteTable(prefix) - if err != nil { - t.Fatal("shouldn't return error when checking if address exists in route table: ", err) - } - if !exists { - t.Fatalf("address %s should exist in route table", prefix) - } + if nh.Intf.Name != defaultNh.Intf.Name { + t.Fatalf("next hop interface name should match with default gateway interface name, got: %s, want: %s", nh.Intf.Name, defaultNh.Intf.Name) } } @@ -384,11 +460,16 @@ func createWGInterface(t *testing.T, interfaceName, ipAddressCIDR string, listen func setupRouteAndCleanup(t *testing.T, r *SysOps, prefix netip.Prefix, intf *net.Interface) { t.Helper() - err := r.AddVPNRoute(prefix, intf) - require.NoError(t, err, "addVPNRoute should not return err") + if err := r.AddVPNRoute(prefix, intf); err != nil { + if !errors.Is(err, syscall.EEXIST) && !errors.Is(err, vars.ErrRouteNotAllowed) { + t.Fatalf("addVPNRoute should not return err: %v", err) + } + t.Logf("addVPNRoute %v returned: %v", prefix, err) + } t.Cleanup(func() { - err = r.RemoveVPNRoute(prefix, intf) - assert.NoError(t, err, "removeVPNRoute should not return err") + if err := r.RemoveVPNRoute(prefix, intf); err != nil && !errors.Is(err, vars.ErrRouteNotAllowed) { + t.Fatalf("removeVPNRoute should not return err: %v", err) + } }) } @@ -422,28 +503,10 @@ func setupTestEnv(t *testing.T) { // 10.10.0.0/24 more specific route exists in vpn table setupRouteAndCleanup(t, r, netip.MustParsePrefix("10.10.0.0/24"), intf) - // 127.0.10.0/24 more specific route exists in vpn table - setupRouteAndCleanup(t, r, netip.MustParsePrefix("127.0.10.0/24"), intf) - // unique route in vpn table setupRouteAndCleanup(t, r, netip.MustParsePrefix("172.16.0.0/12"), intf) } -func assertWGOutInterface(t *testing.T, prefix netip.Prefix, wgIface *iface.WGIface, invert bool) { - t.Helper() - if runtime.GOOS == "linux" && prefix.Addr().IsLoopback() { - return - } - - prefixNexthop, err := GetNextHop(prefix.Addr()) - require.NoError(t, err, "GetNextHop should not return err") - if invert { - assert.NotEqual(t, wgIface.Address().IP.String(), prefixNexthop.IP.String(), "route should not point to wireguard interface IP") - } else { - assert.Equal(t, wgIface.Address().IP.String(), prefixNexthop.IP.String(), "route should point to wireguard interface IP") - } -} - func TestIsVpnRoute(t *testing.T) { tests := []struct { name string diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index 59b6346c6..b48cfa242 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -149,6 +149,10 @@ func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) erro } func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + if err := r.validateRoute(prefix); err != nil { + return err + } + if !nbnet.AdvancedRouting() { return r.genericAddVPNRoute(prefix, intf) } @@ -172,6 +176,10 @@ func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { } func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + if err := r.validateRoute(prefix); err != nil { + return err + } + if !nbnet.AdvancedRouting() { return r.genericRemoveVPNRoute(prefix, intf) } @@ -219,7 +227,7 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) { ones, _ := route.Dst.Mask.Size() - prefix := netip.PrefixFrom(addr, ones) + prefix := netip.PrefixFrom(addr.Unmap(), ones) if prefix.IsValid() { prefixList = append(prefixList, prefix) } @@ -247,7 +255,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { return fmt.Errorf("add gateway and device: %w", err) } - if err := netlink.RouteAdd(route); err != nil && !errors.Is(err, syscall.EEXIST) && !isOpErr(err) { + if err := netlink.RouteAdd(route); err != nil && !isOpErr(err) { return fmt.Errorf("netlink add route: %w", err) } @@ -270,7 +278,7 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error { Dst: ipNet, } - if err := netlink.RouteAdd(route); err != nil && !errors.Is(err, syscall.EEXIST) && !isOpErr(err) { + if err := netlink.RouteAdd(route); err != nil && !isOpErr(err) { return fmt.Errorf("netlink add unreachable route: %w", err) } diff --git a/client/internal/routemanager/systemops/systemops_linux_test.go b/client/internal/routemanager/systemops/systemops_linux_test.go index f0d7472dc..880296d91 100644 --- a/client/internal/routemanager/systemops/systemops_linux_test.go +++ b/client/internal/routemanager/systemops/systemops_linux_test.go @@ -19,7 +19,6 @@ import ( ) var expectedVPNint = "wgtest0" -var expectedLoopbackInt = "lo" var expectedExternalInt = "dummyext0" var expectedInternalInt = "dummyint0" @@ -31,12 +30,6 @@ func init() { dialer: &net.Dialer{}, expectedPacket: createPacketExpectation("192.168.1.1", 12345, "10.10.0.2", 53), }, - { - name: "To more specific route (local) without custom dialer via physical interface", - expectedInterface: expectedLoopbackInt, - dialer: &net.Dialer{}, - expectedPacket: createPacketExpectation("127.0.0.1", 12345, "127.0.10.1", 53), - }, }...) } diff --git a/client/internal/routemanager/systemops/systemops_nonlinux.go b/client/internal/routemanager/systemops/systemops_nonlinux.go index 3b52fc7af..59581255f 100644 --- a/client/internal/routemanager/systemops/systemops_nonlinux.go +++ b/client/internal/routemanager/systemops/systemops_nonlinux.go @@ -11,10 +11,16 @@ import ( ) func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + if err := r.validateRoute(prefix); err != nil { + return err + } return r.genericAddVPNRoute(prefix, intf) } func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + if err := r.validateRoute(prefix); err != nil { + return err + } return r.genericRemoveVPNRoute(prefix, intf) } diff --git a/client/internal/routemanager/systemops/systemops_test.go b/client/internal/routemanager/systemops/systemops_test.go new file mode 100644 index 000000000..1d1f78830 --- /dev/null +++ b/client/internal/routemanager/systemops/systemops_test.go @@ -0,0 +1,268 @@ +package systemops + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/internal/routemanager/notifier" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" +) + +type mockWGIface struct { + address wgaddr.Address + name string +} + +func (m *mockWGIface) Address() wgaddr.Address { + return m.address +} + +func (m *mockWGIface) Name() string { + return m.name +} + +func TestSysOps_validateRoute(t *testing.T) { + wgNetwork := netip.MustParsePrefix("10.0.0.0/24") + mockWG := &mockWGIface{ + address: wgaddr.Address{ + IP: wgNetwork.Addr(), + Network: wgNetwork, + }, + name: "wg0", + } + + sysOps := &SysOps{ + wgInterface: mockWG, + notifier: ¬ifier.Notifier{}, + } + + tests := []struct { + name string + prefix string + expectError bool + }{ + // Valid routes + { + name: "valid IPv4 route", + prefix: "192.168.1.0/24", + expectError: false, + }, + { + name: "valid IPv6 route", + prefix: "2001:db8::/32", + expectError: false, + }, + { + name: "valid single IPv4 host", + prefix: "8.8.8.8/32", + expectError: false, + }, + { + name: "valid single IPv6 host", + prefix: "2001:4860:4860::8888/128", + expectError: false, + }, + + // Invalid routes - loopback + { + name: "IPv4 loopback", + prefix: "127.0.0.1/32", + expectError: true, + }, + { + name: "IPv6 loopback", + prefix: "::1/128", + expectError: true, + }, + + // Invalid routes - link-local unicast + { + name: "IPv4 link-local unicast", + prefix: "169.254.1.1/32", + expectError: true, + }, + { + name: "IPv6 link-local unicast", + prefix: "fe80::1/128", + expectError: true, + }, + + // Invalid routes - multicast + { + name: "IPv4 multicast", + prefix: "224.0.0.1/32", + expectError: true, + }, + { + name: "IPv6 multicast", + prefix: "ff02::1/128", + expectError: true, + }, + + // Invalid routes - link-local multicast + { + name: "IPv4 link-local multicast", + prefix: "224.0.0.0/24", + expectError: true, + }, + { + name: "IPv6 link-local multicast", + prefix: "ff02::/16", + expectError: true, + }, + + // Invalid routes - interface-local multicast (IPv6 only) + { + name: "IPv6 interface-local multicast", + prefix: "ff01::1/128", + expectError: true, + }, + + // Invalid routes - overlaps with WG interface network + { + name: "overlaps with WG network - exact match", + prefix: "10.0.0.0/24", + expectError: true, + }, + { + name: "overlaps with WG network - subset", + prefix: "10.0.0.1/32", + expectError: true, + }, + { + name: "overlaps with WG network - host in range", + prefix: "10.0.0.100/32", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prefix, err := netip.ParsePrefix(tt.prefix) + require.NoError(t, err, "Failed to parse test prefix %s", tt.prefix) + + err = sysOps.validateRoute(prefix) + + if tt.expectError { + require.Error(t, err, "validateRoute() expected error for %s", tt.prefix) + assert.Equal(t, vars.ErrRouteNotAllowed, err, "validateRoute() expected ErrRouteNotAllowed for %s", tt.prefix) + } else { + assert.NoError(t, err, "validateRoute() expected no error for %s", tt.prefix) + } + }) + } +} + +func TestSysOps_validateRoute_SubnetOverlap(t *testing.T) { + wgNetwork := netip.MustParsePrefix("192.168.100.0/24") + mockWG := &mockWGIface{ + address: wgaddr.Address{ + IP: wgNetwork.Addr(), + Network: wgNetwork, + }, + name: "wg0", + } + + sysOps := &SysOps{ + wgInterface: mockWG, + notifier: ¬ifier.Notifier{}, + } + + tests := []struct { + name string + prefix string + expectError bool + description string + }{ + { + name: "identical subnet", + prefix: "192.168.100.0/24", + expectError: true, + description: "exact same network as WG interface", + }, + { + name: "broader subnet containing WG network", + prefix: "192.168.0.0/16", + expectError: false, + description: "broader network that contains WG network should be allowed", + }, + { + name: "host within WG network", + prefix: "192.168.100.50/32", + expectError: true, + description: "specific host within WG network", + }, + { + name: "subnet within WG network", + prefix: "192.168.100.128/25", + expectError: true, + description: "smaller subnet within WG network", + }, + { + name: "adjacent subnet - same /23", + prefix: "192.168.101.0/24", + expectError: false, + description: "adjacent subnet, no overlap", + }, + { + name: "adjacent subnet - different /16", + prefix: "192.167.100.0/24", + expectError: false, + description: "different network, no overlap", + }, + { + name: "WG network broadcast address", + prefix: "192.168.100.255/32", + expectError: true, + description: "broadcast address of WG network", + }, + { + name: "WG network first usable", + prefix: "192.168.100.1/32", + expectError: true, + description: "first usable address in WG network", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prefix, err := netip.ParsePrefix(tt.prefix) + require.NoError(t, err, "Failed to parse test prefix %s", tt.prefix) + + err = sysOps.validateRoute(prefix) + + if tt.expectError { + require.Error(t, err, "validateRoute() expected error for %s (%s)", tt.prefix, tt.description) + assert.Equal(t, vars.ErrRouteNotAllowed, err, "validateRoute() expected ErrRouteNotAllowed for %s (%s)", tt.prefix, tt.description) + } else { + assert.NoError(t, err, "validateRoute() expected no error for %s (%s)", tt.prefix, tt.description) + } + }) + } +} + +func TestSysOps_validateRoute_InvalidPrefix(t *testing.T) { + wgNetwork := netip.MustParsePrefix("10.0.0.0/24") + mockWG := &mockWGIface{ + address: wgaddr.Address{ + IP: wgNetwork.Addr(), + Network: wgNetwork, + }, + name: "wt0", + } + + sysOps := &SysOps{ + wgInterface: mockWG, + notifier: ¬ifier.Notifier{}, + } + + var invalidPrefix netip.Prefix + err := sysOps.validateRoute(invalidPrefix) + + require.Error(t, err, "validateRoute() expected error for invalid prefix") + assert.Equal(t, vars.ErrRouteNotAllowed, err, "validateRoute() expected ErrRouteNotAllowed for invalid prefix") +} diff --git a/client/internal/routemanager/systemops/systemops_unix.go b/client/internal/routemanager/systemops/systemops_unix.go index 0f8f2a341..f284e131b 100644 --- a/client/internal/routemanager/systemops/systemops_unix.go +++ b/client/internal/routemanager/systemops/systemops_unix.go @@ -3,15 +3,19 @@ package systemops import ( + "errors" "fmt" "net" "net/netip" - "os/exec" - "strings" + "strconv" + "syscall" "time" + "unsafe" "github.com/cenkalti/backoff/v4" log "github.com/sirupsen/logrus" + "golang.org/x/net/route" + "golang.org/x/sys/unix" "github.com/netbirdio/netbird/client/internal/statemanager" nbnet "github.com/netbirdio/netbird/util/net" @@ -26,48 +30,16 @@ func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error { } func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { - return r.routeCmd("add", prefix, nexthop) + return r.routeSocket(unix.RTM_ADD, prefix, nexthop) } func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { - return r.routeCmd("delete", prefix, nexthop) + return r.routeSocket(unix.RTM_DELETE, prefix, nexthop) } -func (r *SysOps) routeCmd(action string, prefix netip.Prefix, nexthop Nexthop) error { - inet := "-inet" - if prefix.Addr().Is6() { - inet = "-inet6" - } - - network := prefix.String() - if prefix.IsSingleIP() { - network = prefix.Addr().String() - } - - args := []string{"-n", action, inet, network} - if nexthop.IP.IsValid() { - args = append(args, nexthop.IP.Unmap().String()) - } else if nexthop.Intf != nil { - args = append(args, "-interface", nexthop.Intf.Name) - } - - if err := retryRouteCmd(args); err != nil { - return fmt.Errorf("failed to %s route for %s: %w", action, prefix, err) - } - return nil -} - -func retryRouteCmd(args []string) error { - operation := func() error { - out, err := exec.Command("route", args...).CombinedOutput() - log.Tracef("route %s: %s", strings.Join(args, " "), out) - // https://github.com/golang/go/issues/45736 - if err != nil && strings.Contains(string(out), "sysctl: cannot allocate memory") { - return err - } else if err != nil { - return backoff.Permanent(err) - } - return nil +func (r *SysOps) routeSocket(action int, prefix netip.Prefix, nexthop Nexthop) error { + if !prefix.IsValid() { + return fmt.Errorf("invalid prefix: %s", prefix) } expBackOff := backoff.NewExponentialBackOff() @@ -75,9 +47,157 @@ func retryRouteCmd(args []string) error { expBackOff.MaxInterval = 500 * time.Millisecond expBackOff.MaxElapsedTime = 1 * time.Second - err := backoff.Retry(operation, expBackOff) - if err != nil { - return fmt.Errorf("route cmd retry failed: %w", err) + if err := backoff.Retry(r.routeOp(action, prefix, nexthop), expBackOff); err != nil { + a := "add" + if action == unix.RTM_DELETE { + a = "remove" + } + return fmt.Errorf("%s route for %s: %w", a, prefix, err) } return nil } + +func (r *SysOps) routeOp(action int, prefix netip.Prefix, nexthop Nexthop) func() error { + operation := func() error { + fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC) + if err != nil { + return fmt.Errorf("open routing socket: %w", err) + } + defer func() { + if err := unix.Close(fd); err != nil && !errors.Is(err, unix.EBADF) { + log.Warnf("failed to close routing socket: %v", err) + } + }() + + msg, err := r.buildRouteMessage(action, prefix, nexthop) + if err != nil { + return backoff.Permanent(fmt.Errorf("build route message: %w", err)) + } + + msgBytes, err := msg.Marshal() + if err != nil { + return backoff.Permanent(fmt.Errorf("marshal route message: %w", err)) + } + + if _, err = unix.Write(fd, msgBytes); err != nil { + if errors.Is(err, unix.ENOBUFS) || errors.Is(err, unix.EAGAIN) { + return fmt.Errorf("write: %w", err) + } + return backoff.Permanent(fmt.Errorf("write: %w", err)) + } + + respBuf := make([]byte, 2048) + n, err := unix.Read(fd, respBuf) + if err != nil { + return backoff.Permanent(fmt.Errorf("read route response: %w", err)) + } + + if n > 0 { + if err := r.parseRouteResponse(respBuf[:n]); err != nil { + return backoff.Permanent(err) + } + } + + return nil + } + return operation +} + +func (r *SysOps) buildRouteMessage(action int, prefix netip.Prefix, nexthop Nexthop) (msg *route.RouteMessage, err error) { + msg = &route.RouteMessage{ + Type: action, + Flags: unix.RTF_UP, + Version: unix.RTM_VERSION, + Seq: 1, + } + + const numAddrs = unix.RTAX_NETMASK + 1 + addrs := make([]route.Addr, numAddrs) + + addrs[unix.RTAX_DST], err = addrToRouteAddr(prefix.Addr()) + if err != nil { + return nil, fmt.Errorf("build destination address for %s: %w", prefix.Addr(), err) + } + + if prefix.IsSingleIP() { + msg.Flags |= unix.RTF_HOST + } else { + addrs[unix.RTAX_NETMASK], err = prefixToRouteNetmask(prefix) + if err != nil { + return nil, fmt.Errorf("build netmask for %s: %w", prefix, err) + } + } + + if nexthop.IP.IsValid() { + msg.Flags |= unix.RTF_GATEWAY + addrs[unix.RTAX_GATEWAY], err = addrToRouteAddr(nexthop.IP.Unmap()) + if err != nil { + return nil, fmt.Errorf("build gateway IP address for %s: %w", nexthop.IP, err) + } + } else if nexthop.Intf != nil { + msg.Index = nexthop.Intf.Index + addrs[unix.RTAX_GATEWAY] = &route.LinkAddr{ + Index: nexthop.Intf.Index, + Name: nexthop.Intf.Name, + } + } + + msg.Addrs = addrs + return msg, nil +} + +func (r *SysOps) parseRouteResponse(buf []byte) error { + if len(buf) < int(unsafe.Sizeof(unix.RtMsghdr{})) { + return nil + } + + rtMsg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0])) + if rtMsg.Errno != 0 { + return fmt.Errorf("parse: %d", rtMsg.Errno) + } + + return nil +} + +// addrToRouteAddr converts a netip.Addr to the appropriate route.Addr (*route.Inet4Addr or *route.Inet6Addr). +func addrToRouteAddr(addr netip.Addr) (route.Addr, error) { + if addr.Is4() { + return &route.Inet4Addr{IP: addr.As4()}, nil + } + + if addr.Zone() == "" { + return &route.Inet6Addr{IP: addr.As16()}, nil + } + + var zone int + // zone can be either a numeric zone ID or an interface name. + if z, err := strconv.Atoi(addr.Zone()); err == nil { + zone = z + } else { + iface, err := net.InterfaceByName(addr.Zone()) + if err != nil { + return nil, fmt.Errorf("resolve zone '%s': %w", addr.Zone(), err) + } + zone = iface.Index + } + return &route.Inet6Addr{IP: addr.As16(), ZoneID: zone}, nil +} + +func prefixToRouteNetmask(prefix netip.Prefix) (route.Addr, error) { + bits := prefix.Bits() + if prefix.Addr().Is4() { + m := net.CIDRMask(bits, 32) + var maskBytes [4]byte + copy(maskBytes[:], m) + return &route.Inet4Addr{IP: maskBytes}, nil + } + + if prefix.Addr().Is6() { + m := net.CIDRMask(bits, 128) + var maskBytes [16]byte + copy(maskBytes[:], m) + return &route.Inet6Addr{IP: maskBytes}, nil + } + + return nil, fmt.Errorf("unknown IP version in prefix: %s", prefix.Addr().String()) +} diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go index f66161595..11eaa435e 100644 --- a/client/internal/routemanager/systemops/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -1,5 +1,3 @@ -//go:build windows - package systemops import ( @@ -9,9 +7,8 @@ import ( "net" "net/netip" "os" - "os/exec" + "runtime/debug" "strconv" - "strings" "sync" "syscall" "time" @@ -21,11 +18,12 @@ import ( "github.com/yusufpapurcu/wmi" "golang.org/x/sys/windows" - "github.com/netbirdio/netbird/client/firewall/uspfilter" "github.com/netbirdio/netbird/client/internal/statemanager" nbnet "github.com/netbirdio/netbird/util/net" ) +const InfiniteLifetime = 0xffffffff + type RouteUpdateType int // RouteUpdate represents a change in the routing table. @@ -58,9 +56,13 @@ type MSFT_NetRoute struct { AddressFamily uint16 } -// MIB_IPFORWARD_ROW2 is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2 +// luid represents a locally unique identifier for network interfaces +type luid uint64 + +// MIB_IPFORWARD_ROW2 represents a route entry in the routing table. +// It is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2 type MIB_IPFORWARD_ROW2 struct { - InterfaceLuid uint64 + InterfaceLuid luid InterfaceIndex uint32 DestinationPrefix IP_ADDRESS_PREFIX NextHop SOCKADDR_INET_NEXTHOP @@ -108,9 +110,14 @@ type SOCKADDR_INET_NEXTHOP struct { type MIB_NOTIFICATION_TYPE int32 var ( - modiphlpapi = windows.NewLazyDLL("iphlpapi.dll") - procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2") - procCancelMibChangeNotify2 = modiphlpapi.NewProc("CancelMibChangeNotify2") + modiphlpapi = windows.NewLazyDLL("iphlpapi.dll") + procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2") + procCancelMibChangeNotify2 = modiphlpapi.NewProc("CancelMibChangeNotify2") + procCreateIpForwardEntry2 = modiphlpapi.NewProc("CreateIpForwardEntry2") + procDeleteIpForwardEntry2 = modiphlpapi.NewProc("DeleteIpForwardEntry2") + procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2") + procInitializeIpForwardEntry = modiphlpapi.NewProc("InitializeIpForwardEntry") + procConvertInterfaceIndexToLuid = modiphlpapi.NewProc("ConvertInterfaceIndexToLuid") prefixList []netip.Prefix lastUpdate time.Time @@ -139,6 +146,8 @@ func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error { } func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + log.Debugf("Adding route to %s via %s", prefix, nexthop) + // if we don't have an interface but a zone, extract the interface index from the zone if nexthop.IP.Zone() != "" && nexthop.Intf == nil { zone, err := strconv.Atoi(nexthop.IP.Zone()) if err != nil { @@ -147,23 +156,187 @@ func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { nexthop.Intf = &net.Interface{Index: zone} } - return addRouteCmd(prefix, nexthop) + return addRoute(prefix, nexthop) } func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { - args := []string{"delete", prefix.String()} - if nexthop.IP.IsValid() { - ip := nexthop.IP.WithZone("") - args = append(args, ip.Unmap().String()) + log.Debugf("Removing route to %s via %s", prefix, nexthop) + return deleteRoute(prefix, nexthop) +} + +// setupRouteEntry prepares a route entry with common configuration +func setupRouteEntry(prefix netip.Prefix, nexthop Nexthop) (*MIB_IPFORWARD_ROW2, error) { + route := &MIB_IPFORWARD_ROW2{} + + initializeIPForwardEntry(route) + + // Convert interface index to luid if interface is specified + if nexthop.Intf != nil { + var luid luid + if err := convertInterfaceIndexToLUID(uint32(nexthop.Intf.Index), &luid); err != nil { + return nil, fmt.Errorf("convert interface index to luid: %w", err) + } + route.InterfaceLuid = luid + route.InterfaceIndex = uint32(nexthop.Intf.Index) } - routeCmd := uspfilter.GetSystem32Command("route") + if err := setDestinationPrefix(&route.DestinationPrefix, prefix); err != nil { + return nil, fmt.Errorf("set destination prefix: %w", err) + } - out, err := exec.Command(routeCmd, args...).CombinedOutput() - log.Tracef("route %s: %s", strings.Join(args, " "), out) + if nexthop.IP.IsValid() { + if err := setNextHop(&route.NextHop, nexthop.IP); err != nil { + return nil, fmt.Errorf("set next hop: %w", err) + } + } - if err != nil { - return fmt.Errorf("remove route: %w", err) + return route, nil +} + +// addRoute adds a route using Windows iphelper APIs +func addRoute(prefix netip.Prefix, nexthop Nexthop) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic in addRoute: %v, stack trace: %s", r, debug.Stack()) + } + }() + + route, setupErr := setupRouteEntry(prefix, nexthop) + if setupErr != nil { + return fmt.Errorf("setup route entry: %w", setupErr) + } + + route.Metric = 1 + route.ValidLifetime = InfiniteLifetime + route.PreferredLifetime = InfiniteLifetime + + return createIPForwardEntry2(route) +} + +// deleteRoute deletes a route using Windows iphelper APIs +func deleteRoute(prefix netip.Prefix, nexthop Nexthop) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic in deleteRoute: %v, stack trace: %s", r, debug.Stack()) + } + }() + + route, setupErr := setupRouteEntry(prefix, nexthop) + if setupErr != nil { + return fmt.Errorf("setup route entry: %w", setupErr) + } + + if err := getIPForwardEntry2(route); err != nil { + return fmt.Errorf("get route entry: %w", err) + } + + return deleteIPForwardEntry2(route) +} + +// setDestinationPrefix sets the destination prefix in the route structure +func setDestinationPrefix(prefix *IP_ADDRESS_PREFIX, dest netip.Prefix) error { + addr := dest.Addr() + prefix.PrefixLength = uint8(dest.Bits()) + + if addr.Is4() { + prefix.Prefix.sin6_family = windows.AF_INET + ip4 := addr.As4() + binary.BigEndian.PutUint32(prefix.Prefix.data[:4], + uint32(ip4[0])<<24|uint32(ip4[1])<<16|uint32(ip4[2])<<8|uint32(ip4[3])) + return nil + } + + if addr.Is6() { + prefix.Prefix.sin6_family = windows.AF_INET6 + ip6 := addr.As16() + copy(prefix.Prefix.data[4:20], ip6[:]) + + if zone := addr.Zone(); zone != "" { + if scopeID, err := strconv.ParseUint(zone, 10, 32); err == nil { + binary.BigEndian.PutUint32(prefix.Prefix.data[20:24], uint32(scopeID)) + } + } + return nil + } + + return fmt.Errorf("invalid address family") +} + +// setNextHop sets the next hop address in the route structure +func setNextHop(nextHop *SOCKADDR_INET_NEXTHOP, addr netip.Addr) error { + if addr.Is4() { + nextHop.sin6_family = windows.AF_INET + ip4 := addr.As4() + binary.BigEndian.PutUint32(nextHop.data[:4], + uint32(ip4[0])<<24|uint32(ip4[1])<<16|uint32(ip4[2])<<8|uint32(ip4[3])) + return nil + } + + if addr.Is6() { + nextHop.sin6_family = windows.AF_INET6 + ip6 := addr.As16() + copy(nextHop.data[4:20], ip6[:]) + + // Handle zone if present + if zone := addr.Zone(); zone != "" { + if scopeID, err := strconv.ParseUint(zone, 10, 32); err == nil { + binary.BigEndian.PutUint32(nextHop.data[20:24], uint32(scopeID)) + } + } + return nil + } + + return fmt.Errorf("invalid address family") +} + +// Windows API wrappers +func createIPForwardEntry2(route *MIB_IPFORWARD_ROW2) error { + r1, _, e1 := syscall.SyscallN(procCreateIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) + if r1 != 0 { + if e1 != 0 { + return fmt.Errorf("CreateIpForwardEntry2: %w", e1) + } + return fmt.Errorf("CreateIpForwardEntry2: code %d", r1) + } + return nil +} + +func deleteIPForwardEntry2(route *MIB_IPFORWARD_ROW2) error { + r1, _, e1 := syscall.SyscallN(procDeleteIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) + if r1 != 0 { + if e1 != 0 { + return fmt.Errorf("DeleteIpForwardEntry2: %w", e1) + } + return fmt.Errorf("DeleteIpForwardEntry2: code %d", r1) + } + return nil +} + +func getIPForwardEntry2(route *MIB_IPFORWARD_ROW2) error { + r1, _, e1 := syscall.SyscallN(procGetIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) + if r1 != 0 { + if e1 != 0 { + return fmt.Errorf("GetIpForwardEntry2: %w", e1) + } + return fmt.Errorf("GetIpForwardEntry2: code %d", r1) + } + return nil +} + +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-initializeipforwardentry +func initializeIPForwardEntry(route *MIB_IPFORWARD_ROW2) { + // Does not return anything. Trying to handle the error might return an uninitialized value. + _, _, _ = syscall.SyscallN(procInitializeIpForwardEntry.Addr(), uintptr(unsafe.Pointer(route))) +} + +func convertInterfaceIndexToLUID(interfaceIndex uint32, interfaceLUID *luid) error { + r1, _, e1 := syscall.SyscallN(procConvertInterfaceIndexToLuid.Addr(), + uintptr(interfaceIndex), uintptr(unsafe.Pointer(interfaceLUID))) + if r1 != 0 { + if e1 != 0 { + return fmt.Errorf("ConvertInterfaceIndexToLuid: %w", e1) + } + return fmt.Errorf("ConvertInterfaceIndexToLuid: code %d", r1) } return nil } @@ -319,7 +492,7 @@ func cancelMibChangeNotify2(handle windows.Handle) error { } // GetRoutesFromTable returns the current routing table from with prefixes only. -// It ccaches the result for 2 seconds to avoid blocking the caller. +// It caches the result for 2 seconds to avoid blocking the caller. func GetRoutesFromTable() ([]netip.Prefix, error) { mux.Lock() defer mux.Unlock() @@ -388,35 +561,6 @@ func GetRoutes() ([]Route, error) { return routes, nil } -func addRouteCmd(prefix netip.Prefix, nexthop Nexthop) error { - args := []string{"add", prefix.String()} - - if nexthop.IP.IsValid() { - ip := nexthop.IP.WithZone("") - args = append(args, ip.Unmap().String()) - } else { - addr := "0.0.0.0" - if prefix.Addr().Is6() { - addr = "::" - } - args = append(args, addr) - } - - if nexthop.Intf != nil { - args = append(args, "if", strconv.Itoa(nexthop.Intf.Index)) - } - - routeCmd := uspfilter.GetSystem32Command("route") - - out, err := exec.Command(routeCmd, args...).CombinedOutput() - log.Tracef("route %s: %s", strings.Join(args, " "), out) - if err != nil { - return fmt.Errorf("route add: %w", err) - } - - return nil -} - func isCacheDisabled() bool { return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true" } diff --git a/client/internal/routemanager/systemops/systemops_windows_test.go b/client/internal/routemanager/systemops/systemops_windows_test.go index 19b006017..523bd0b0d 100644 --- a/client/internal/routemanager/systemops/systemops_windows_test.go +++ b/client/internal/routemanager/systemops/systemops_windows_test.go @@ -5,18 +5,23 @@ import ( "encoding/json" "fmt" "net" + "net/netip" "os/exec" "strings" "testing" "time" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" nbnet "github.com/netbirdio/netbird/util/net" ) -var expectedExtInt = "Ethernet1" +var ( + expectedExternalInt = "Ethernet1" + expectedVPNint = "wgtest0" +) type RouteInfo struct { NextHop string `json:"nexthop"` @@ -43,8 +48,6 @@ type testCase struct { dialer dialer } -var expectedVPNint = "wgtest0" - var testCases = []testCase{ { name: "To external host without custom dialer via vpn", @@ -52,14 +55,14 @@ var testCases = []testCase{ expectedSourceIP: "100.64.0.1", expectedDestPrefix: "128.0.0.0/1", expectedNextHop: "0.0.0.0", - expectedInterface: "wgtest0", + expectedInterface: expectedVPNint, dialer: &net.Dialer{}, }, { name: "To external host with custom dialer via physical interface", destination: "192.0.2.1:53", expectedDestPrefix: "192.0.2.1/32", - expectedInterface: expectedExtInt, + expectedInterface: expectedExternalInt, dialer: nbnet.NewDialer(), }, @@ -67,24 +70,15 @@ var testCases = []testCase{ name: "To duplicate internal route with custom dialer via physical interface", destination: "10.0.0.2:53", expectedDestPrefix: "10.0.0.2/32", - expectedInterface: expectedExtInt, + expectedInterface: expectedExternalInt, dialer: nbnet.NewDialer(), }, - { - name: "To duplicate internal route without custom dialer via physical interface", // local route takes precedence - destination: "10.0.0.2:53", - expectedSourceIP: "127.0.0.1", - expectedDestPrefix: "10.0.0.0/8", - expectedNextHop: "0.0.0.0", - expectedInterface: "Loopback Pseudo-Interface 1", - dialer: &net.Dialer{}, - }, { name: "To unique vpn route with custom dialer via physical interface", destination: "172.16.0.2:53", expectedDestPrefix: "172.16.0.2/32", - expectedInterface: expectedExtInt, + expectedInterface: expectedExternalInt, dialer: nbnet.NewDialer(), }, { @@ -93,7 +87,7 @@ var testCases = []testCase{ expectedSourceIP: "100.64.0.1", expectedDestPrefix: "172.16.0.0/12", expectedNextHop: "0.0.0.0", - expectedInterface: "wgtest0", + expectedInterface: expectedVPNint, dialer: &net.Dialer{}, }, @@ -103,22 +97,14 @@ var testCases = []testCase{ expectedSourceIP: "100.64.0.1", expectedDestPrefix: "10.10.0.0/24", expectedNextHop: "0.0.0.0", - expectedInterface: "wgtest0", - dialer: &net.Dialer{}, - }, - - { - name: "To more specific route (local) without custom dialer via physical interface", - destination: "127.0.10.2:53", - expectedSourceIP: "127.0.0.1", - expectedDestPrefix: "127.0.0.0/8", - expectedNextHop: "0.0.0.0", - expectedInterface: "Loopback Pseudo-Interface 1", + expectedInterface: expectedVPNint, dialer: &net.Dialer{}, }, } func TestRouting(t *testing.T) { + log.SetLevel(log.DebugLevel) + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { setupTestEnv(t) @@ -129,7 +115,7 @@ func TestRouting(t *testing.T) { require.NoError(t, err, "Failed to fetch interface IP") output := testRoute(t, tc.destination, tc.dialer) - if tc.expectedInterface == expectedExtInt { + if tc.expectedInterface == expectedExternalInt { verifyOutput(t, output, ip, tc.expectedDestPrefix, route.NextHop, route.InterfaceAlias) } else { verifyOutput(t, output, tc.expectedSourceIP, tc.expectedDestPrefix, tc.expectedNextHop, tc.expectedInterface) @@ -242,19 +228,23 @@ func setupDummyInterfacesAndRoutes(t *testing.T) { func addDummyRoute(t *testing.T, dstCIDR string) { t.Helper() - script := fmt.Sprintf(`New-NetRoute -DestinationPrefix "%s" -InterfaceIndex 1 -PolicyStore ActiveStore`, dstCIDR) - - output, err := exec.Command("powershell", "-Command", script).CombinedOutput() + prefix, err := netip.ParsePrefix(dstCIDR) if err != nil { - t.Logf("Failed to add dummy route: %v\nOutput: %s", err, output) - t.FailNow() + t.Fatalf("Failed to parse destination CIDR %s: %v", dstCIDR, err) + } + + nexthop := Nexthop{ + Intf: &net.Interface{Index: 1}, + } + + if err = addRoute(prefix, nexthop); err != nil { + t.Fatalf("Failed to add dummy route: %v", err) } t.Cleanup(func() { - script = fmt.Sprintf(`Remove-NetRoute -DestinationPrefix "%s" -InterfaceIndex 1 -Confirm:$false`, dstCIDR) - output, err := exec.Command("powershell", "-Command", script).CombinedOutput() + err := deleteRoute(prefix, nexthop) if err != nil { - t.Logf("Failed to remove dummy route: %v\nOutput: %s", err, output) + t.Logf("Failed to remove dummy route: %v", err) } }) } diff --git a/client/server/trace.go b/client/server/trace.go index 8b9d375f3..e4ac91487 100644 --- a/client/server/trace.go +++ b/client/server/trace.go @@ -3,11 +3,11 @@ package server import ( "context" "fmt" - "net" "net/netip" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/firewall/uspfilter" + "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" ) @@ -19,81 +19,32 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) ( s.mutex.Lock() defer s.mutex.Unlock() - if s.connectClient == nil { - return nil, fmt.Errorf("connect client not initialized") - } - engine := s.connectClient.Engine() - if engine == nil { - return nil, fmt.Errorf("engine not initialized") + tracer, engine, err := s.getPacketTracer() + if err != nil { + return nil, err } - fwManager := engine.GetFirewallManager() - if fwManager == nil { - return nil, fmt.Errorf("firewall manager not initialized") + srcAddr, err := s.parseAddress(req.GetSourceIp(), engine) + if err != nil { + return nil, fmt.Errorf("invalid source IP address: %w", err) } - tracer, ok := fwManager.(packetTracer) - if !ok { - return nil, fmt.Errorf("firewall manager does not support packet tracing") + dstAddr, err := s.parseAddress(req.GetDestinationIp(), engine) + if err != nil { + return nil, fmt.Errorf("invalid destination IP address: %w", err) } - srcIP := net.ParseIP(req.GetSourceIp()) - if req.GetSourceIp() == "self" { - srcIP = engine.GetWgAddr() + protocol, err := s.parseProtocol(req.GetProtocol()) + if err != nil { + return nil, err } - srcAddr, ok := netip.AddrFromSlice(srcIP) - if !ok { - return nil, fmt.Errorf("invalid source IP address") + direction, err := s.parseDirection(req.GetDirection()) + if err != nil { + return nil, err } - dstIP := net.ParseIP(req.GetDestinationIp()) - if req.GetDestinationIp() == "self" { - dstIP = engine.GetWgAddr() - } - - dstAddr, ok := netip.AddrFromSlice(dstIP) - if !ok { - return nil, fmt.Errorf("invalid source IP address") - } - - if srcIP == nil || dstIP == nil { - return nil, fmt.Errorf("invalid IP address") - } - - var tcpState *uspfilter.TCPState - if flags := req.GetTcpFlags(); flags != nil { - tcpState = &uspfilter.TCPState{ - SYN: flags.GetSyn(), - ACK: flags.GetAck(), - FIN: flags.GetFin(), - RST: flags.GetRst(), - PSH: flags.GetPsh(), - URG: flags.GetUrg(), - } - } - - var dir fw.RuleDirection - switch req.GetDirection() { - case "in": - dir = fw.RuleDirectionIN - case "out": - dir = fw.RuleDirectionOUT - default: - return nil, fmt.Errorf("invalid direction") - } - - var protocol fw.Protocol - switch req.GetProtocol() { - case "tcp": - protocol = fw.ProtocolTCP - case "udp": - protocol = fw.ProtocolUDP - case "icmp": - protocol = fw.ProtocolICMP - default: - return nil, fmt.Errorf("invalid protocolcol") - } + tcpState := s.parseTCPFlags(req.GetTcpFlags()) builder := &uspfilter.PacketBuilder{ SrcIP: srcAddr, @@ -101,16 +52,96 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) ( Protocol: protocol, SrcPort: uint16(req.GetSourcePort()), DstPort: uint16(req.GetDestinationPort()), - Direction: dir, + Direction: direction, TCPState: tcpState, ICMPType: uint8(req.GetIcmpType()), ICMPCode: uint8(req.GetIcmpCode()), } + trace, err := tracer.TracePacketFromBuilder(builder) if err != nil { return nil, fmt.Errorf("trace packet: %w", err) } + return s.buildTraceResponse(trace), nil +} + +func (s *Server) getPacketTracer() (packetTracer, *internal.Engine, error) { + if s.connectClient == nil { + return nil, nil, fmt.Errorf("connect client not initialized") + } + + engine := s.connectClient.Engine() + if engine == nil { + return nil, nil, fmt.Errorf("engine not initialized") + } + + fwManager := engine.GetFirewallManager() + if fwManager == nil { + return nil, nil, fmt.Errorf("firewall manager not initialized") + } + + tracer, ok := fwManager.(packetTracer) + if !ok { + return nil, nil, fmt.Errorf("firewall manager does not support packet tracing") + } + + return tracer, engine, nil +} + +func (s *Server) parseAddress(addr string, engine *internal.Engine) (netip.Addr, error) { + if addr == "self" { + return engine.GetWgAddr(), nil + } + + a, err := netip.ParseAddr(addr) + if err != nil { + return netip.Addr{}, err + } + + return a.Unmap(), nil +} + +func (s *Server) parseProtocol(protocol string) (fw.Protocol, error) { + switch protocol { + case "tcp": + return fw.ProtocolTCP, nil + case "udp": + return fw.ProtocolUDP, nil + case "icmp": + return fw.ProtocolICMP, nil + default: + return "", fmt.Errorf("invalid protocol") + } +} + +func (s *Server) parseDirection(direction string) (fw.RuleDirection, error) { + switch direction { + case "in": + return fw.RuleDirectionIN, nil + case "out": + return fw.RuleDirectionOUT, nil + default: + return 0, fmt.Errorf("invalid direction") + } +} + +func (s *Server) parseTCPFlags(flags *proto.TCPFlags) *uspfilter.TCPState { + if flags == nil { + return nil + } + + return &uspfilter.TCPState{ + SYN: flags.GetSyn(), + ACK: flags.GetAck(), + FIN: flags.GetFin(), + RST: flags.GetRst(), + PSH: flags.GetPsh(), + URG: flags.GetUrg(), + } +} + +func (s *Server) buildTraceResponse(trace *uspfilter.PacketTrace) *proto.TracePacketResponse { resp := &proto.TracePacketResponse{} for _, result := range trace.Results { @@ -119,10 +150,12 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) ( Message: result.Message, Allowed: result.Allowed, } + if result.ForwarderAction != nil { details := fmt.Sprintf("%s to %s", result.ForwarderAction.Action, result.ForwarderAction.RemoteAddr) stage.ForwardingDetails = &details } + resp.Stages = append(resp.Stages, stage) } @@ -130,5 +163,5 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) ( resp.FinalDisposition = trace.Results[len(trace.Results)-1].Allowed } - return resp, nil + return resp } diff --git a/util/net/net.go b/util/net/net.go index b573f9aeb..fdcf4ee6a 100644 --- a/util/net/net.go +++ b/util/net/net.go @@ -1,8 +1,10 @@ package net import ( + "fmt" "math/big" "net" + "net/netip" "github.com/google/uuid" ) @@ -54,11 +56,13 @@ func GenerateConnID() ConnectionID { return ConnectionID(uuid.NewString()) } -func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP { - // Calculate the last IP in the CIDR range +func GetLastIPFromNetwork(network netip.Prefix, fromEnd int) (netip.Addr, error) { var endIP net.IP - for i := 0; i < len(network.IP); i++ { - endIP = append(endIP, network.IP[i]|^network.Mask[i]) + addr := network.Addr().AsSlice() + mask := net.CIDRMask(network.Bits(), len(addr)*8) + + for i := 0; i < len(addr); i++ { + endIP = append(endIP, addr[i]|^mask[i]) } // convert to big.Int @@ -70,5 +74,10 @@ func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP { resultInt := big.NewInt(0) resultInt.Sub(endInt, fromEndBig) - return resultInt.Bytes() + ip, ok := netip.AddrFromSlice(resultInt.Bytes()) + if !ok { + return netip.Addr{}, fmt.Errorf("invalid IP address from network %s", network) + } + + return ip.Unmap(), nil } diff --git a/util/net/net_test.go b/util/net/net_test.go new file mode 100644 index 000000000..e0633cb6a --- /dev/null +++ b/util/net/net_test.go @@ -0,0 +1,94 @@ +package net + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetLastIPFromNetwork(t *testing.T) { + tests := []struct { + name string + network string + fromEnd int + expected string + expectErr bool + }{ + { + name: "IPv4 /24 network - last IP (fromEnd=0)", + network: "192.168.1.0/24", + fromEnd: 0, + expected: "192.168.1.255", + }, + { + name: "IPv4 /24 network - fromEnd=1", + network: "192.168.1.0/24", + fromEnd: 1, + expected: "192.168.1.254", + }, + { + name: "IPv4 /24 network - fromEnd=5", + network: "192.168.1.0/24", + fromEnd: 5, + expected: "192.168.1.250", + }, + { + name: "IPv4 /16 network - last IP", + network: "10.0.0.0/16", + fromEnd: 0, + expected: "10.0.255.255", + }, + { + name: "IPv4 /16 network - fromEnd=256", + network: "10.0.0.0/16", + fromEnd: 256, + expected: "10.0.254.255", + }, + { + name: "IPv4 /32 network - single host", + network: "192.168.1.100/32", + fromEnd: 0, + expected: "192.168.1.100", + }, + { + name: "IPv6 /64 network - last IP", + network: "2001:db8::/64", + fromEnd: 0, + expected: "2001:db8::ffff:ffff:ffff:ffff", + }, + { + name: "IPv6 /64 network - fromEnd=1", + network: "2001:db8::/64", + fromEnd: 1, + expected: "2001:db8::ffff:ffff:ffff:fffe", + }, + { + name: "IPv6 /128 network - single host", + network: "2001:db8::1/128", + fromEnd: 0, + expected: "2001:db8::1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + network, err := netip.ParsePrefix(tt.network) + require.NoError(t, err, "Failed to parse network prefix") + + result, err := GetLastIPFromNetwork(network, tt.fromEnd) + + if tt.expectErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + expectedIP, err := netip.ParseAddr(tt.expected) + require.NoError(t, err, "Failed to parse expected IP") + + assert.Equal(t, expectedIP, result, "IP mismatch for network %s with fromEnd=%d", tt.network, tt.fromEnd) + }) + } +} From b604c6614037e3908bf0e137675ef2ffaf27999b Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 4 Jun 2025 17:38:49 +0300 Subject: [PATCH 054/213] [management] Add postgres support for activity event store (#3890) --- go.mod | 2 +- go.sum | 4 +- .../activity/{sqlite => store}/crypt.go | 2 +- .../activity/{sqlite => store}/crypt_test.go | 2 +- .../activity/{sqlite => store}/migration.go | 22 +++--- .../{sqlite => store}/migration_test.go | 13 ++-- .../{sqlite/sqlite.go => store/sql_store.go} | 77 +++++++++++++++---- .../sql_store_test.go} | 6 +- 8 files changed, 92 insertions(+), 36 deletions(-) rename management/server/activity/{sqlite => store}/crypt.go (99%) rename management/server/activity/{sqlite => store}/crypt_test.go (99%) rename management/server/activity/{sqlite => store}/migration.go (91%) rename management/server/activity/{sqlite => store}/migration_test.go (93%) rename management/server/activity/{sqlite/sqlite.go => store/sql_store.go} (74%) rename management/server/activity/{sqlite/sqlite_test.go => store/sql_store_test.go} (90%) diff --git a/go.mod b/go.mod index c86acdf26..11dc88c43 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203 + github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index 226ee94c2..f887cee94 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203 h1:uxxbLPXQgC9VO15epNPtrD6zazyd5rZeqC5hQSmCdZU= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203/go.mod h1:2ZE6/tBBCKHQggPfO2UOQjyjXI7k+JDVl2ymorTOVQs= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c h1:SdZxYjR9XXHLyRsTbS1EHBr6+RI15oie1K9Q8yvi3FY= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/server/activity/sqlite/crypt.go b/management/server/activity/store/crypt.go similarity index 99% rename from management/server/activity/sqlite/crypt.go rename to management/server/activity/store/crypt.go index 096f49ea3..ce97347d4 100644 --- a/management/server/activity/sqlite/crypt.go +++ b/management/server/activity/store/crypt.go @@ -1,4 +1,4 @@ -package sqlite +package store import ( "bytes" diff --git a/management/server/activity/sqlite/crypt_test.go b/management/server/activity/store/crypt_test.go similarity index 99% rename from management/server/activity/sqlite/crypt_test.go rename to management/server/activity/store/crypt_test.go index aff3a08b1..700bbcd6b 100644 --- a/management/server/activity/sqlite/crypt_test.go +++ b/management/server/activity/store/crypt_test.go @@ -1,4 +1,4 @@ -package sqlite +package store import ( "bytes" diff --git a/management/server/activity/sqlite/migration.go b/management/server/activity/store/migration.go similarity index 91% rename from management/server/activity/sqlite/migration.go rename to management/server/activity/store/migration.go index 6da7893a0..af19a34eb 100644 --- a/management/server/activity/sqlite/migration.go +++ b/management/server/activity/store/migration.go @@ -1,4 +1,4 @@ -package sqlite +package store import ( "context" @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" "gorm.io/gorm" + "gorm.io/gorm/clause" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/migration" @@ -132,11 +133,6 @@ func migrateDuplicateDeletedUsers(ctx context.Context, db *gorm.DB) error { } if err = db.Transaction(func(tx *gorm.DB) error { - groupById := tx.Model(model).Select("MAX(rowid)").Group("id") - if err = tx.Delete(model, "rowid NOT IN (?)", groupById).Error; err != nil { - return err - } - if err = tx.Migrator().RenameTable("deleted_users", "deleted_users_old"); err != nil { return err } @@ -145,12 +141,20 @@ func migrateDuplicateDeletedUsers(ctx context.Context, db *gorm.DB) error { return err } - if err = tx.Exec(` - INSERT INTO deleted_users (id, email, name, enc_algo) SELECT id, email, name, enc_algo - FROM deleted_users_old;`).Error; err != nil { + var deletedUsers []activity.DeletedUser + if err = tx.Table("deleted_users_old").Find(&deletedUsers).Error; err != nil { return err } + for _, deletedUser := range deletedUsers { + if err = tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"email", "name", "enc_algo"}), + }).Create(&deletedUser).Error; err != nil { + return err + } + } + return tx.Migrator().DropTable("deleted_users_old") }); err != nil { return err diff --git a/management/server/activity/sqlite/migration_test.go b/management/server/activity/store/migration_test.go similarity index 93% rename from management/server/activity/sqlite/migration_test.go rename to management/server/activity/store/migration_test.go index 498c976d9..e3261d9fa 100644 --- a/management/server/activity/sqlite/migration_test.go +++ b/management/server/activity/store/migration_test.go @@ -1,17 +1,17 @@ -package sqlite +package store import ( "context" - "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" + "gorm.io/driver/postgres" "gorm.io/gorm" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/migration" + "github.com/netbirdio/netbird/management/server/testutil" ) const ( @@ -21,8 +21,11 @@ const ( func setupDatabase(t *testing.T) *gorm.DB { t.Helper() - dbFile := filepath.Join(t.TempDir(), eventSinkDB) - db, err := gorm.Open(sqlite.Open(dbFile)) + cleanup, dsn, err := testutil.CreatePostgresTestContainer() + require.NoError(t, err, "Failed to create Postgres test container") + t.Cleanup(cleanup) + + db, err := gorm.Open(postgres.Open(dsn)) require.NoError(t, err) sql, err := db.DB() diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/store/sql_store.go similarity index 74% rename from management/server/activity/sqlite/sqlite.go rename to management/server/activity/store/sql_store.go index 6d198fca9..80b165938 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/store/sql_store.go @@ -1,17 +1,22 @@ -package sqlite +package store import ( "context" "fmt" + "os" "path/filepath" + "runtime" + "strconv" log "github.com/sirupsen/logrus" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/logger" "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/types" ) const ( @@ -22,6 +27,10 @@ const ( fallbackEmail = "unknown@unknown.com" gcmEncAlgo = "GCM" + + storeEngineEnv = "NB_ACTIVITY_EVENT_STORE_ENGINE" + postgresDsnEnv = "NB_ACTIVITY_EVENT_POSTGRES_DSN" + sqlMaxOpenConnsEnv = "NB_SQL_MAX_OPEN_CONNS" ) type eventWithNames struct { @@ -38,28 +47,19 @@ type Store struct { fieldEncrypt *FieldEncrypt } -// NewSQLiteStore creates a new Store with an event table if not exists. -func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) { +// NewSqlStore creates a new Store with an event table if not exists. +func NewSqlStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) { crypt, err := NewFieldEncrypt(encryptionKey) if err != nil { return nil, err } - dbFile := filepath.Join(dataDir, eventSinkDB) - db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) + db, err := initDatabase(ctx, dataDir) if err != nil { - return nil, err + return nil, fmt.Errorf("initialize database: %w", err) } - sql, err := db.DB() - if err != nil { - return nil, err - } - sql.SetMaxOpenConns(1) - if err = migrate(ctx, crypt, db); err != nil { return nil, fmt.Errorf("events database migration: %w", err) } @@ -236,3 +236,52 @@ func (store *Store) Close(_ context.Context) error { } return nil } + +func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) { + var dialector gorm.Dialector + var storeEngine = types.SqliteStoreEngine + + if engine, ok := os.LookupEnv(storeEngineEnv); ok { + storeEngine = types.Engine(engine) + } + + switch storeEngine { + case types.SqliteStoreEngine: + dialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB)) + case types.PostgresStoreEngine: + dsn, ok := os.LookupEnv(postgresDsnEnv) + if !ok { + return nil, fmt.Errorf("%s environment variable not set", postgresDsnEnv) + } + dialector = postgres.Open(dsn) + default: + return nil, fmt.Errorf("unsupported store engine: %s", storeEngine) + } + log.WithContext(ctx).Infof("using %s as activity event store engine", storeEngine) + + db, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + return nil, fmt.Errorf("open db connection: %w", err) + } + + return configureConnectionPool(db, storeEngine) +} + +func configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) { + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + if storeEngine == types.SqliteStoreEngine { + sqlDB.SetMaxOpenConns(1) + } else { + conns, err := strconv.Atoi(os.Getenv(sqlMaxOpenConnsEnv)) + if err != nil { + conns = runtime.NumCPU() + } + sqlDB.SetMaxOpenConns(conns) + } + + return db, nil +} diff --git a/management/server/activity/sqlite/sqlite_test.go b/management/server/activity/store/sql_store_test.go similarity index 90% rename from management/server/activity/sqlite/sqlite_test.go rename to management/server/activity/store/sql_store_test.go index b10f9b58a..8c0d159df 100644 --- a/management/server/activity/sqlite/sqlite_test.go +++ b/management/server/activity/store/sql_store_test.go @@ -1,4 +1,4 @@ -package sqlite +package store import ( "context" @@ -11,10 +11,10 @@ import ( "github.com/netbirdio/netbird/management/server/activity" ) -func TestNewSQLiteStore(t *testing.T) { +func TestNewSqlStore(t *testing.T) { dataDir := t.TempDir() key, _ := GenerateKey() - store, err := NewSQLiteStore(context.Background(), dataDir, key) + store, err := NewSqlStore(context.Background(), dataDir, key) if err != nil { t.Fatal(err) return From 609654eee7d441846021959bc386338b4696815a Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:12:48 +0200 Subject: [PATCH 055/213] [client] Allow userspace local forwarding to internal interfaces if requested (#3884) --- client/firewall/uspfilter/forwarder/tcp.go | 4 ++-- client/firewall/uspfilter/forwarder/udp.go | 4 ++-- client/firewall/uspfilter/uspfilter.go | 23 +++++++++++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go index 04b3ae233..64e54e293 100644 --- a/client/firewall/uspfilter/forwarder/tcp.go +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -111,12 +111,12 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn if errInToOut != nil { if !isClosedError(errInToOut) { - f.logger.Error("proxyTCP: copy error (in -> out): %v", errInToOut) + f.logger.Error("proxyTCP: copy error (in -> out) for %s: %v", epID(id), errInToOut) } } if errOutToIn != nil { if !isClosedError(errOutToIn) { - f.logger.Error("proxyTCP: copy error (out -> in): %v", errOutToIn) + f.logger.Error("proxyTCP: copy error (out -> in) for %s: %v", epID(id), errOutToIn) } } diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go index cb88aa59a..f237a313d 100644 --- a/client/firewall/uspfilter/forwarder/udp.go +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -250,10 +250,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack wg.Wait() if outboundErr != nil && !isClosedError(outboundErr) { - f.logger.Error("proxyUDP: copy error (outbound->inbound): %v", outboundErr) + f.logger.Error("proxyUDP: copy error (outbound->inbound) for %s: %v", epID(id), outboundErr) } if inboundErr != nil && !isClosedError(inboundErr) { - f.logger.Error("proxyUDP: copy error (inbound->outbound): %v", inboundErr) + f.logger.Error("proxyUDP: copy error (inbound->outbound) for %s: %v", epID(id), inboundErr) } var rxPackets, txPackets uint64 diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index eede1ab13..c216bc302 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -39,8 +39,12 @@ const ( // EnvForceUserspaceRouter forces userspace routing even if native routing is available. EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER" - // EnvEnableNetstackLocalForwarding enables forwarding of local traffic to the native stack when running netstack - // Leaving this on by default introduces a security risk as sockets on listening on localhost only will be accessible + // EnvEnableLocalForwarding enables forwarding of local traffic to the native stack for internal (non-NetBird) interfaces. + // Default off as it might be security risk because sockets listening on localhost only will become accessible. + EnvEnableLocalForwarding = "NB_ENABLE_LOCAL_FORWARDING" + + // EnvEnableNetstackLocalForwarding is an alias for EnvEnableLocalForwarding. + // In netstack mode, it enables forwarding of local traffic to the native stack for all interfaces. EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING" ) @@ -147,6 +151,11 @@ func parseCreateEnv() (bool, bool) { if err != nil { log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err) } + } else if val := os.Getenv(EnvEnableLocalForwarding); val != "" { + enableLocalForwarding, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvEnableLocalForwarding, err) + } } return disableConntrack, enableLocalForwarding @@ -779,9 +788,10 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet return true } - // if running in netstack mode we need to pass this to the forwarder - if m.netstack && m.localForwarding { - return m.handleNetstackLocalTraffic(packetData) + // If requested we pass local traffic to internal interfaces to the forwarder. + // netstack doesn't have an interface to forward packets to the native stack so we always need to use the forwarder. + if m.localForwarding && (m.netstack || dstIP != m.wgIface.Address().IP) { + return m.handleForwardedLocalTraffic(packetData) } // track inbound packets to get the correct direction and session id for flows @@ -791,8 +801,7 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet return false } -func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool { - +func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool { fwd := m.forwarder.Load() if fwd == nil { m.logger.Trace("Dropping local packet (forwarder not initialized)") From 9424b88db2239a5205b429110806d6f5ab1397ec Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 5 Jun 2025 11:51:39 +0200 Subject: [PATCH 056/213] [client] Add output similar to wg show to the debug package (#3922) --- client/anonymize/anonymize.go | 16 +++ client/iface/configurer/kernel_unix.go | 43 +++++++ client/iface/configurer/usp.go | 149 +++++++++++++++++++++++++ client/iface/configurer/wgshow.go | 24 ++++ client/iface/device/interface.go | 1 + client/iface/iface.go | 4 + client/internal/debug/debug.go | 5 + client/internal/debug/wgshow.go | 66 +++++++++++ client/internal/engine.go | 3 +- client/internal/engine_test.go | 4 + client/internal/iface_common.go | 1 + client/internal/peer/status.go | 23 ++++ 12 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 client/iface/configurer/wgshow.go create mode 100644 client/internal/debug/wgshow.go diff --git a/client/anonymize/anonymize.go b/client/anonymize/anonymize.go index 2fc9d49d3..89e653300 100644 --- a/client/anonymize/anonymize.go +++ b/client/anonymize/anonymize.go @@ -69,6 +69,22 @@ func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr { return a.ipAnonymizer[ip] } +func (a *Anonymizer) AnonymizeUDPAddr(addr net.UDPAddr) net.UDPAddr { + // Convert IP to netip.Addr + ip, ok := netip.AddrFromSlice(addr.IP) + if !ok { + return addr + } + + anonIP := a.AnonymizeIP(ip) + + return net.UDPAddr{ + IP: anonIP.AsSlice(), + Port: addr.Port, + Zone: addr.Zone, + } +} + // isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool { if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 { diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go index 87076fea8..91991177e 100644 --- a/client/iface/configurer/kernel_unix.go +++ b/client/iface/configurer/kernel_unix.go @@ -12,6 +12,8 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +var zeroKey wgtypes.Key + type KernelConfigurer struct { deviceName string } @@ -201,6 +203,47 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error { func (c *KernelConfigurer) Close() { } +func (c *KernelConfigurer) FullStats() (*Stats, error) { + wg, err := wgctrl.New() + if err != nil { + return nil, fmt.Errorf("wgctl: %w", err) + } + defer func() { + err = wg.Close() + if err != nil { + log.Errorf("Got error while closing wgctl: %v", err) + } + }() + + wgDevice, err := wg.Device(c.deviceName) + if err != nil { + return nil, fmt.Errorf("get device %s: %w", c.deviceName, err) + } + fullStats := &Stats{ + DeviceName: wgDevice.Name, + PublicKey: wgDevice.PublicKey.String(), + ListenPort: wgDevice.ListenPort, + FWMark: wgDevice.FirewallMark, + Peers: []Peer{}, + } + + for _, p := range wgDevice.Peers { + peer := Peer{ + PublicKey: p.PublicKey.String(), + AllowedIPs: p.AllowedIPs, + TxBytes: p.TransmitBytes, + RxBytes: p.ReceiveBytes, + LastHandshake: p.LastHandshakeTime, + PresharedKey: p.PresharedKey != zeroKey, + } + if p.Endpoint != nil { + peer.Endpoint = *p.Endpoint + } + fullStats.Peers = append(fullStats.Peers, peer) + } + return fullStats, nil +} + func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) { stats := make(map[string]WGStats) wg, err := wgctrl.New() diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index d7ab1ec6f..914788821 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -19,10 +19,17 @@ import ( ) const ( + privateKey = "private_key" ipcKeyLastHandshakeTimeSec = "last_handshake_time_sec" ipcKeyLastHandshakeTimeNsec = "last_handshake_time_nsec" ipcKeyTxBytes = "tx_bytes" ipcKeyRxBytes = "rx_bytes" + allowedIP = "allowed_ip" + endpoint = "endpoint" + fwmark = "fwmark" + listenPort = "listen_port" + publicKey = "public_key" + presharedKey = "preshared_key" ) var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found") @@ -186,6 +193,15 @@ func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, ip string) error { return c.device.IpcSet(toWgUserspaceString(config)) } +func (c *WGUSPConfigurer) FullStats() (*Stats, error) { + ipcStr, err := c.device.IpcGet() + if err != nil { + return nil, fmt.Errorf("IpcGet failed: %w", err) + } + + return parseStatus(c.deviceName, ipcStr) +} + // startUAPI starts the UAPI listener for managing the WireGuard interface via external tool func (t *WGUSPConfigurer) startUAPI() { var err error @@ -365,3 +381,136 @@ func getFwmark() int { } return 0 } + +func hexToWireguardKey(hexKey string) (wgtypes.Key, error) { + // Decode hex string to bytes + keyBytes, err := hex.DecodeString(hexKey) + if err != nil { + return wgtypes.Key{}, fmt.Errorf("failed to decode hex key: %w", err) + } + + // Check if we have the right number of bytes (WireGuard keys are 32 bytes) + if len(keyBytes) != 32 { + return wgtypes.Key{}, fmt.Errorf("invalid key length: expected 32 bytes, got %d", len(keyBytes)) + } + + // Convert to wgtypes.Key + var key wgtypes.Key + copy(key[:], keyBytes) + + return key, nil +} + +func parseStatus(deviceName, ipcStr string) (*Stats, error) { + stats := &Stats{DeviceName: deviceName} + var currentPeer *Peer + for _, line := range strings.Split(strings.TrimSpace(ipcStr), "\n") { + if line == "" { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := parts[0] + val := parts[1] + + switch key { + case privateKey: + key, err := hexToWireguardKey(val) + if err != nil { + log.Errorf("failed to parse private key: %v", err) + continue + } + stats.PublicKey = key.PublicKey().String() + case publicKey: + // Save previous peer + if currentPeer != nil { + stats.Peers = append(stats.Peers, *currentPeer) + } + key, err := hexToWireguardKey(val) + if err != nil { + log.Errorf("failed to parse public key: %v", err) + continue + } + currentPeer = &Peer{ + PublicKey: key.String(), + } + case listenPort: + if port, err := strconv.Atoi(val); err == nil { + stats.ListenPort = port + } + case fwmark: + if fwmark, err := strconv.Atoi(val); err == nil { + stats.FWMark = fwmark + } + case endpoint: + if currentPeer == nil { + continue + } + + host, portStr, err := net.SplitHostPort(strings.Trim(val, "[]")) + if err != nil { + log.Errorf("failed to parse endpoint: %v", err) + continue + } + port, err := strconv.Atoi(portStr) + if err != nil { + log.Errorf("failed to parse endpoint port: %v", err) + continue + } + currentPeer.Endpoint = net.UDPAddr{ + IP: net.ParseIP(host), + Port: port, + } + case allowedIP: + if currentPeer == nil { + continue + } + _, ipnet, err := net.ParseCIDR(val) + if err == nil { + currentPeer.AllowedIPs = append(currentPeer.AllowedIPs, *ipnet) + } + case ipcKeyTxBytes: + if currentPeer == nil { + continue + } + rxBytes, err := toBytes(val) + if err != nil { + continue + } + currentPeer.TxBytes = rxBytes + case ipcKeyRxBytes: + if currentPeer == nil { + continue + } + rxBytes, err := toBytes(val) + if err != nil { + continue + } + currentPeer.RxBytes = rxBytes + + case ipcKeyLastHandshakeTimeSec: + if currentPeer == nil { + continue + } + + ts, err := toLastHandshake(val) + if err != nil { + continue + } + currentPeer.LastHandshake = ts + case presharedKey: + if currentPeer == nil { + continue + } + if val != "" { + currentPeer.PresharedKey = true + } + } + } + if currentPeer != nil { + stats.Peers = append(stats.Peers, *currentPeer) + } + return stats, nil +} diff --git a/client/iface/configurer/wgshow.go b/client/iface/configurer/wgshow.go new file mode 100644 index 000000000..604264026 --- /dev/null +++ b/client/iface/configurer/wgshow.go @@ -0,0 +1,24 @@ +package configurer + +import ( + "net" + "time" +) + +type Peer struct { + PublicKey string + Endpoint net.UDPAddr + AllowedIPs []net.IPNet + TxBytes int64 + RxBytes int64 + LastHandshake time.Time + PresharedKey bool +} + +type Stats struct { + DeviceName string + PublicKey string + ListenPort int + FWMark int + Peers []Peer +} diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go index a1d44a150..31ebdf4b8 100644 --- a/client/iface/device/interface.go +++ b/client/iface/device/interface.go @@ -17,4 +17,5 @@ type WGConfigurer interface { RemoveAllowedIP(peerKey string, allowedIP string) error Close() GetStats() (map[string]configurer.WGStats, error) + FullStats() (*configurer.Stats, error) } diff --git a/client/iface/iface.go b/client/iface/iface.go index 1f659af29..f4394c476 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -216,6 +216,10 @@ func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) { return w.configurer.GetStats() } +func (w *WGIface) FullStats() (*configurer.Stats, error) { + return w.configurer.FullStats() +} + func (w *WGIface) waitUntilRemoved() error { maxWaitTime := 5 * time.Second timeout := time.NewTimer(maxWaitTime) diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index a753ece0c..a7d873c8f 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -270,11 +270,16 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("Failed to add corrupted state files to debug bundle: %v", err) } + if err := g.addWgShow(); err != nil { + log.Errorf("Failed to add wg show output: %v", err) + } + if g.logFile != "console" { if err := g.addLogfile(); err != nil { return fmt.Errorf("add log file: %w", err) } } + return nil } diff --git a/client/internal/debug/wgshow.go b/client/internal/debug/wgshow.go new file mode 100644 index 000000000..e4b4c2368 --- /dev/null +++ b/client/internal/debug/wgshow.go @@ -0,0 +1,66 @@ +package debug + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/netbirdio/netbird/client/iface/configurer" +) + +type WGIface interface { + FullStats() (*configurer.Stats, error) +} + +func (g *BundleGenerator) addWgShow() error { + result, err := g.statusRecorder.PeersStatus() + if err != nil { + return err + } + + output := g.toWGShowFormat(result) + reader := bytes.NewReader([]byte(output)) + + if err := g.addFileToZip(reader, "wgshow.txt"); err != nil { + return fmt.Errorf("add wg show to zip: %w", err) + } + return nil +} + +func (g *BundleGenerator) toWGShowFormat(s *configurer.Stats) string { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("interface: %s\n", s.DeviceName)) + sb.WriteString(fmt.Sprintf(" public key: %s\n", s.PublicKey)) + sb.WriteString(fmt.Sprintf(" listen port: %d\n", s.ListenPort)) + if s.FWMark != 0 { + sb.WriteString(fmt.Sprintf(" fwmark: %#x\n", s.FWMark)) + } + + for _, peer := range s.Peers { + sb.WriteString(fmt.Sprintf("\npeer: %s\n", peer.PublicKey)) + if peer.Endpoint.IP != nil { + if g.anonymize { + anonEndpoint := g.anonymizer.AnonymizeUDPAddr(peer.Endpoint) + sb.WriteString(fmt.Sprintf(" endpoint: %s\n", anonEndpoint.String())) + } else { + sb.WriteString(fmt.Sprintf(" endpoint: %s\n", peer.Endpoint.String())) + } + } + if len(peer.AllowedIPs) > 0 { + var ipStrings []string + for _, ipnet := range peer.AllowedIPs { + ipStrings = append(ipStrings, ipnet.String()) + } + sb.WriteString(fmt.Sprintf(" allowed ips: %s\n", strings.Join(ipStrings, ", "))) + } + sb.WriteString(fmt.Sprintf(" latest handshake: %s\n", peer.LastHandshake.Format(time.RFC1123))) + sb.WriteString(fmt.Sprintf(" transfer: %d B received, %d B sent\n", peer.RxBytes, peer.TxBytes)) + if peer.PresharedKey { + sb.WriteString(" preshared key: (hidden)\n") + } + } + + return sb.String() +} diff --git a/client/internal/engine.go b/client/internal/engine.go index 0dec799bf..e47007749 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -359,6 +359,7 @@ func (e *Engine) Start() error { return fmt.Errorf("new wg interface: %w", err) } e.wgInterface = wgIface + e.statusRecorder.SetWgIface(wgIface) // start flow manager right after interface creation publicKey := e.config.WgPrivateKey.PublicKey() @@ -380,7 +381,6 @@ func (e *Engine) Start() error { return fmt.Errorf("run rosenpass manager: %w", err) } } - e.stateManager.Start() initialRoutes, dnsServer, err := e.newDnsServer() @@ -1453,6 +1453,7 @@ func (e *Engine) close() { log.Errorf("failed closing Netbird interface %s %v", e.config.WgIfaceName, err) } e.wgInterface = nil + e.statusRecorder.SetWgIface(nil) } if !isNil(e.sshServer) { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 82c1ba0e2..6bdd9ae3c 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -99,6 +99,10 @@ type MockWGIface struct { GetNetFunc func() *netstack.Net } +func (m *MockWGIface) FullStats() (*configurer.Stats, error) { + return nil, fmt.Errorf("not implemented") +} + func (m *MockWGIface) GetInterfaceGUIDString() (string, error) { return m.GetInterfaceGUIDStringFunc() } diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index e1761ff84..95bf146f9 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -37,4 +37,5 @@ type wgIfaceBase interface { GetWGDevice() *wgdevice.Device GetStats() (map[string]configurer.WGStats, error) GetNet() *netstack.Net + FullStats() (*configurer.Stats, error) } diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 0c6aac372..ed2f1fe47 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -3,6 +3,7 @@ package peer import ( "context" "errors" + "fmt" "net/netip" "slices" "sync" @@ -32,6 +33,10 @@ type ResolvedDomainInfo struct { ParentDomain domain.Domain } +type WGIfaceStatus interface { + FullStats() (*configurer.Stats, error) +} + type EventListener interface { OnEvent(event *proto.SystemEvent) } @@ -202,6 +207,7 @@ type Status struct { ingressGwMgr *ingressgw.Manager routeIDLookup routeIDLookup + wgIface WGIfaceStatus } // NewRecorder returns a new Status instance @@ -1078,6 +1084,23 @@ func (d *Status) GetEventHistory() []*proto.SystemEvent { return d.eventQueue.GetAll() } +func (d *Status) SetWgIface(wgInterface WGIfaceStatus) { + d.mux.Lock() + defer d.mux.Unlock() + + d.wgIface = wgInterface +} + +func (d *Status) PeersStatus() (*configurer.Stats, error) { + d.mux.Lock() + defer d.mux.Unlock() + if d.wgIface == nil { + return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status") + } + + return d.wgIface.FullStats() +} + type EventQueue struct { maxSize int events []*proto.SystemEvent From df82a45d99a736c026944f89aaccff40b8b3219e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:39:58 +0200 Subject: [PATCH 057/213] [client] Improve dns match trace log (#3928) --- client/internal/dns/handler_chain.go | 81 ++++++++++++++-------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 6baf9ed95..22caaa761 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -1,6 +1,7 @@ package dns import ( + "fmt" "slices" "strings" "sync" @@ -148,61 +149,42 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } qname := strings.ToLower(r.Question[0].Name) - log.Tracef("handling DNS request for domain=%s", qname) c.mu.RLock() handlers := slices.Clone(c.handlers) c.mu.RUnlock() if log.IsLevelEnabled(log.TraceLevel) { - log.Tracef("current handlers (%d):", len(handlers)) + var b strings.Builder + b.WriteString(fmt.Sprintf("DNS request domain=%s, handlers (%d):\n", qname, len(handlers))) for _, h := range handlers { - log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d", - h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority) + b.WriteString(fmt.Sprintf(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d\n", + h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority)) } + log.Trace(strings.TrimSuffix(b.String(), "\n")) } // Try handlers in priority order for _, entry := range handlers { - var matched bool - switch { - case entry.Pattern == ".": - matched = true - case entry.IsWildcard: - parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".") - matched = len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern) - default: - // For non-wildcard patterns: - // If handler wants subdomain matching, allow suffix match - // Otherwise require exact match - if entry.MatchSubdomains { - matched = strings.EqualFold(qname, entry.Pattern) || strings.HasSuffix(qname, "."+entry.Pattern) - } else { - matched = strings.EqualFold(qname, entry.Pattern) + matched := c.isHandlerMatch(qname, entry) + + if matched { + log.Tracef("handler matched: domain=%s -> pattern=%s wildcard=%v match_subdomain=%v priority=%d", + qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority) + + chainWriter := &ResponseWriterChain{ + ResponseWriter: w, + origPattern: entry.OrigPattern, } - } + entry.Handler.ServeDNS(chainWriter, r) - if !matched { - log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d matched=false", - qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard, entry.Priority) - continue + // If handler wants to continue, try next handler + if chainWriter.shouldContinue { + log.Tracef("handler requested continue to next handler for domain=%s", qname) + continue + } + return } - - log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d", - qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority) - - chainWriter := &ResponseWriterChain{ - ResponseWriter: w, - origPattern: entry.OrigPattern, - } - entry.Handler.ServeDNS(chainWriter, r) - - // If handler wants to continue, try next handler - if chainWriter.shouldContinue { - log.Tracef("handler requested continue to next handler") - continue - } - return } // No handler matched or all handlers passed @@ -213,3 +195,22 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { log.Errorf("failed to write DNS response: %v", err) } } + +func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool { + switch { + case entry.Pattern == ".": + return true + case entry.IsWildcard: + parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".") + return len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern) + default: + // For non-wildcard patterns: + // If handler wants subdomain matching, allow suffix match + // Otherwise require exact match + if entry.MatchSubdomains { + return strings.EqualFold(qname, entry.Pattern) || strings.HasSuffix(qname, "."+entry.Pattern) + } else { + return strings.EqualFold(qname, entry.Pattern) + } + } +} From 55957a1960a3bda96f84a9fdc6cbeabba2896153 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:40:23 +0200 Subject: [PATCH 058/213] [client] Run registerdns before flushing (#3926) * Run registerdns before flushing * Disable WINS, dynamic updates and registration --- client/internal/dns/host_windows.go | 125 +++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 12 deletions(-) diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index cfba29501..f8939328a 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -1,11 +1,14 @@ package dns import ( + "context" "errors" "fmt" "io" + "os/exec" "strings" "syscall" + "time" "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" @@ -41,6 +44,20 @@ const ( interfaceConfigNameServerKey = "NameServer" interfaceConfigSearchListKey = "SearchList" + // Network interface DNS registration settings + disableDynamicUpdateKey = "DisableDynamicUpdate" + registrationEnabledKey = "RegistrationEnabled" + maxNumberOfAddressesToRegisterKey = "MaxNumberOfAddressesToRegister" + + // NetBIOS/WINS settings + netbtInterfacePath = `SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces` + netbiosOptionsKey = "NetbiosOptions" + + // NetBIOS option values: 0 = from DHCP, 1 = enabled, 2 = disabled + netbiosFromDHCP = 0 + netbiosEnabled = 1 + netbiosDisabled = 2 + // RP_FORCE: Reapply all policies even if no policy change was detected rpForce = 0x1 ) @@ -67,16 +84,85 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { log.Infof("detected GPO DNS policy configuration, using policy store") } - return ®istryConfigurator{ + configurator := ®istryConfigurator{ guid: guid, gpo: useGPO, - }, nil + } + + if err := configurator.configureInterface(); err != nil { + log.Errorf("failed to configure interface settings: %v", err) + } + + return configurator, nil } func (r *registryConfigurator) supportCustomPort() bool { return false } +func (r *registryConfigurator) configureInterface() error { + var merr *multierror.Error + + if err := r.disableDNSRegistrationForInterface(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("disable DNS registration: %w", err)) + } + + if err := r.disableWINSForInterface(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("disable WINS: %w", err)) + } + + return nberrors.FormatErrorOrNil(merr) +} + +func (r *registryConfigurator) disableDNSRegistrationForInterface() error { + regKey, err := r.getInterfaceRegistryKey() + if err != nil { + return fmt.Errorf("get interface registry key: %w", err) + } + defer closer(regKey) + + var merr *multierror.Error + + if err := regKey.SetDWordValue(disableDynamicUpdateKey, 1); err != nil { + merr = multierror.Append(merr, fmt.Errorf("set %s: %w", disableDynamicUpdateKey, err)) + } + + if err := regKey.SetDWordValue(registrationEnabledKey, 0); err != nil { + merr = multierror.Append(merr, fmt.Errorf("set %s: %w", registrationEnabledKey, err)) + } + + if err := regKey.SetDWordValue(maxNumberOfAddressesToRegisterKey, 0); err != nil { + merr = multierror.Append(merr, fmt.Errorf("set %s: %w", maxNumberOfAddressesToRegisterKey, err)) + } + + if merr == nil || len(merr.Errors) == 0 { + log.Infof("disabled DNS registration for interface %s", r.guid) + } + + return nberrors.FormatErrorOrNil(merr) +} + +func (r *registryConfigurator) disableWINSForInterface() error { + netbtKeyPath := fmt.Sprintf(`%s\Tcpip_%s`, netbtInterfacePath, r.guid) + + regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, netbtKeyPath, registry.SET_VALUE) + if err != nil { + regKey, _, err = registry.CreateKey(registry.LOCAL_MACHINE, netbtKeyPath, registry.SET_VALUE) + if err != nil { + return fmt.Errorf("create NetBT interface key %s: %w", netbtKeyPath, err) + } + } + defer closer(regKey) + + // NetbiosOptions: 2 = disabled + if err := regKey.SetDWordValue(netbiosOptionsKey, netbiosDisabled); err != nil { + return fmt.Errorf("set %s: %w", netbiosOptionsKey, err) + } + + log.Infof("disabled WINS/NetBIOS for interface %s", r.guid) + return nil +} + func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { if config.RouteAll { if err := r.addDNSSetupForAll(config.ServerIP); err != nil { @@ -119,9 +205,7 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager return fmt.Errorf("update search domains: %w", err) } - if err := r.flushDNSCache(); err != nil { - log.Errorf("failed to flush DNS cache: %v", err) - } + go r.flushDNSCache() return nil } @@ -191,7 +275,25 @@ func (r *registryConfigurator) string() string { return "registry" } -func (r *registryConfigurator) flushDNSCache() error { +func (r *registryConfigurator) registerDNS() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + // nolint:misspell + cmd := exec.CommandContext(ctx, "ipconfig", "/registerdns") + out, err := cmd.CombinedOutput() + + if err != nil { + log.Errorf("failed to register DNS: %v, output: %s", err, out) + return + } + + log.Info("registered DNS names") +} + +func (r *registryConfigurator) flushDNSCache() { + r.registerDNS() + // dnsFlushResolverCacheFn.Call() may panic if the func is not found defer func() { if rec := recover(); rec != nil { @@ -202,13 +304,14 @@ func (r *registryConfigurator) flushDNSCache() error { ret, _, err := dnsFlushResolverCacheFn.Call() if ret == 0 { if err != nil && !errors.Is(err, syscall.Errno(0)) { - return fmt.Errorf("DnsFlushResolverCache failed: %w", err) + log.Errorf("DnsFlushResolverCache failed: %v", err) + return } - return fmt.Errorf("DnsFlushResolverCache failed") + log.Errorf("DnsFlushResolverCache failed") + return } log.Info("flushed DNS cache") - return nil } func (r *registryConfigurator) updateSearchDomains(domains []string) error { @@ -263,9 +366,7 @@ func (r *registryConfigurator) restoreHostDNS() error { return fmt.Errorf("remove interface registry key: %w", err) } - if err := r.flushDNSCache(); err != nil { - log.Errorf("failed to flush DNS cache: %v", err) - } + go r.flushDNSCache() return nil } From 84354951d37fdcff137e0f045cff4190d5ee61a2 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:54:15 +0200 Subject: [PATCH 059/213] [client] Add systemd netbird logs to debug bundle (#3917) --- client/cmd/service.go | 10 ++- client/internal/debug/debug.go | 9 ++- client/internal/debug/debug_linux.go | 89 ++++++++++++++++++++++++- client/internal/debug/debug_nonlinux.go | 6 ++ 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/client/cmd/service.go b/client/cmd/service.go index 005479306..156e67d6d 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "runtime" "sync" "github.com/kardianos/service" @@ -27,12 +28,19 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program { } func newSVCConfig() *service.Config { - return &service.Config{ + config := &service.Config{ Name: serviceName, DisplayName: "Netbird", Description: "Netbird mesh network client", Option: make(service.KeyValue), + EnvVars: make(map[string]string), } + + if runtime.GOOS == "linux" { + config.EnvVars["SYSTEMD_UNIT"] = serviceName + } + + return config } func newSVC(prg *program, conf *service.Config) (service.Service, error) { diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index a7d873c8f..dfed47f05 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -274,10 +274,15 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("Failed to add wg show output: %v", err) } - if g.logFile != "console" { + if g.logFile != "console" && g.logFile != "" { if err := g.addLogfile(); err != nil { - return fmt.Errorf("add log file: %w", err) + log.Errorf("Failed to add log file to debug bundle: %v", err) + if err := g.trySystemdLogFallback(); err != nil { + log.Errorf("Failed to add systemd logs as fallback: %v", err) + } } + } else if err := g.trySystemdLogFallback(); err != nil { + log.Errorf("Failed to add systemd logs: %v", err) } return nil diff --git a/client/internal/debug/debug_linux.go b/client/internal/debug/debug_linux.go index b4907beca..4626cd9a2 100644 --- a/client/internal/debug/debug_linux.go +++ b/client/internal/debug/debug_linux.go @@ -4,17 +4,104 @@ package debug import ( "bytes" + "context" "encoding/binary" + "errors" "fmt" + "os" "os/exec" "sort" "strings" + "time" "github.com/google/nftables" "github.com/google/nftables/expr" log "github.com/sirupsen/logrus" ) +const ( + maxLogEntries = 100000 + maxLogAge = 7 * 24 * time.Hour // Last 7 days +) + +// trySystemdLogFallback attempts to get logs from systemd journal as fallback +func (g *BundleGenerator) trySystemdLogFallback() error { + log.Debug("Attempting to collect systemd journal logs") + + serviceName := getServiceName() + journalLogs, err := getSystemdLogs(serviceName) + if err != nil { + return fmt.Errorf("get systemd logs for %s: %w", serviceName, err) + } + + if strings.Contains(journalLogs, "No recent log entries found") { + log.Debug("No recent log entries found in systemd journal") + return nil + } + + if g.anonymize { + journalLogs = g.anonymizer.AnonymizeString(journalLogs) + } + + logReader := strings.NewReader(journalLogs) + fileName := fmt.Sprintf("systemd-%s.log", serviceName) + if err := g.addFileToZip(logReader, fileName); err != nil { + return fmt.Errorf("add systemd logs to bundle: %w", err) + } + + log.Infof("Added systemd journal logs for %s to debug bundle", serviceName) + return nil +} + +// getServiceName gets the service name from environment or defaults to netbird +func getServiceName() string { + if unitName := os.Getenv("SYSTEMD_UNIT"); unitName != "" { + log.Debugf("Detected SYSTEMD_UNIT environment variable: %s", unitName) + return unitName + } + + return "netbird" +} + +// getSystemdLogs retrieves logs from systemd journal for a specific service using journalctl +func getSystemdLogs(serviceName string) (string, error) { + args := []string{ + "-u", fmt.Sprintf("%s.service", serviceName), + "--since", fmt.Sprintf("-%s", maxLogAge.String()), + "--lines", fmt.Sprintf("%d", maxLogEntries), + "--no-pager", + "--output", "short-iso", + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "journalctl", args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return "", fmt.Errorf("journalctl command timed out after 30 seconds") + } + if strings.Contains(err.Error(), "executable file not found") { + return "", fmt.Errorf("journalctl command not found: %w", err) + } + return "", fmt.Errorf("execute journalctl: %w (stderr: %s)", err, stderr.String()) + } + + logs := stdout.String() + if strings.TrimSpace(logs) == "" { + return "No recent log entries found in systemd journal", nil + } + + header := fmt.Sprintf("=== Systemd Journal Logs for %s.service (last %d entries, max %s) ===\n", + serviceName, maxLogEntries, maxLogAge.String()) + + return header + logs, nil +} + // addFirewallRules collects and adds firewall rules to the archive func (g *BundleGenerator) addFirewallRules() error { log.Info("Collecting firewall rules") @@ -481,7 +568,7 @@ func formatExpr(exp expr.Any) string { case *expr.Fib: return formatFib(e) case *expr.Target: - return fmt.Sprintf("jump %s", e.Name) // Properly format jump targets + return fmt.Sprintf("jump %s", e.Name) case *expr.Immediate: if e.Register == 1 { return formatImmediateData(e.Data) diff --git a/client/internal/debug/debug_nonlinux.go b/client/internal/debug/debug_nonlinux.go index ef93620a0..b0ff55613 100644 --- a/client/internal/debug/debug_nonlinux.go +++ b/client/internal/debug/debug_nonlinux.go @@ -6,3 +6,9 @@ package debug func (g *BundleGenerator) addFirewallRules() error { return nil } + +func (g *BundleGenerator) trySystemdLogFallback() error { + // Systemd is only available on Linux + // TODO: Add BSD support + return nil +} From 6c0cdb6ed17a90992410269eb75362e48ac303db Mon Sep 17 00:00:00 2001 From: Ghazy Abdallah Date: Thu, 5 Jun 2025 15:15:01 +0300 Subject: [PATCH 060/213] [misc] fix: traefik relay accessibility (#3696) --- infrastructure_files/base.setup.env | 2 ++ infrastructure_files/configure.sh | 2 ++ infrastructure_files/docker-compose.yml.tmpl | 2 +- .../docker-compose.yml.tmpl.traefik | 22 +++---------------- infrastructure_files/management.json.tmpl | 2 +- infrastructure_files/setup.env.example | 11 ++++++++++ 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index ebc38a11f..fdba1f215 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -23,6 +23,7 @@ NETBIRD_SIGNAL_PORT=${NETBIRD_SIGNAL_PORT:-10000} # Relay NETBIRD_RELAY_DOMAIN=${NETBIRD_RELAY_DOMAIN:-$NETBIRD_DOMAIN} NETBIRD_RELAY_PORT=${NETBIRD_RELAY_PORT:-33080} +NETBIRD_RELAY_ENDPOINT=${NETBIRD_RELAY_ENDPOINT:-rel://$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT} # Relay auth secret NETBIRD_RELAY_AUTH_SECRET= @@ -135,5 +136,6 @@ export COTURN_TAG export NETBIRD_TURN_EXTERNAL_IP export NETBIRD_RELAY_DOMAIN export NETBIRD_RELAY_PORT +export NETBIRD_RELAY_ENDPOINT export NETBIRD_RELAY_AUTH_SECRET export NETBIRD_RELAY_TAG diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index d02e4f40c..6898555aa 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -170,6 +170,7 @@ fi if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then export NETBIRD_DASHBOARD_ENDPOINT="https://$NETBIRD_DOMAIN:443" export NETBIRD_SIGNAL_ENDPOINT="https://$NETBIRD_DOMAIN:$NETBIRD_SIGNAL_PORT" + export NETBIRD_RELAY_ENDPOINT="rels://$NETBIRD_DOMAIN:$NETBIRD_SIGNAL_PORT/relay" echo "Letsencrypt was disabled, the Https-endpoints cannot be used anymore" echo " and a reverse-proxy with Https needs to be placed in front of netbird!" @@ -178,6 +179,7 @@ if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then echo "- $NETBIRD_MGMT_API_ENDPOINT/api -http-> management:$NETBIRD_MGMT_API_PORT" echo "- $NETBIRD_MGMT_API_ENDPOINT/management.ManagementService/ -grpc-> management:$NETBIRD_MGMT_API_PORT" echo "- $NETBIRD_SIGNAL_ENDPOINT/signalexchange.SignalExchange/ -grpc-> signal:80" + echo "- $NETBIRD_RELAY_ENDPOINT/ -http-> relay:33080" echo "You most likely also have to change NETBIRD_MGMT_API_ENDPOINT in base.setup.env and port-mappings in docker-compose.yml.tmpl and rerun this script." echo " The target of the forwards depends on your setup. Beware of the gRPC protocol instead of http for management and signal!" echo "You are also free to remove any occurrences of the Letsencrypt-volume $LETSENCRYPT_VOLUMENAME" diff --git a/infrastructure_files/docker-compose.yml.tmpl b/infrastructure_files/docker-compose.yml.tmpl index dc491ae23..b529f9606 100644 --- a/infrastructure_files/docker-compose.yml.tmpl +++ b/infrastructure_files/docker-compose.yml.tmpl @@ -57,7 +57,7 @@ services: environment: - NB_LOG_LEVEL=info - NB_LISTEN_ADDRESS=:$NETBIRD_RELAY_PORT - - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT + - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_ENDPOINT # todo: change to a secure secret - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET ports: diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik index 8cc3df309..8da3cabb5 100644 --- a/infrastructure_files/docker-compose.yml.tmpl.traefik +++ b/infrastructure_files/docker-compose.yml.tmpl.traefik @@ -3,9 +3,6 @@ services: dashboard: image: netbirdio/dashboard:$NETBIRD_DASHBOARD_TAG restart: unless-stopped - #ports: - # - 80:80 - # - 443:443 environment: # Endpoints - NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT @@ -43,11 +40,6 @@ services: restart: unless-stopped volumes: - $SIGNAL_VOLUMENAME:/var/lib/netbird - #ports: - # - $NETBIRD_SIGNAL_PORT:80 - # # port and command for Let's Encrypt validation - # - 443:443 - # command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"] labels: - traefik.enable=true - traefik.http.routers.netbird-signal.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/signalexchange.SignalExchange/`) @@ -65,12 +57,10 @@ services: restart: unless-stopped environment: - NB_LOG_LEVEL=info - - NB_LISTEN_ADDRESS=:$NETBIRD_RELAY_PORT - - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT + - NB_LISTEN_ADDRESS=:33080 + - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_ENDPOINT # todo: change to a secure secret - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET - # ports: - # - $NETBIRD_RELAY_PORT:$NETBIRD_RELAY_PORT logging: driver: "json-file" options: @@ -79,7 +69,7 @@ services: labels: - traefik.enable=true - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`) - - traefik.http.services.netbird-relay.loadbalancer.server.port=$NETBIRD_RELAY_PORT + - traefik.http.services.netbird-relay.loadbalancer.server.port=33080 # Management management: @@ -91,10 +81,6 @@ services: - $MGMT_VOLUMENAME:/var/lib/netbird - $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt:ro - ./management.json:/etc/netbird/management.json - #ports: - # - $NETBIRD_MGMT_API_PORT:443 #API port - # # command for Let's Encrypt validation without dashboard container - # command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"] command: [ "--port", "33073", "--log-file", "console", @@ -129,8 +115,6 @@ services: domainname: $TURN_DOMAIN volumes: - ./turnserver.conf:/etc/turnserver.conf:ro - # - ./privkey.pem:/etc/coturn/private/privkey.pem:ro - # - ./cert.pem:/etc/coturn/certs/cert.pem:ro network_mode: host command: - -c /etc/turnserver.conf diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index c0e57b4fd..4d09816ef 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -21,7 +21,7 @@ "TimeBasedCredentials": false }, "Relay": { - "Addresses": ["rel://$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT"], + "Addresses": ["$NETBIRD_RELAY_ENDPOINT"], "CredentialsTTL": "24h", "Secret": "$NETBIRD_RELAY_AUTH_SECRET" }, diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example index b1b64de78..b5b718a71 100644 --- a/infrastructure_files/setup.env.example +++ b/infrastructure_files/setup.env.example @@ -102,4 +102,15 @@ NETBIRD_RELAY_DOMAIN="" # Relay server connection port. If none is supplied # it will default to 33080 +# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy NETBIRD_RELAY_PORT="" + +# Management API connectin port. If none is supplied +# it will default to 33073 +# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy +NETBIRD_MGMT_API_PORT="" + +# Signal service connectin port. If none is supplied +# it will default to 10000 +# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy +NETBIRD_SIGNAL_PORT="" From c6cceba381b81e7296d4ffec25cadeeda9e6f43c Mon Sep 17 00:00:00 2001 From: Robert Neumann Date: Thu, 5 Jun 2025 14:16:04 +0200 Subject: [PATCH 061/213] Update getting-started-with-zitadel.sh - fix zitadel user console (#3446) --- infrastructure_files/getting-started-with-zitadel.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure_files/getting-started-with-zitadel.sh b/infrastructure_files/getting-started-with-zitadel.sh index 9b80058c2..1e67bd177 100644 --- a/infrastructure_files/getting-started-with-zitadel.sh +++ b/infrastructure_files/getting-started-with-zitadel.sh @@ -602,6 +602,7 @@ renderCaddyfile() { reverse_proxy /debug/* h2c://zitadel:8080 reverse_proxy /device/* h2c://zitadel:8080 reverse_proxy /device h2c://zitadel:8080 + reverse_proxy /zitadel.user.v2.UserService/* h2c://zitadel:8080 # Dashboard reverse_proxy /* dashboard:80 } From 122a89c02b4aa3aadd399cb5cb0ec9e7ba12584a Mon Sep 17 00:00:00 2001 From: Abdul Latif <55663276+orchard0@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:28:19 +0100 Subject: [PATCH 062/213] [misc] remove error causing dnf config-manager add (#3925) --- release_files/install.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/release_files/install.sh b/release_files/install.sh index da5c613d5..0f63529ea 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -262,13 +262,6 @@ install_netbird() { ;; dnf) add_rpm_repo - ${SUDO} dnf -y install dnf-plugin-config-manager - if [[ "$(dnf --version | head -n1 | cut -d. -f1)" > "4" ]]; - then - ${SUDO} dnf config-manager addrepo --from-repofile=/etc/yum.repos.d/netbird.repo - else - ${SUDO} dnf config-manager --add-repo /etc/yum.repos.d/netbird.repo - fi ${SUDO} dnf -y install netbird if ! $SKIP_UI_APP; then From 64f111923efbd682f7d627845d8f7831d6b27472 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:22:59 +0200 Subject: [PATCH 063/213] [client] Increase stun status probe timeout (#3930) --- client/internal/engine.go | 2 +- client/internal/relay/relay.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index e47007749..0962e9004 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1678,7 +1678,7 @@ func (e *Engine) RunHealthProbes() bool { func (e *Engine) probeICE(stuns, turns []*stun.URI) []relay.ProbeResult { return append( relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns), - relay.ProbeAll(e.ctx, relay.ProbeSTUN, turns)..., + relay.ProbeAll(e.ctx, relay.ProbeTURN, turns)..., ) } diff --git a/client/internal/relay/relay.go b/client/internal/relay/relay.go index 7d98a6060..6e1f83a9a 100644 --- a/client/internal/relay/relay.go +++ b/client/internal/relay/relay.go @@ -170,7 +170,7 @@ func ProbeAll( var wg sync.WaitGroup for i, uri := range relays { - ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + ctx, cancel := context.WithTimeout(ctx, 6*time.Second) defer cancel() wg.Add(1) From b56f61bf1b093afaed242a22f861475a40eafe1d Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 5 Jun 2025 14:44:44 +0100 Subject: [PATCH 064/213] [misc] fix relay exposed address test (#3931) --- .github/workflows/test-infrastructure-files.yml | 4 ++-- infrastructure_files/configure.sh | 2 +- infrastructure_files/setup.env.example | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index 8c2d21c8f..e2f9e40c8 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -172,11 +172,11 @@ jobs: grep "NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN" docker-compose.yml grep NETBIRD_STORE_ENGINE_POSTGRES_DSN docker-compose.yml | egrep "$NETBIRD_STORE_ENGINE_POSTGRES_DSN" # check relay values - grep "NB_EXPOSED_ADDRESS=$CI_NETBIRD_DOMAIN:33445" docker-compose.yml + grep "NB_EXPOSED_ADDRESS=rels://$CI_NETBIRD_DOMAIN:33445" docker-compose.yml grep "NB_LISTEN_ADDRESS=:33445" docker-compose.yml grep '33445:33445' docker-compose.yml grep -A 10 'relay:' docker-compose.yml | egrep 'NB_AUTH_SECRET=.+$' - grep -A 7 Relay management.json | grep "rel://$CI_NETBIRD_DOMAIN:33445" + grep -A 7 Relay management.json | grep "rels://$CI_NETBIRD_DOMAIN:33445" grep -A 7 Relay management.json | egrep '"Secret": ".+"' grep DisablePromptLogin management.json | grep 'true' grep LoginFlag management.json | grep 0 diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index 6898555aa..e3fcbfdde 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -170,7 +170,7 @@ fi if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then export NETBIRD_DASHBOARD_ENDPOINT="https://$NETBIRD_DOMAIN:443" export NETBIRD_SIGNAL_ENDPOINT="https://$NETBIRD_DOMAIN:$NETBIRD_SIGNAL_PORT" - export NETBIRD_RELAY_ENDPOINT="rels://$NETBIRD_DOMAIN:$NETBIRD_SIGNAL_PORT/relay" + export NETBIRD_RELAY_ENDPOINT="rels://$NETBIRD_DOMAIN:$NETBIRD_RELAY_PORT/relay" echo "Letsencrypt was disabled, the Https-endpoints cannot be used anymore" echo " and a reverse-proxy with Https needs to be placed in front of netbird!" diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example index b5b718a71..b6a209953 100644 --- a/infrastructure_files/setup.env.example +++ b/infrastructure_files/setup.env.example @@ -105,12 +105,12 @@ NETBIRD_RELAY_DOMAIN="" # should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy NETBIRD_RELAY_PORT="" -# Management API connectin port. If none is supplied +# Management API connecting port. If none is supplied # it will default to 33073 # should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy NETBIRD_MGMT_API_PORT="" -# Signal service connectin port. If none is supplied +# Signal service connecting port. If none is supplied # it will default to 10000 # should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy NETBIRD_SIGNAL_PORT="" From 0f7c7f1da2f07221d67ea621422ea930fb0da76e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sun, 8 Jun 2025 09:53:27 +0100 Subject: [PATCH 065/213] [misc] use generic slack url (#3939) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0f2df848..1d2a976c2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@
- +
@@ -29,7 +29,7 @@
See Documentation
- Join our Slack channel + Join our Slack channel
From 0f050e5fe1b2f34b484a6f082cc483f0712a3135 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sun, 8 Jun 2025 11:19:54 +0100 Subject: [PATCH 066/213] [client] Optmize process check time (#3938) This PR optimizes the process check time by updating the implementation of getRunningProcesses and introducing new benchmark tests. Updated getRunningProcesses to use process.Pids() instead of process.Processes() Added benchmark tests for both the new and the legacy implementations Benchmark: https://github.com/netbirdio/netbird/actions/runs/15512741612 todo: evaluate windows optmizations and caching risks --- client/system/process.go | 8 +++-- client/system/process_test.go | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 client/system/process_test.go diff --git a/client/system/process.go b/client/system/process.go index 2e43fcfe0..87e21eb9d 100644 --- a/client/system/process.go +++ b/client/system/process.go @@ -11,16 +11,18 @@ import ( // getRunningProcesses returns a list of running process paths. func getRunningProcesses() ([]string, error) { - processes, err := process.Processes() + processIDs, err := process.Pids() if err != nil { return nil, err } processMap := make(map[string]bool) - for _, p := range processes { + for _, pID := range processIDs { + p := &process.Process{Pid: pID} + path, _ := p.Exe() if path != "" { - processMap[path] = true + processMap[path] = false } } diff --git a/client/system/process_test.go b/client/system/process_test.go new file mode 100644 index 000000000..505808a9e --- /dev/null +++ b/client/system/process_test.go @@ -0,0 +1,58 @@ +package system + +import ( + "testing" + + "github.com/shirou/gopsutil/v3/process" +) + +func Benchmark_getRunningProcesses(b *testing.B) { + b.Run("getRunningProcesses new", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ps, err := getRunningProcesses() + if err != nil { + b.Fatalf("unexpected error: %v", err) + } + if len(ps) == 0 { + b.Fatalf("expected non-empty process list, got empty") + } + } + }) + b.Run("getRunningProcesses old", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ps, err := getRunningProcessesOld() + if err != nil { + b.Fatalf("unexpected error: %v", err) + } + if len(ps) == 0 { + b.Fatalf("expected non-empty process list, got empty") + } + } + }) + s, _ := getRunningProcesses() + b.Logf("getRunningProcesses returned %d processes", len(s)) + s, _ = getRunningProcessesOld() + b.Logf("getRunningProcessesOld returned %d processes", len(s)) +} + +func getRunningProcessesOld() ([]string, error) { + processes, err := process.Processes() + if err != nil { + return nil, err + } + + processMap := make(map[string]bool) + for _, p := range processes { + path, _ := p.Exe() + if path != "" { + processMap[path] = true + } + } + + uniqueProcesses := make([]string, 0, len(processMap)) + for p := range processMap { + uniqueProcesses = append(uniqueProcesses, p) + } + + return uniqueProcesses, nil +} From 3c535cdd2b0f2193466c8bd4223626401856b5bd Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:10:34 +0200 Subject: [PATCH 067/213] [client] Add lazy connections to routed networks (#3908) --- client/internal/conn_mgr.go | 11 + client/internal/engine.go | 24 +- client/internal/engine_test.go | 85 +-- client/internal/lazyconn/manager/manager.go | 130 ++++- client/internal/peer/status.go | 4 + client/internal/routemanager/client/client.go | 74 ++- .../routemanager/client/client_bench_test.go | 155 +++++ .../routemanager/client/client_test.go | 539 ++++++++++++++++-- client/internal/routemanager/manager.go | 22 +- client/internal/routemanager/manager_test.go | 6 +- client/internal/routemanager/mock.go | 15 +- 11 files changed, 918 insertions(+), 147 deletions(-) create mode 100644 client/internal/routemanager/client/client_bench_test.go diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index f7b1f6a05..88b47c511 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -14,6 +14,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/route" ) // ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections. @@ -97,6 +98,16 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er } } +// UpdateRouteHAMap updates the route HA mappings in the lazy connection manager +func (e *ConnMgr) UpdateRouteHAMap(haMap route.HAMap) { + if !e.isStartedWithLazyMgr() { + log.Debugf("lazy connection manager is not started, skipping UpdateRouteHAMap") + return + } + + e.lazyConnMgr.UpdateRouteHAMap(haMap) +} + // SetExcludeList sets the list of peer IDs that should always have permanent connections. func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) { if e.lazyConnMgr == nil { diff --git a/client/internal/engine.go b/client/internal/engine.go index 0962e9004..034057fe0 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1007,7 +1007,15 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { // apply routes first, route related actions might depend on routing being enabled routes := toRoutes(networkMap.GetRoutes()) - if err := e.routeManager.UpdateRoutes(serial, routes, dnsRouteFeatureFlag); err != nil { + serverRoutes, clientRoutes := e.routeManager.ClassifyRoutes(routes) + + // lazy mgr needs to be aware of which routes are available before they are applied + if e.connMgr != nil { + e.connMgr.UpdateRouteHAMap(clientRoutes) + log.Debugf("updated lazy connection manager with %d HA groups", len(clientRoutes)) + } + + if err := e.routeManager.UpdateRoutes(serial, serverRoutes, clientRoutes, dnsRouteFeatureFlag); err != nil { log.Errorf("failed to update routes: %v", err) } @@ -1067,7 +1075,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { } // must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store - excludedLazyPeers := e.toExcludedLazyPeers(routes, forwardingRules, networkMap.GetRemotePeers()) + excludedLazyPeers := e.toExcludedLazyPeers(forwardingRules, networkMap.GetRemotePeers()) e.connMgr.SetExcludeList(excludedLazyPeers) e.networkSerial = serial @@ -1933,18 +1941,8 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewal return forwardingRules, nberrors.FormatErrorOrNil(merr) } -func (e *Engine) toExcludedLazyPeers(routes []*route.Route, rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool { +func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool { excludedPeers := make(map[string]bool) - for _, r := range routes { - if r.Peer == "" { - continue - } - if !excludedPeers[r.Peer] { - log.Infof("exclude router peer from lazy connection: %s", r.Peer) - excludedPeers[r.Peer] = true - } - } - for _, r := range rules { ip := r.TranslatedAddress for _, p := range peers { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 6bdd9ae3c..a4470e0ec 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -643,12 +643,12 @@ func TestEngine_Sync(t *testing.T) { func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { testCases := []struct { - name string - inputErr error - networkMap *mgmtProto.NetworkMap - expectedLen int - expectedRoutes []*route.Route - expectedSerial uint64 + name string + inputErr error + networkMap *mgmtProto.NetworkMap + expectedLen int + expectedClientRoutes route.HAMap + expectedSerial uint64 }{ { name: "Routes Config Should Be Passed To Manager", @@ -676,22 +676,26 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { }, }, expectedLen: 2, - expectedRoutes: []*route.Route{ - { - ID: "a", - Network: netip.MustParsePrefix("192.168.0.0/24"), - NetID: "n1", - Peer: "p1", - NetworkType: 1, - Masquerade: false, + expectedClientRoutes: route.HAMap{ + "n1|192.168.0.0/24": []*route.Route{ + { + ID: "a", + Network: netip.MustParsePrefix("192.168.0.0/24"), + NetID: "n1", + Peer: "p1", + NetworkType: 1, + Masquerade: false, + }, }, - { - ID: "b", - Network: netip.MustParsePrefix("192.168.1.0/24"), - NetID: "n2", - Peer: "p1", - NetworkType: 1, - Masquerade: false, + "n2|192.168.1.0/24": []*route.Route{ + { + ID: "b", + Network: netip.MustParsePrefix("192.168.1.0/24"), + NetID: "n2", + Peer: "p1", + NetworkType: 1, + Masquerade: false, + }, }, }, expectedSerial: 1, @@ -704,9 +708,9 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { RemotePeersIsEmpty: false, Routes: nil, }, - expectedLen: 0, - expectedRoutes: []*route.Route{}, - expectedSerial: 1, + expectedLen: 0, + expectedClientRoutes: nil, + expectedSerial: 1, }, { name: "Error Shouldn't Break Engine", @@ -717,9 +721,9 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { RemotePeersIsEmpty: false, Routes: nil, }, - expectedLen: 0, - expectedRoutes: []*route.Route{}, - expectedSerial: 1, + expectedLen: 0, + expectedClientRoutes: nil, + expectedSerial: 1, }, } @@ -762,16 +766,29 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { engine.wgInterface, err = iface.NewWGIFace(opts) assert.NoError(t, err, "shouldn't return error") input := struct { - inputSerial uint64 - inputRoutes []*route.Route + inputSerial uint64 + clientRoutes route.HAMap }{} mockRouteManager := &routemanager.MockManager{ - UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error { + UpdateRoutesFunc: func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error { input.inputSerial = updateSerial - input.inputRoutes = newRoutes + input.clientRoutes = clientRoutes return testCase.inputErr }, + ClassifyRoutesFunc: func(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) { + if len(newRoutes) == 0 { + return nil, nil + } + + // Classify all routes as client routes (not matching our public key) + clientRoutes := make(route.HAMap) + for _, r := range newRoutes { + haID := r.GetHAUniqueID() + clientRoutes[haID] = append(clientRoutes[haID], r) + } + return nil, clientRoutes + }, } engine.routeManager = mockRouteManager @@ -789,8 +806,8 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { err = engine.updateNetworkMap(testCase.networkMap) assert.NoError(t, err, "shouldn't return error") assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match") - assert.Len(t, input.inputRoutes, testCase.expectedLen, "clientRoutes len should match") - assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "clientRoutes should match") + assert.Len(t, input.clientRoutes, testCase.expectedLen, "clientRoutes len should match") + assert.Equal(t, testCase.expectedClientRoutes, input.clientRoutes, "clientRoutes should match") }) } } @@ -951,7 +968,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { assert.NoError(t, err, "shouldn't return error") mockRouteManager := &routemanager.MockManager{ - UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error { + UpdateRoutesFunc: func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error { return nil }, } diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go index af12a73e4..15979d553 100644 --- a/client/internal/lazyconn/manager/manager.go +++ b/client/internal/lazyconn/manager/manager.go @@ -6,6 +6,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" "github.com/netbirdio/netbird/client/internal/lazyconn" "github.com/netbirdio/netbird/client/internal/lazyconn/activity" @@ -13,6 +14,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer/dispatcher" peerid "github.com/netbirdio/netbird/client/internal/peer/id" "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/route" ) const ( @@ -37,6 +39,7 @@ type Config struct { // - Managing inactivity monitors for lazy connections (based on peer disconnection events) // - Maintaining a list of excluded peers that should always have permanent connections // - Handling connection establishment based on peer signaling +// - Managing route HA groups and activating all peers in a group when one peer is activated type Manager struct { peerStore *peerstore.Store connStateDispatcher *dispatcher.ConnectionDispatcher @@ -51,6 +54,11 @@ type Manager struct { activityManager *activity.Manager inactivityMonitors map[peerid.ConnID]*inactivity.Monitor + // Route HA group management + peerToHAGroups map[string][]route.HAUniqueID // peer ID -> HA groups they belong to + haGroupToPeers map[route.HAUniqueID][]string // HA group -> peer IDs in the group + routesMu sync.RWMutex // protects route mappings + cancel context.CancelFunc onInactive chan peerid.ConnID } @@ -66,6 +74,8 @@ func NewManager(config Config, peerStore *peerstore.Store, wgIface lazyconn.WGIf excludes: make(map[string]lazyconn.PeerConfig), activityManager: activity.NewManager(wgIface), inactivityMonitors: make(map[peerid.ConnID]*inactivity.Monitor), + peerToHAGroups: make(map[string][]route.HAUniqueID), + haGroupToPeers: make(map[route.HAUniqueID][]string), onInactive: make(chan peerid.ConnID), } @@ -87,6 +97,41 @@ func NewManager(config Config, peerStore *peerstore.Store, wgIface lazyconn.WGIf return m } +// UpdateRouteHAMap updates the HA group mappings for routes +// This should be called when route configuration changes +func (m *Manager) UpdateRouteHAMap(haMap route.HAMap) { + m.routesMu.Lock() + defer m.routesMu.Unlock() + + maps.Clear(m.peerToHAGroups) + maps.Clear(m.haGroupToPeers) + + for haUniqueID, routes := range haMap { + var peers []string + + peerSet := make(map[string]bool) + for _, r := range routes { + if !peerSet[r.Peer] { + peerSet[r.Peer] = true + peers = append(peers, r.Peer) + } + } + + if len(peers) <= 1 { + continue + } + + m.haGroupToPeers[haUniqueID] = peers + + for _, peerID := range peers { + m.peerToHAGroups[peerID] = append(m.peerToHAGroups[peerID], haUniqueID) + } + } + + log.Debugf("updated route HA mappings: %d HA groups, %d peers with routes", + len(m.haGroupToPeers), len(m.peerToHAGroups)) +} + // Start starts the manager and listens for peer activity and inactivity events func (m *Manager) Start(ctx context.Context) { defer m.close() @@ -209,25 +254,47 @@ func (m *Manager) RemovePeer(peerID string) { } // ActivatePeer activates a peer connection when a signal message is received +// Also activates all peers in the same HA groups as this peer func (m *Manager) ActivatePeer(ctx context.Context, peerID string) (found bool) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() + cfg, mp := m.getPeerForActivation(peerID) + if cfg == nil { + return false + } + if !m.activateSinglePeer(ctx, cfg, mp) { + return false + } + + m.activateHAGroupPeers(ctx, peerID) + + return true +} + +// getPeerForActivation checks if a peer can be activated and returns the necessary structs +// Returns nil values if the peer should be skipped +func (m *Manager) getPeerForActivation(peerID string) (*lazyconn.PeerConfig, *managedPeer) { cfg, ok := m.managedPeers[peerID] if !ok { - return false + return nil, nil } mp, ok := m.managedPeersByConnID[cfg.PeerConnID] if !ok { - return false + return nil, nil } // signal messages coming continuously after success activation, with this avoid the multiple activation if mp.expectedWatcher == watcherInactivity { - return false + return nil, nil } + return cfg, mp +} + +// activateSinglePeer activates a single peer (internal method) +func (m *Manager) activateSinglePeer(ctx context.Context, cfg *lazyconn.PeerConfig, mp *managedPeer) bool { mp.expectedWatcher = watcherInactivity m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID) @@ -238,12 +305,53 @@ func (m *Manager) ActivatePeer(ctx context.Context, peerID string) (found bool) return false } - mp.peerCfg.Log.Infof("starting inactivity monitor") + cfg.Log.Infof("starting inactivity monitor") go im.Start(ctx, m.onInactive) return true } +// activateHAGroupPeers activates all peers in HA groups that the given peer belongs to +func (m *Manager) activateHAGroupPeers(ctx context.Context, triggerPeerID string) { + m.routesMu.RLock() + haGroups := m.peerToHAGroups[triggerPeerID] + m.routesMu.RUnlock() + + if len(haGroups) == 0 { + log.Debugf("peer %s is not part of any HA groups", triggerPeerID) + return + } + + activatedCount := 0 + for _, haGroup := range haGroups { + m.routesMu.RLock() + peers := m.haGroupToPeers[haGroup] + m.routesMu.RUnlock() + + for _, peerID := range peers { + if peerID == triggerPeerID { + continue + } + + cfg, mp := m.getPeerForActivation(peerID) + if cfg == nil { + continue + } + + if m.activateSinglePeer(ctx, cfg, mp) { + activatedCount++ + cfg.Log.Infof("activated peer as part of HA group %s (triggered by %s)", haGroup, triggerPeerID) + m.peerStore.PeerConnOpen(ctx, cfg.PublicKey) + } + } + } + + if activatedCount > 0 { + log.Infof("activated %d additional peers in HA groups for peer %s (groups: %v)", + activatedCount, triggerPeerID, haGroups) + } +} + func (m *Manager) addActivePeer(ctx context.Context, peerCfg lazyconn.PeerConfig) error { if _, ok := m.managedPeers[peerCfg.PublicKey]; ok { peerCfg.Log.Warnf("peer already managed") @@ -297,6 +405,13 @@ func (m *Manager) close() { m.inactivityMonitors = make(map[peerid.ConnID]*inactivity.Monitor) m.managedPeers = make(map[string]*lazyconn.PeerConfig) m.managedPeersByConnID = make(map[peerid.ConnID]*managedPeer) + + // Clear route mappings + m.routesMu.Lock() + m.peerToHAGroups = make(map[string][]route.HAUniqueID) + m.haGroupToPeers = make(map[route.HAUniqueID][]string) + m.routesMu.Unlock() + log.Infof("lazy connection manager closed") } @@ -317,10 +432,11 @@ func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) mp.peerCfg.Log.Infof("detected peer activity") - mp.expectedWatcher = watcherInactivity + if !m.activateSinglePeer(ctx, mp.peerCfg, mp) { + return + } - mp.peerCfg.Log.Infof("starting inactivity monitor") - go m.inactivityMonitors[peerConnID].Start(ctx, m.onInactive) + m.activateHAGroupPeers(ctx, mp.peerCfg.PublicKey) m.peerStore.PeerConnOpen(ctx, mp.peerCfg.PublicKey) } diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index ed2f1fe47..abeafd757 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -576,6 +576,10 @@ func (d *Status) FinishPeerListModifications() { d.mux.Unlock() d.notifyPeerListChanged() + + for key := range d.peers { + d.notifyPeerStateChangeListeners(key) + } } func (d *Status) SubscribeToPeerStateChanges(ctx context.Context, peerID string) *StatusChangeSubscription { diff --git a/client/internal/routemanager/client/client.go b/client/internal/routemanager/client/client.go index 5582591a9..6e3cf61c9 100644 --- a/client/internal/routemanager/client/client.go +++ b/client/internal/routemanager/client/client.go @@ -38,9 +38,9 @@ const ( ) type routerPeerStatus struct { - connected bool - relayed bool - latency time.Duration + status peer.ConnStatus + relayed bool + latency time.Duration } type RoutesUpdate struct { @@ -68,6 +68,7 @@ type WatcherConfig struct { // Watcher watches route and peer changes and updates allowed IPs accordingly. // Once stopped, it cannot be reused. +// The methods are not thread-safe and should be synchronized externally. type Watcher struct { ctx context.Context cancel context.CancelFunc @@ -78,6 +79,7 @@ type Watcher struct { peerStateUpdate chan struct{} routePeersNotifiers map[string]chan struct{} // map of peer key to channel for peer state changes currentChosen *route.Route + currentChosenStatus *routerPeerStatus handler RouteHandler updateSerial uint64 } @@ -95,6 +97,7 @@ func NewWatcher(config WatcherConfig) *Watcher { routeUpdate: make(chan RoutesUpdate), peerStateUpdate: make(chan struct{}), handler: config.Handler, + currentChosenStatus: nil, } return client } @@ -108,9 +111,9 @@ func (w *Watcher) getRouterPeerStatuses() map[route.ID]routerPeerStatus { continue } routePeerStatuses[r.ID] = routerPeerStatus{ - connected: peerStatus.ConnStatus == peer.StatusConnected, - relayed: peerStatus.Relayed, - latency: peerStatus.Latency, + status: peerStatus.ConnStatus, + relayed: peerStatus.Relayed, + latency: peerStatus.Latency, } } return routePeerStatuses @@ -121,15 +124,17 @@ func (w *Watcher) getRouterPeerStatuses() map[route.ID]routerPeerStatus { // preference for non-relayed and direct connections. // // It follows these prioritization rules: -// * Connected peers: Only routes with connected peers are considered. +// * Connection status: Both connected and idle peers are considered, but connected peers always take precedence. +// * Idle peer penalty: Idle peers receive a significant score penalty to ensure any connected peer is preferred. // * Metric: Routes with lower metrics (better) are prioritized. // * Non-relayed: Routes without relays are preferred. // * Latency: Routes with lower latency are prioritized. +// * Allowed IPs: Idle peers can still receive allowed IPs to enable lazy connection triggering. // * we compare the current score + 10ms to the chosen score to avoid flapping between routes // * Stability: In case of equal scores, the currently active route (if any) is maintained. // // It returns the ID of the selected optimal route. -func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID { +func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) (route.ID, routerPeerStatus) { var chosen route.ID chosenScore := float64(0) currScore := float64(0) @@ -139,10 +144,13 @@ func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]router currID = w.currentChosen.ID } + var chosenStatus routerPeerStatus + for _, r := range w.routes { tempScore := float64(0) peerStatus, found := routePeerStatuses[r.ID] - if !found || !peerStatus.connected { + // connecting status equals disconnected: no wireguard endpoint to assign allowed IPs to + if !found || peerStatus.status == peer.StatusConnecting { continue } @@ -155,8 +163,8 @@ func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]router latency := 999 * time.Millisecond if peerStatus.latency != 0 { latency = peerStatus.latency - } else { - log.Tracef("peer %s has 0 latency, range %s", r.Peer, w.handler) + } else if !peerStatus.relayed && peerStatus.status != peer.StatusIdle { + log.Tracef("peer %s has 0 latency: [%v]", r.Peer, w.handler) } // avoid negative tempScore on the higher latency calculation @@ -167,17 +175,24 @@ func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]router // higher latency is worse score tempScore += 1 - latency.Seconds() + // apply significant penalty for idle peers to ensure connected peers always take precedence + if peerStatus.status == peer.StatusConnected { + tempScore += 100_000 + } + if !peerStatus.relayed { tempScore++ } if tempScore > chosenScore || (tempScore == chosenScore && chosen == "") { chosen = r.ID + chosenStatus = peerStatus chosenScore = tempScore } if chosen == "" && currID == "" { chosen = r.ID + chosenStatus = peerStatus chosenScore = tempScore } @@ -204,13 +219,13 @@ func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]router peers = append(peers, r.Peer) } - log.Infof("network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", w.handler, peers) + log.Infof("network [%v] has not been assigned a routing peer as no peers from the list %s are currently available", w.handler, peers) case chosen != currID: // we compare the current score + 10ms to the chosen score to avoid flapping between routes if currScore != 0 && currScore+0.01 > chosenScore { log.Debugf("keeping current routing peer %s for [%v]: the score difference with latency is less than 0.01(10ms): current: %f, new: %f", w.currentChosen.Peer, w.handler, currScore, chosenScore) - return currID + return currID, chosenStatus } var p string if rt := w.routes[chosen]; rt != nil { @@ -219,7 +234,7 @@ func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]router log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, w.handler) } - return chosen + return chosen, chosenStatus } func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) { @@ -279,10 +294,28 @@ func (w *Watcher) removeAllowedIPs(route *route.Route, rsn reason) error { return nil } +// shouldSkipRecalculation checks if we can skip route recalculation for the same route without status changes +func (w *Watcher) shouldSkipRecalculation(newChosenID route.ID, newStatus routerPeerStatus) bool { + if w.currentChosen == nil { + return false + } + + isSameRoute := w.currentChosen.ID == newChosenID && w.currentChosen.Equal(w.routes[newChosenID]) + if !isSameRoute { + return false + } + + if w.currentChosenStatus != nil { + return w.currentChosenStatus.status == newStatus.status + } + + return true +} + func (w *Watcher) recalculateRoutes(rsn reason) error { routerPeerStatuses := w.getRouterPeerStatuses() - newChosenID := w.getBestRouteFromStatuses(routerPeerStatuses) + newChosenID, newStatus := w.getBestRouteFromStatuses(routerPeerStatuses) // If no route is chosen, remove the route from the peer if newChosenID == "" { @@ -295,13 +328,13 @@ func (w *Watcher) recalculateRoutes(rsn reason) error { } w.currentChosen = nil + w.currentChosenStatus = nil return nil } - // If the chosen route is the same as the current route, do nothing - if w.currentChosen != nil && w.currentChosen.ID == newChosenID && - w.currentChosen.Equal(w.routes[newChosenID]) { + // If we can skip recalculation for the same route without changes, do nothing + if w.shouldSkipRecalculation(newChosenID, newStatus) { return nil } @@ -316,8 +349,12 @@ func (w *Watcher) recalculateRoutes(rsn reason) error { if err := w.addAllowedIPs(newChosenRoute); err != nil { return fmt.Errorf("add new: %w", err) } + if newStatus.status != peer.StatusIdle { + w.connectEvent(newChosenRoute) + } w.currentChosen = newChosenRoute + w.currentChosenStatus = &newStatus return nil } @@ -497,6 +534,7 @@ func (w *Watcher) Stop() { if err := w.removeAllowedIPs(w.currentChosen, reasonShutdown); err != nil { log.Errorf("Failed to remove routes for [%v]: %v", w.handler, err) } + w.currentChosenStatus = nil } func HandlerFromRoute( diff --git a/client/internal/routemanager/client/client_bench_test.go b/client/internal/routemanager/client/client_bench_test.go new file mode 100644 index 000000000..1fc41ec33 --- /dev/null +++ b/client/internal/routemanager/client/client_bench_test.go @@ -0,0 +1,155 @@ +package client + +import ( + "context" + "fmt" + "net/netip" + "sync" + "testing" + "time" + + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/route" +) + +type benchmarkTier struct { + name string + peers int + routes int + haPeersPerGroup int +} + +var benchmarkTiers = []benchmarkTier{ + {"Small", 100, 50, 4}, + {"Medium", 1000, 200, 16}, + {"Large", 5000, 500, 32}, +} + +type mockRouteHandler struct { + network string +} + +func (m *mockRouteHandler) String() string { return m.network } +func (m *mockRouteHandler) AddRoute(context.Context) error { return nil } +func (m *mockRouteHandler) RemoveRoute() error { return nil } +func (m *mockRouteHandler) AddAllowedIPs(string) error { return nil } +func (m *mockRouteHandler) RemoveAllowedIPs() error { return nil } + +func generateBenchmarkData(tier benchmarkTier) (*peer.Status, map[route.ID]*route.Route) { + statusRecorder := peer.NewRecorder("test-mgm") + routes := make(map[route.ID]*route.Route) + + peerKeys := make([]string, tier.peers) + for i := 0; i < tier.peers; i++ { + peerKey := fmt.Sprintf("peer-%d", i) + peerKeys[i] = peerKey + fqdn := fmt.Sprintf("peer-%d.example.com", i) + ip := fmt.Sprintf("10.0.%d.%d", i/256, i%256) + + err := statusRecorder.AddPeer(peerKey, fqdn, ip) + if err != nil { + panic(fmt.Sprintf("failed to add peer: %v", err)) + } + + var status peer.ConnStatus + var latency time.Duration + relayed := false + + switch i % 10 { + case 0, 1: // 20% disconnected + status = peer.StatusConnecting + latency = 0 + case 2: // 10% idle + status = peer.StatusIdle + latency = 50 * time.Millisecond + case 3, 4: // 20% relayed + status = peer.StatusConnected + relayed = true + latency = time.Duration(50+i%100) * time.Millisecond + default: // 50% direct connection + status = peer.StatusConnected + latency = time.Duration(10+i%40) * time.Millisecond + } + + // Update peer state + state := peer.State{ + PubKey: peerKey, + IP: ip, + FQDN: fqdn, + ConnStatus: status, + ConnStatusUpdate: time.Now(), + Relayed: relayed, + Latency: latency, + Mux: &sync.RWMutex{}, + } + + err = statusRecorder.UpdatePeerState(state) + if err != nil { + panic(fmt.Sprintf("failed to update peer state: %v", err)) + } + } + + routeID := 0 + for i := 0; i < tier.routes; i++ { + network := fmt.Sprintf("192.168.%d.0/24", i%256) + prefix := netip.MustParsePrefix(network) + + haGroupSize := 1 + if i%4 == 0 { // 25% of routes have HA + haGroupSize = tier.haPeersPerGroup + } + + for j := 0; j < haGroupSize; j++ { + peerIndex := (i*tier.haPeersPerGroup + j) % tier.peers + peerKey := peerKeys[peerIndex] + + rID := route.ID(fmt.Sprintf("route-%d-%d", i, j)) + + metric := 100 + j*10 + + routes[rID] = &route.Route{ + ID: rID, + Network: prefix, + Peer: peerKey, + Metric: metric, + NetID: route.NetID(fmt.Sprintf("net-%d", i)), + } + routeID++ + } + } + + return statusRecorder, routes +} + +// Benchmark the optimized recalculate routes +func BenchmarkRecalculateRoutes(b *testing.B) { + for _, tier := range benchmarkTiers { + b.Run(tier.name, func(b *testing.B) { + statusRecorder, routes := generateBenchmarkData(tier) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + watcher := &Watcher{ + ctx: ctx, + statusRecorder: statusRecorder, + routes: routes, + routePeersNotifiers: make(map[string]chan struct{}), + routeUpdate: make(chan RoutesUpdate), + peerStateUpdate: make(chan struct{}), + handler: &mockRouteHandler{network: "benchmark"}, + currentChosenStatus: nil, + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + err := watcher.recalculateRoutes(reasonPeerUpdate) + if err != nil { + b.Fatalf("recalculateRoutes failed: %v", err) + } + } + }) + } +} diff --git a/client/internal/routemanager/client/client_test.go b/client/internal/routemanager/client/client_test.go index 48a9495bf..e7aff28b6 100644 --- a/client/internal/routemanager/client/client_test.go +++ b/client/internal/routemanager/client/client_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager/static" "github.com/netbirdio/netbird/route" ) @@ -23,8 +24,8 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "one route", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, + status: peer.StatusConnected, + relayed: false, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -41,8 +42,8 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "one connected routes with relayed and direct", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: true, + status: peer.StatusConnected, + relayed: true, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -59,8 +60,8 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "one connected routes with relayed and no direct", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: true, + status: peer.StatusConnected, + relayed: true, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -77,8 +78,8 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "no connected peers", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: false, - relayed: false, + status: peer.StatusConnecting, + relayed: false, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -95,12 +96,12 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "multiple connected peers with different metrics", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, + status: peer.StatusConnected, + relayed: false, }, "route2": { - connected: true, - relayed: false, + status: peer.StatusConnected, + relayed: false, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -122,12 +123,12 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "multiple connected peers with one relayed", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, + status: peer.StatusConnected, + relayed: false, }, "route2": { - connected: true, - relayed: true, + status: peer.StatusConnected, + relayed: true, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -149,12 +150,12 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "multiple connected peers with different latencies", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - latency: 300 * time.Millisecond, + status: peer.StatusConnected, + latency: 300 * time.Millisecond, }, "route2": { - connected: true, - latency: 10 * time.Millisecond, + status: peer.StatusConnected, + latency: 10 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -176,12 +177,12 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "should ignore routes with latency 0", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - latency: 0 * time.Millisecond, + status: peer.StatusConnected, + latency: 0 * time.Millisecond, }, "route2": { - connected: true, - latency: 10 * time.Millisecond, + status: peer.StatusConnected, + latency: 10 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -203,14 +204,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "current route with similar score and similar but slightly worse latency should not change", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, - latency: 15 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 15 * time.Millisecond, }, "route2": { - connected: true, - relayed: false, - latency: 10 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 10 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -232,14 +233,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "relayed routes with latency 0 should maintain previous choice", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: true, - latency: 0 * time.Millisecond, + status: peer.StatusConnected, + relayed: true, + latency: 0 * time.Millisecond, }, "route2": { - connected: true, - relayed: true, - latency: 0 * time.Millisecond, + status: peer.StatusConnected, + relayed: true, + latency: 0 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -261,14 +262,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "p2p routes with latency 0 should maintain previous choice", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, - latency: 0 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 0 * time.Millisecond, }, "route2": { - connected: true, - relayed: false, - latency: 0 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 0 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -290,14 +291,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "current route with bad score should be changed to route with better score", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, - latency: 200 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 200 * time.Millisecond, }, "route2": { - connected: true, - relayed: false, - latency: 10 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 10 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -319,14 +320,14 @@ func TestGetBestrouteFromStatuses(t *testing.T) { name: "current chosen route doesn't exist anymore", statuses: map[route.ID]routerPeerStatus{ "route1": { - connected: true, - relayed: false, - latency: 20 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 20 * time.Millisecond, }, "route2": { - connected: true, - relayed: false, - latency: 10 * time.Millisecond, + status: peer.StatusConnected, + relayed: false, + latency: 10 * time.Millisecond, }, }, existingRoutes: map[route.ID]*route.Route{ @@ -344,6 +345,422 @@ func TestGetBestrouteFromStatuses(t *testing.T) { currentRoute: "routeDoesntExistAnymore", expectedRouteID: "route2", }, + { + name: "connected peer should be preferred over idle peer", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnected, + relayed: false, + latency: 100 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, + { + name: "idle peer should be selected when no connected peers", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnecting, + relayed: false, + latency: 5 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route1", + }, + { + name: "best idle peer should be selected among multiple idle peers", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 100 * time.Millisecond, + }, + "route2": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, + { + name: "connecting peers should not be considered for routing", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusConnecting, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnecting, + relayed: false, + latency: 5 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "", + }, + { + name: "mixed statuses - connected wins over idle and connecting", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusConnecting, + relayed: false, + latency: 5 * time.Millisecond, + }, + "route2": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route3": { + status: peer.StatusConnected, + relayed: true, + latency: 200 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + "route3": { + ID: "route3", + Metric: route.MaxMetric, + Peer: "peer3", + }, + }, + currentRoute: "", + expectedRouteID: "route3", + }, + { + name: "idle peer with better metric should win over idle peer with worse metric", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 50 * time.Millisecond, + }, + "route2": { + status: peer.StatusIdle, + relayed: false, + latency: 50 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: 5000, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route1", + }, + { + name: "current idle route should be maintained for similar scores", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 20 * time.Millisecond, + }, + "route2": { + status: peer.StatusIdle, + relayed: false, + latency: 15 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "route1", + expectedRouteID: "route1", + }, + { + name: "idle peer with zero latency should still be considered", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 0 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnecting, + relayed: false, + latency: 10 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route1", + }, + { + name: "direct idle peer preferred over relayed idle peer", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: true, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusIdle, + relayed: false, + latency: 50 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, + { + name: "connected peer with worse metric still beats idle peer with better metric", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnected, + relayed: false, + latency: 50 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: 1000, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, + { + name: "connected peer wins even when idle peer has all advantages", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 1 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnected, + relayed: true, + latency: 30 * time.Minute, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: 1, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, + { + name: "connected peer should be preferred over idle peer", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnected, + relayed: false, + latency: 100 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, + { + name: "idle peer should be selected when no connected peers", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + "route2": { + status: peer.StatusConnecting, + relayed: false, + latency: 5 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route1", + }, + { + name: "best idle peer should be selected among multiple idle peers", + statuses: map[route.ID]routerPeerStatus{ + "route1": { + status: peer.StatusIdle, + relayed: false, + latency: 100 * time.Millisecond, + }, + "route2": { + status: peer.StatusIdle, + relayed: false, + latency: 10 * time.Millisecond, + }, + }, + existingRoutes: map[route.ID]*route.Route{ + "route1": { + ID: "route1", + Metric: route.MaxMetric, + Peer: "peer1", + }, + "route2": { + ID: "route2", + Metric: route.MaxMetric, + Peer: "peer2", + }, + }, + currentRoute: "", + expectedRouteID: "route2", + }, } // fill the test data with random routes @@ -368,18 +785,18 @@ func TestGetBestrouteFromStatuses(t *testing.T) { for i := 0; i < 50; i++ { id := route.ID(fmt.Sprintf("dummy_p1_%d", i)) dummyStatus := routerPeerStatus{ - connected: false, - relayed: true, - latency: 0, + status: peer.StatusConnecting, + relayed: true, + latency: 0, } tc.statuses[id] = dummyStatus } for i := 0; i < 50; i++ { id := route.ID(fmt.Sprintf("dummy_p2_%d", i)) dummyStatus := routerPeerStatus{ - connected: false, - relayed: true, - latency: 0, + status: peer.StatusConnecting, + relayed: true, + latency: 0, } tc.statuses[id] = dummyStatus } @@ -401,7 +818,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) { currentChosen: currentRoute, } - chosenRoute := client.getBestRouteFromStatuses(tc.statuses) + chosenRoute, _ := client.getBestRouteFromStatuses(tc.statuses) if chosenRoute != tc.expectedRouteID { t.Errorf("expected routeID %s, got %s", tc.expectedRouteID, chosenRoute) } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 8dbbb5f77..93a3788eb 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -41,7 +41,8 @@ import ( // Manager is a route manager interface type Manager interface { Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) - UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, useNewDNSRoute bool) error + UpdateRoutes(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error + ClassifyRoutes(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) TriggerSelection(route.HAMap) GetRouteSelector() *routeselector.RouteSelector GetClientRoutes() route.HAMap @@ -319,7 +320,12 @@ func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error { return nberrors.FormatErrorOrNil(merr) } -func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, useNewDNSRoute bool) error { +func (m *DefaultManager) UpdateRoutes( + updateSerial uint64, + serverRoutes map[route.ID]*route.Route, + clientRoutes route.HAMap, + useNewDNSRoute bool, +) error { select { case <-m.ctx.Done(): log.Infof("not updating routes as context is closed") @@ -331,11 +337,9 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro defer m.mux.Unlock() m.useNewDNSRoute = useNewDNSRoute - newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes) - var merr *multierror.Error if !m.disableClientRoutes { - filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap) + filteredClientRoutes := m.routeSelector.FilterSelected(clientRoutes) if err := m.updateSystemRoutes(filteredClientRoutes); err != nil { merr = multierror.Append(merr, fmt.Errorf("update system routes: %w", err)) @@ -344,13 +348,13 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro m.updateClientNetworks(updateSerial, filteredClientRoutes) m.notifier.OnNewRoutes(filteredClientRoutes) } - m.clientRoutes = newClientRoutesIDMap + m.clientRoutes = clientRoutes if m.serverRouter == nil { return nberrors.FormatErrorOrNil(merr) } - if err := m.serverRouter.UpdateRoutes(newServerRoutesMap, useNewDNSRoute); err != nil { + if err := m.serverRouter.UpdateRoutes(serverRoutes, useNewDNSRoute); err != nil { merr = multierror.Append(merr, fmt.Errorf("update server routes: %w", err)) } @@ -481,7 +485,7 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks rout } } -func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) { +func (m *DefaultManager) ClassifyRoutes(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) { newClientRoutesIDMap := make(route.HAMap) newServerRoutesMap := make(map[route.ID]*route.Route) ownNetworkIDs := make(map[route.HAUniqueID]bool) @@ -508,7 +512,7 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID] } func (m *DefaultManager) initialClientRoutes(initialRoutes []*route.Route) []*route.Route { - _, crMap := m.classifyRoutes(initialRoutes) + _, crMap := m.ClassifyRoutes(initialRoutes) rs := make([]*route.Route, 0, len(crMap)) for _, routes := range crMap { rs = append(rs, routes...) diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index a46ae080e..486ee080a 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -439,12 +439,14 @@ func TestManagerUpdateRoutes(t *testing.T) { routeManager.serverRouter = nil } + serverRoutes, clientRoutes := routeManager.ClassifyRoutes(testCase.inputRoutes) + if len(testCase.inputInitRoutes) > 0 { - err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes, false) + err = routeManager.UpdateRoutes(testCase.inputSerial, serverRoutes, clientRoutes, false) require.NoError(t, err, "should update routes with init routes") } - err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes, false) + err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), serverRoutes, clientRoutes, false) require.NoError(t, err, "should update routes") expectedWatchers := testCase.clientNetworkWatchersExpected diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index 64fdffceb..63bad689e 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -14,7 +14,8 @@ import ( // MockManager is the mock instance of a route manager type MockManager struct { - UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) error + ClassifyRoutesFunc func(routes []*route.Route) (map[route.ID]*route.Route, route.HAMap) + UpdateRoutesFunc func (updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error TriggerSelectionFunc func(haMap route.HAMap) GetRouteSelectorFunc func() *routeselector.RouteSelector GetClientRoutesFunc func() route.HAMap @@ -32,13 +33,21 @@ func (m *MockManager) InitialRouteRange() []string { } // UpdateRoutes mock implementation of UpdateRoutes from Manager interface -func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, b bool) error { +func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error { if m.UpdateRoutesFunc != nil { - return m.UpdateRoutesFunc(updateSerial, newRoutes) + return m.UpdateRoutesFunc(updateSerial, newRoutes, clientRoutes, useNewDNSRoute) } return nil } +// ClassifyRoutes mock implementation of ClassifyRoutes from Manager interface +func (m *MockManager) ClassifyRoutes(routes []*route.Route) (map[route.ID]*route.Route, route.HAMap) { + if m.ClassifyRoutesFunc != nil { + return m.ClassifyRoutesFunc(routes) + } + return nil, nil +} + func (m *MockManager) TriggerSelection(networks route.HAMap) { if m.TriggerSelectionFunc != nil { m.TriggerSelectionFunc(networks) From de27d6df3650bf085afa1eaa1f6084b65bae683c Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 9 Jun 2025 14:34:53 +0300 Subject: [PATCH 068/213] [management] Add account ID index to activity events (#3946) --- management/server/activity/event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 5bcc67fbc..8fd5e3371 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -31,7 +31,7 @@ type Event struct { // TargetID is the ID of an object that was effected by the event (e.g., a peer) TargetID string // AccountID is the ID of an account where the event happened - AccountID string + AccountID string `gorm:"index"` // Meta of the event, e.g. deleted peer information like name, IP, etc Meta map[string]any `gorm:"serializer:json"` From 6127a011965268613dd0b7f8145655cefca8d62b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:26:28 +0200 Subject: [PATCH 069/213] [client] Remove strings from allowed IPs (#3920) --- client/iface/configurer/common.go | 17 +++++++++++ client/iface/configurer/kernel_unix.go | 29 +++++++++++-------- client/iface/configurer/usp.go | 23 ++++++++------- client/iface/device/interface.go | 7 +++-- client/iface/iface.go | 23 ++++----------- client/internal/engine_test.go | 8 ++--- client/internal/iface_common.go | 4 +-- .../routemanager/iface/iface_common.go | 5 ++-- client/internal/routemanager/manager.go | 4 +-- 9 files changed, 68 insertions(+), 52 deletions(-) create mode 100644 client/iface/configurer/common.go diff --git a/client/iface/configurer/common.go b/client/iface/configurer/common.go new file mode 100644 index 000000000..088cff69d --- /dev/null +++ b/client/iface/configurer/common.go @@ -0,0 +1,17 @@ +package configurer + +import ( + "net" + "net/netip" +) + +func prefixesToIPNets(prefixes []netip.Prefix) []net.IPNet { + ipNets := make([]net.IPNet, len(prefixes)) + for i, prefix := range prefixes { + ipNets[i] = net.IPNet{ + IP: prefix.Addr().AsSlice(), // Convert netip.Addr to net.IP + Mask: net.CIDRMask(prefix.Bits(), prefix.Addr().BitLen()), // Create subnet mask + } + } + return ipNets +} diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go index 91991177e..4922a54fc 100644 --- a/client/iface/configurer/kernel_unix.go +++ b/client/iface/configurer/kernel_unix.go @@ -5,6 +5,7 @@ package configurer import ( "fmt" "net" + "net/netip" "time" log "github.com/sirupsen/logrus" @@ -45,7 +46,7 @@ func (c *KernelConfigurer) ConfigureInterface(privateKey string, port int) error return nil } -func (c *KernelConfigurer) UpdatePeer(peerKey string, allowedIps []net.IPNet, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { +func (c *KernelConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { peerKeyParsed, err := wgtypes.ParseKey(peerKey) if err != nil { return err @@ -54,7 +55,7 @@ func (c *KernelConfigurer) UpdatePeer(peerKey string, allowedIps []net.IPNet, ke PublicKey: peerKeyParsed, ReplaceAllowedIPs: false, // don't replace allowed ips, wg will handle duplicated peer IP - AllowedIPs: allowedIps, + AllowedIPs: prefixesToIPNets(allowedIps), PersistentKeepaliveInterval: &keepAlive, Endpoint: endpoint, PresharedKey: preSharedKey, @@ -91,10 +92,10 @@ func (c *KernelConfigurer) RemovePeer(peerKey string) error { return nil } -func (c *KernelConfigurer) AddAllowedIP(peerKey string, allowedIP string) error { - _, ipNet, err := net.ParseCIDR(allowedIP) - if err != nil { - return err +func (c *KernelConfigurer) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { + ipNet := net.IPNet{ + IP: allowedIP.Addr().AsSlice(), + Mask: net.CIDRMask(allowedIP.Bits(), allowedIP.Addr().BitLen()), } peerKeyParsed, err := wgtypes.ParseKey(peerKey) @@ -105,7 +106,7 @@ func (c *KernelConfigurer) AddAllowedIP(peerKey string, allowedIP string) error PublicKey: peerKeyParsed, UpdateOnly: true, ReplaceAllowedIPs: false, - AllowedIPs: []net.IPNet{*ipNet}, + AllowedIPs: []net.IPNet{ipNet}, } config := wgtypes.Config{ @@ -118,10 +119,10 @@ func (c *KernelConfigurer) AddAllowedIP(peerKey string, allowedIP string) error return nil } -func (c *KernelConfigurer) RemoveAllowedIP(peerKey string, allowedIP string) error { - _, ipNet, err := net.ParseCIDR(allowedIP) - if err != nil { - return fmt.Errorf("parse allowed IP: %w", err) +func (c *KernelConfigurer) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error { + ipNet := net.IPNet{ + IP: allowedIP.Addr().AsSlice(), + Mask: net.CIDRMask(allowedIP.Bits(), allowedIP.Addr().BitLen()), } peerKeyParsed, err := wgtypes.ParseKey(peerKey) @@ -189,7 +190,11 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error { if err != nil { return err } - defer wg.Close() + defer func() { + if err := wg.Close(); err != nil { + log.Errorf("Failed to close wgctrl client: %v", err) + } + }() // validate if device with name exists _, err = wg.Device(c.deviceName) diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 914788821..79ce91eea 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "net" + "net/netip" "os" "runtime" "strconv" @@ -67,7 +68,7 @@ func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error return c.device.IpcSet(toWgUserspaceString(config)) } -func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []net.IPNet, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { +func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { peerKeyParsed, err := wgtypes.ParseKey(peerKey) if err != nil { return err @@ -76,7 +77,7 @@ func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []net.IPNet, kee PublicKey: peerKeyParsed, ReplaceAllowedIPs: false, // don't replace allowed ips, wg will handle duplicated peer IP - AllowedIPs: allowedIps, + AllowedIPs: prefixesToIPNets(allowedIps), PersistentKeepaliveInterval: &keepAlive, PresharedKey: preSharedKey, Endpoint: endpoint, @@ -106,10 +107,10 @@ func (c *WGUSPConfigurer) RemovePeer(peerKey string) error { return c.device.IpcSet(toWgUserspaceString(config)) } -func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP string) error { - _, ipNet, err := net.ParseCIDR(allowedIP) - if err != nil { - return err +func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { + ipNet := net.IPNet{ + IP: allowedIP.Addr().AsSlice(), + Mask: net.CIDRMask(allowedIP.Bits(), allowedIP.Addr().BitLen()), } peerKeyParsed, err := wgtypes.ParseKey(peerKey) @@ -120,7 +121,7 @@ func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP string) error { PublicKey: peerKeyParsed, UpdateOnly: true, ReplaceAllowedIPs: false, - AllowedIPs: []net.IPNet{*ipNet}, + AllowedIPs: []net.IPNet{ipNet}, } config := wgtypes.Config{ @@ -130,7 +131,7 @@ func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP string) error { return c.device.IpcSet(toWgUserspaceString(config)) } -func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, ip string) error { +func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error { ipc, err := c.device.IpcGet() if err != nil { return err @@ -153,6 +154,8 @@ func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, ip string) error { foundPeer := false removedAllowedIP := false + ip := allowedIP.String() + for _, line := range lines { line = strings.TrimSpace(line) @@ -175,8 +178,8 @@ func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, ip string) error { // Append the line to the output string if foundPeer && strings.HasPrefix(line, "allowed_ip=") { - allowedIP := strings.TrimPrefix(line, "allowed_ip=") - _, ipNet, err := net.ParseCIDR(allowedIP) + allowedIPStr := strings.TrimPrefix(line, "allowed_ip=") + _, ipNet, err := net.ParseCIDR(allowedIPStr) if err != nil { return err } diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go index 31ebdf4b8..296eb7dda 100644 --- a/client/iface/device/interface.go +++ b/client/iface/device/interface.go @@ -2,6 +2,7 @@ package device import ( "net" + "net/netip" "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -11,10 +12,10 @@ import ( type WGConfigurer interface { ConfigureInterface(privateKey string, port int) error - UpdatePeer(peerKey string, allowedIps []net.IPNet, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error + UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error RemovePeer(peerKey string) error - AddAllowedIP(peerKey string, allowedIP string) error - RemoveAllowedIP(peerKey string, allowedIP string) error + AddAllowedIP(peerKey string, allowedIP netip.Prefix) error + RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error Close() GetStats() (map[string]configurer.WGStats, error) FullStats() (*configurer.Stats, error) diff --git a/client/iface/iface.go b/client/iface/iface.go index f4394c476..7d609f4cd 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -111,14 +111,14 @@ func (w *WGIface) UpdateAddr(newAddr string) error { } // UpdatePeer updates existing Wireguard Peer or creates a new one if doesn't exist -// Endpoint is optional +// Endpoint is optional. +// If allowedIps is given it will be added to the existing ones. func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { w.mu.Lock() defer w.mu.Unlock() - netIPNets := prefixesToIPNets(allowedIps) - log.Debugf("updating interface %s peer %s, endpoint %s", w.tun.DeviceName(), peerKey, endpoint) - return w.configurer.UpdatePeer(peerKey, netIPNets, keepAlive, endpoint, preSharedKey) + log.Debugf("updating interface %s peer %s, endpoint %s, allowedIPs %v", w.tun.DeviceName(), peerKey, endpoint, allowedIps) + return w.configurer.UpdatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey) } // RemovePeer removes a Wireguard Peer from the interface iface @@ -131,7 +131,7 @@ func (w *WGIface) RemovePeer(peerKey string) error { } // AddAllowedIP adds a prefix to the allowed IPs list of peer -func (w *WGIface) AddAllowedIP(peerKey string, allowedIP string) error { +func (w *WGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { w.mu.Lock() defer w.mu.Unlock() @@ -140,7 +140,7 @@ func (w *WGIface) AddAllowedIP(peerKey string, allowedIP string) error { } // RemoveAllowedIP removes a prefix from the allowed IPs list of peer -func (w *WGIface) RemoveAllowedIP(peerKey string, allowedIP string) error { +func (w *WGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error { w.mu.Lock() defer w.mu.Unlock() @@ -254,14 +254,3 @@ func (w *WGIface) GetNet() *netstack.Net { return w.tun.GetNet() } - -func prefixesToIPNets(prefixes []netip.Prefix) []net.IPNet { - ipNets := make([]net.IPNet, len(prefixes)) - for i, prefix := range prefixes { - ipNets[i] = net.IPNet{ - IP: net.IP(prefix.Addr().AsSlice()), // Convert netip.Addr to net.IP - Mask: net.CIDRMask(prefix.Bits(), prefix.Addr().BitLen()), // Create subnet mask - } - } - return ipNets -} diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index a4470e0ec..8c084e366 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -86,8 +86,8 @@ type MockWGIface struct { UpdateAddrFunc func(newAddr string) error UpdatePeerFunc func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error RemovePeerFunc func(peerKey string) error - AddAllowedIPFunc func(peerKey string, allowedIP string) error - RemoveAllowedIPFunc func(peerKey string, allowedIP string) error + AddAllowedIPFunc func(peerKey string, allowedIP netip.Prefix) error + RemoveAllowedIPFunc func(peerKey string, allowedIP netip.Prefix) error CloseFunc func() error SetFilterFunc func(filter device.PacketFilter) error GetFilterFunc func() device.PacketFilter @@ -147,11 +147,11 @@ func (m *MockWGIface) RemovePeer(peerKey string) error { return m.RemovePeerFunc(peerKey) } -func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error { +func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { return m.AddAllowedIPFunc(peerKey, allowedIP) } -func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error { +func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error { return m.RemoveAllowedIPFunc(peerKey, allowedIP) } diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index 95bf146f9..999472411 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -28,8 +28,8 @@ type wgIfaceBase interface { GetProxy() wgproxy.Proxy UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error RemovePeer(peerKey string) error - AddAllowedIP(peerKey string, allowedIP string) error - RemoveAllowedIP(peerKey string, allowedIP string) error + AddAllowedIP(peerKey string, allowedIP netip.Prefix) error + RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error Close() error SetFilter(filter device.PacketFilter) error GetFilter() device.PacketFilter diff --git a/client/internal/routemanager/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go index 02d3f4c6f..f844f4bed 100644 --- a/client/internal/routemanager/iface/iface_common.go +++ b/client/internal/routemanager/iface/iface_common.go @@ -2,14 +2,15 @@ package iface import ( "net" + "net/netip" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" ) type wgIfaceBase interface { - AddAllowedIP(peerKey string, allowedIP string) error - RemoveAllowedIP(peerKey string, allowedIP string) error + AddAllowedIP(peerKey string, allowedIP netip.Prefix) error + RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error Name() string Address() wgaddr.Address diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 93a3788eb..919bf25e3 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -159,10 +159,10 @@ func (m *DefaultManager) setupRefCounters(useNoop bool) { m.allowedIPsRefCounter = refcounter.New( func(prefix netip.Prefix, peerKey string) (string, error) { // save peerKey to use it in the remove function - return peerKey, m.wgInterface.AddAllowedIP(peerKey, prefix.String()) + return peerKey, m.wgInterface.AddAllowedIP(peerKey, prefix) }, func(prefix netip.Prefix, peerKey string) error { - if err := m.wgInterface.RemoveAllowedIP(peerKey, prefix.String()); err != nil { + if err := m.wgInterface.RemoveAllowedIP(peerKey, prefix); err != nil { if !errors.Is(err, configurer.ErrPeerNotFound) && !errors.Is(err, configurer.ErrAllowedIPNotFound) { return err } From e002a2e6e8eb55af89f1ec538cc4528c5ecf8b96 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:27:06 +0200 Subject: [PATCH 070/213] [client] Add more advanced settings to UI (#3941) --- client/proto/daemon.pb.go | 886 ++++++++++++++++++++----------------- client/proto/daemon.proto | 10 + client/server/server.go | 16 +- client/ui/client_ui.go | 210 +++------ client/ui/event_handler.go | 204 +++++++++ 5 files changed, 749 insertions(+), 577 deletions(-) create mode 100644 client/ui/event_handler.go diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index b88d0aa31..6c4c2dc93 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1008,6 +1008,11 @@ type GetConfigResponse struct { DisableNotifications bool `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"` LazyConnectionEnabled bool `protobuf:"varint,14,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` BlockInbound bool `protobuf:"varint,15,opt,name=blockInbound,proto3" json:"blockInbound,omitempty"` + NetworkMonitor bool `protobuf:"varint,16,opt,name=networkMonitor,proto3" json:"networkMonitor,omitempty"` + DisableDns bool `protobuf:"varint,17,opt,name=disable_dns,json=disableDns,proto3" json:"disable_dns,omitempty"` + DisableClientRoutes bool `protobuf:"varint,18,opt,name=disable_client_routes,json=disableClientRoutes,proto3" json:"disable_client_routes,omitempty"` + DisableServerRoutes bool `protobuf:"varint,19,opt,name=disable_server_routes,json=disableServerRoutes,proto3" json:"disable_server_routes,omitempty"` + BlockLanAccess bool `protobuf:"varint,20,opt,name=block_lan_access,json=blockLanAccess,proto3" json:"block_lan_access,omitempty"` } func (x *GetConfigResponse) Reset() { @@ -1140,6 +1145,41 @@ func (x *GetConfigResponse) GetBlockInbound() bool { return false } +func (x *GetConfigResponse) GetNetworkMonitor() bool { + if x != nil { + return x.NetworkMonitor + } + return false +} + +func (x *GetConfigResponse) GetDisableDns() bool { + if x != nil { + return x.DisableDns + } + return false +} + +func (x *GetConfigResponse) GetDisableClientRoutes() bool { + if x != nil { + return x.DisableClientRoutes + } + return false +} + +func (x *GetConfigResponse) GetDisableServerRoutes() bool { + if x != nil { + return x.DisableServerRoutes + } + return false +} + +func (x *GetConfigResponse) GetBlockLanAccess() bool { + if x != nil { + return x.BlockLanAccess + } + return false +} + // PeerState contains the latest state of a peer type PeerState struct { state protoimpl.MessageState @@ -3792,7 +3832,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc8, 0x04, 0x0a, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x06, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, @@ -3829,428 +3869,442 @@ var file_daemon_proto_rawDesc = []byte{ 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, - 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, - 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, - 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, - 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, - 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, + 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, + 0x1f, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6e, 0x73, + 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x13, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, + 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, + 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, + 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, + 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, + 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, + 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, - 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, - 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, - 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, - 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, - 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, - 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, - 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, - 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, - 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, - 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, - 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, - 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, - 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, - 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, - 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, - 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, - 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, - 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, - 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, - 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, - 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, - 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, - 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x22, 0xef, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, - 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, - 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, - 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, - 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, - 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, - 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, - 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, - 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, - 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, - 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, - 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, - 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, - 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, - 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, - 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, - 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, - 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, - 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, - 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, - 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, - 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, - 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, - 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, - 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, - 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, - 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, - 0x73, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, - 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, - 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, - 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, - 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, - 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, - 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, - 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, - 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, + 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, + 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, + 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, + 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, + 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, + 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, + 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, + 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, + 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, + 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xef, 0x03, 0x0a, + 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, + 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, + 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, + 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x15, + 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, + 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, + 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, + 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, + 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, + 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, + 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, + 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, 0x01, 0x0a, 0x08, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, + 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, + 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, + 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, + 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, + 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, + 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, + 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, + 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x88, 0x01, 0x0a, + 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, + 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, + 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, + 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, - 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, - 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, - 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, - 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, - 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, - 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, - 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, - 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, - 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, - 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, - 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, - 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, - 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, - 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, - 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, - 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, - 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, - 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, - 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, - 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, - 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, - 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, - 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, - 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, - 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, - 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, - 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, - 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, - 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, - 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, - 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, - 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, - 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, - 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, - 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, - 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, - 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, - 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, - 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, - 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, - 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, - 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, - 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, - 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, - 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, - 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, - 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, - 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, - 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, - 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, - 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, - 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, - 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, - 0x0b, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, - 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, - 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, + 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, + 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, + 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, + 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, + 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, + 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, + 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, + 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, + 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, + 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, + 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, + 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, + 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, + 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, + 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, + 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, + 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, + 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, + 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, + 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, + 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, + 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, + 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, + 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, + 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, + 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, + 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, + 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, + 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, + 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, + 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, + 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, 0x0a, 0x0d, 0x44, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, + 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, + 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, + 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, - 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, - 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, - 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, - 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, - 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, - 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, - 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, + 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, + 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, + 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, + 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, - 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, - 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, - 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, + 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, + 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, + 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, + 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index a46ba554a..2a1a49d1b 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -207,6 +207,16 @@ message GetConfigResponse { bool lazyConnectionEnabled = 14; bool blockInbound = 15; + + bool networkMonitor = 16; + + bool disable_dns = 17; + + bool disable_client_routes = 18; + + bool disable_server_routes = 19; + + bool block_lan_access = 20; } // PeerState contains the latest state of a peer diff --git a/client/server/server.go b/client/server/server.go index 2025a89ec..72837b59d 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -750,7 +750,6 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto if preSharedKey != "" { preSharedKey = "**********" } - } disableNotifications := true @@ -758,6 +757,16 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto disableNotifications = *s.config.DisableNotifications } + networkMonitor := false + if s.config.NetworkMonitor != nil { + networkMonitor = *s.config.NetworkMonitor + } + + disableDNS := s.config.DisableDNS + disableClientRoutes := s.config.DisableClientRoutes + disableServerRoutes := s.config.DisableServerRoutes + blockLANAccess := s.config.BlockLANAccess + return &proto.GetConfigResponse{ ManagementUrl: managementURL, ConfigFile: s.latestConfigInput.ConfigPath, @@ -773,6 +782,11 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto LazyConnectionEnabled: s.config.LazyConnectionEnabled, BlockInbound: s.config.BlockInbound, DisableNotifications: disableNotifications, + NetworkMonitor: networkMonitor, + DisableDns: disableDNS, + DisableClientRoutes: disableClientRoutes, + DisableServerRoutes: disableServerRoutes, + BlockLanAccess: blockLANAccess, }, nil } diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index f0202b8e7..a501043d2 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -170,6 +170,8 @@ type serviceClient struct { addr string conn proto.DaemonServiceClient + eventHandler *eventHandler + icAbout []byte icConnected []byte icDisconnected []byte @@ -217,6 +219,11 @@ type serviceClient struct { // switch elements for settings form sRosenpassPermissive *widget.Check + sNetworkMonitor *widget.Check + sDisableDNS *widget.Check + sDisableClientRoutes *widget.Check + sDisableServerRoutes *widget.Check + sBlockLANAccess *widget.Check // observable settings over corresponding iMngURL and iPreSharedKey values. managementURL string @@ -225,6 +232,11 @@ type serviceClient struct { RosenpassPermissive bool interfaceName string interfacePort int + networkMonitor bool + disableDNS bool + disableClientRoutes bool + disableServerRoutes bool + blockLANAccess bool connected bool update *version.Update @@ -266,6 +278,7 @@ func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool update: version.NewUpdate(), } + s.eventHandler = newEventHandler(s) s.setNewIcons() switch { @@ -332,14 +345,20 @@ func (s *serviceClient) showSettingsUI() { s.iPreSharedKey = widget.NewPasswordEntry() s.iInterfaceName = widget.NewEntry() s.iInterfacePort = widget.NewEntry() + s.sRosenpassPermissive = widget.NewCheck("Enable Rosenpass permissive mode", nil) + s.sNetworkMonitor = widget.NewCheck("Restarts NetBird when the network changes", nil) + s.sDisableDNS = widget.NewCheck("Keeps system DNS settings unchanged", nil) + s.sDisableClientRoutes = widget.NewCheck("This peer won't route traffic to other peers", nil) + s.sDisableServerRoutes = widget.NewCheck("This peer won't act as router for others", nil) + s.sBlockLANAccess = widget.NewCheck("Blocks local network access when used as exit node", nil) + s.wSettings.SetContent(s.getSettingsForm()) - s.wSettings.Resize(fyne.NewSize(600, 400)) + s.wSettings.Resize(fyne.NewSize(600, 500)) s.wSettings.SetFixedSize(true) s.getSrvConfig() - s.wSettings.Show() } @@ -355,6 +374,11 @@ func (s *serviceClient) getSettingsForm() *widget.Form { {Text: "Pre-shared Key", Widget: s.iPreSharedKey}, {Text: "Config File", Widget: s.iConfigFile}, {Text: "Log File", Widget: s.iLogFile}, + {Text: "Network Monitor", Widget: s.sNetworkMonitor}, + {Text: "Disable DNS", Widget: s.sDisableDNS}, + {Text: "Disable Client Routes", Widget: s.sDisableClientRoutes}, + {Text: "Disable Server Routes", Widget: s.sDisableServerRoutes}, + {Text: "Disable LAN Access", Widget: s.sBlockLANAccess}, }, SubmitText: "Save", OnSubmit: func() { @@ -377,11 +401,15 @@ func (s *serviceClient) getSettingsForm() *widget.Form { defer s.wSettings.Close() - // If the management URL, pre-shared key, admin URL, Rosenpass permissive mode, - // interface name, or interface port have changed, we attempt to re-login with the new settings. + // Check if any settings have changed if s.managementURL != iMngURL || s.preSharedKey != s.iPreSharedKey.Text || s.adminURL != iAdminURL || s.RosenpassPermissive != s.sRosenpassPermissive.Checked || - s.interfaceName != s.iInterfaceName.Text || s.interfacePort != int(port) { + s.interfaceName != s.iInterfaceName.Text || s.interfacePort != int(port) || + s.networkMonitor != s.sNetworkMonitor.Checked || + s.disableDNS != s.sDisableDNS.Checked || + s.disableClientRoutes != s.sDisableClientRoutes.Checked || + s.disableServerRoutes != s.sDisableServerRoutes.Checked || + s.blockLANAccess != s.sBlockLANAccess.Checked { s.managementURL = iMngURL s.preSharedKey = s.iPreSharedKey.Text @@ -394,6 +422,11 @@ func (s *serviceClient) getSettingsForm() *widget.Form { RosenpassPermissive: &s.sRosenpassPermissive.Checked, InterfaceName: &s.iInterfaceName.Text, WireguardPort: &port, + NetworkMonitor: &s.sNetworkMonitor.Checked, + DisableDns: &s.sDisableDNS.Checked, + DisableClientRoutes: &s.sDisableClientRoutes.Checked, + DisableServerRoutes: &s.sDisableServerRoutes.Checked, + BlockLanAccess: &s.sBlockLANAccess.Checked, } if s.iPreSharedKey.Text != censoredPreSharedKey { @@ -696,162 +729,9 @@ func (s *serviceClient) onTrayReady() { }) go s.eventManager.Start(s.ctx) - - go s.listenEvents() + go s.eventHandler.listen(s.ctx) } -func (s *serviceClient) listenEvents() { - for { - select { - case <-s.mUp.ClickedCh: - s.mUp.Disable() - go func() { - defer s.mUp.Enable() - err := s.menuUpClick() - if err != nil { - s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) - return - } - }() - case <-s.mDown.ClickedCh: - s.mDown.Disable() - go func() { - defer s.mDown.Enable() - err := s.menuDownClick() - if err != nil { - s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) - return - } - }() - case <-s.mAllowSSH.ClickedCh: - if s.mAllowSSH.Checked() { - s.mAllowSSH.Uncheck() - } else { - s.mAllowSSH.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mAutoConnect.ClickedCh: - if s.mAutoConnect.Checked() { - s.mAutoConnect.Uncheck() - } else { - s.mAutoConnect.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mEnableRosenpass.ClickedCh: - if s.mEnableRosenpass.Checked() { - s.mEnableRosenpass.Uncheck() - } else { - s.mEnableRosenpass.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mLazyConnEnabled.ClickedCh: - if s.mLazyConnEnabled.Checked() { - s.mLazyConnEnabled.Uncheck() - } else { - s.mLazyConnEnabled.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mBlockInbound.ClickedCh: - if s.mBlockInbound.Checked() { - s.mBlockInbound.Uncheck() - } else { - s.mBlockInbound.Check() - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - case <-s.mAdvancedSettings.ClickedCh: - s.mAdvancedSettings.Disable() - go func() { - defer s.mAdvancedSettings.Enable() - defer s.getSrvConfig() - s.runSelfCommand("settings", "true") - }() - case <-s.mCreateDebugBundle.ClickedCh: - s.mCreateDebugBundle.Disable() - go func() { - defer s.mCreateDebugBundle.Enable() - s.runSelfCommand("debug", "true") - }() - case <-s.mQuit.ClickedCh: - systray.Quit() - return - case <-s.mGitHub.ClickedCh: - err := openURL("https://github.com/netbirdio/netbird") - if err != nil { - log.Errorf("%s", err) - } - case <-s.mUpdate.ClickedCh: - err := openURL(version.DownloadUrl()) - if err != nil { - log.Errorf("%s", err) - } - case <-s.mNetworks.ClickedCh: - s.mNetworks.Disable() - go func() { - defer s.mNetworks.Enable() - s.runSelfCommand("networks", "true") - }() - case <-s.mNotifications.ClickedCh: - if s.mNotifications.Checked() { - s.mNotifications.Uncheck() - } else { - s.mNotifications.Check() - } - if s.eventManager != nil { - s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) - } - if err := s.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) - } - } - } -} - -func (s *serviceClient) runSelfCommand(command, arg string) { - proc, err := os.Executable() - if err != nil { - log.Errorf("Error getting executable path: %v", err) - return - } - - cmd := exec.Command(proc, - fmt.Sprintf("--%s=%s", command, arg), - fmt.Sprintf("--daemon-addr=%s", s.addr), - ) - - if out := s.attachOutput(cmd); out != nil { - defer func() { - if err := out.Close(); err != nil { - log.Errorf("Error closing log file %s: %v", s.logFile, err) - } - }() - } - - log.Printf("Running command: %s --%s=%s --daemon-addr=%s", proc, command, arg, s.addr) - - err = cmd.Run() - - if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - log.Printf("Command '%s %s' failed with exit code %d", command, arg, exitErr.ExitCode()) - } else { - log.Printf("Failed to start/run command '%s %s': %v", command, arg, err) - } - return - } - - log.Printf("Command '%s %s' completed successfully.", command, arg) -} func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File { if s.logFile == "" { @@ -937,6 +817,12 @@ func (s *serviceClient) getSrvConfig() { s.interfaceName = cfg.InterfaceName s.interfacePort = int(cfg.WireguardPort) + s.networkMonitor = cfg.NetworkMonitor + s.disableDNS = cfg.DisableDns + s.disableClientRoutes = cfg.DisableClientRoutes + s.disableServerRoutes = cfg.DisableServerRoutes + s.blockLANAccess = cfg.BlockLanAccess + if s.showAdvancedSettings { s.iMngURL.SetText(s.managementURL) s.iAdminURL.SetText(s.adminURL) @@ -949,6 +835,11 @@ func (s *serviceClient) getSrvConfig() { if !cfg.RosenpassEnabled { s.sRosenpassPermissive.Disable() } + s.sNetworkMonitor.SetChecked(cfg.NetworkMonitor) + s.sDisableDNS.SetChecked(cfg.DisableDns) + s.sDisableClientRoutes.SetChecked(cfg.DisableClientRoutes) + s.sDisableServerRoutes.SetChecked(cfg.DisableServerRoutes) + s.sBlockLANAccess.SetChecked(cfg.BlockLanAccess) } if s.mNotifications == nil { @@ -962,7 +853,6 @@ func (s *serviceClient) getSrvConfig() { if s.eventManager != nil { s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) } - } func (s *serviceClient) onUpdateAvailable() { diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go new file mode 100644 index 000000000..f7072c6b8 --- /dev/null +++ b/client/ui/event_handler.go @@ -0,0 +1,204 @@ +//go:build !(linux && 386) + +package main + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + + "fyne.io/fyne/v2" + "fyne.io/systray" + log "github.com/sirupsen/logrus" +) + +type eventHandler struct { + client *serviceClient +} + +func newEventHandler(client *serviceClient) *eventHandler { + return &eventHandler{ + client: client, + } +} + +func (h *eventHandler) listen(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-h.client.mUp.ClickedCh: + h.handleConnectClick() + case <-h.client.mDown.ClickedCh: + h.handleDisconnectClick() + case <-h.client.mAllowSSH.ClickedCh: + h.handleAllowSSHClick() + case <-h.client.mAutoConnect.ClickedCh: + h.handleAutoConnectClick() + case <-h.client.mEnableRosenpass.ClickedCh: + h.handleRosenpassClick() + case <-h.client.mLazyConnEnabled.ClickedCh: + h.handleLazyConnectionClick() + case <-h.client.mBlockInbound.ClickedCh: + h.handleBlockInboundClick() + case <-h.client.mAdvancedSettings.ClickedCh: + h.handleAdvancedSettingsClick() + case <-h.client.mCreateDebugBundle.ClickedCh: + h.handleCreateDebugBundleClick() + case <-h.client.mQuit.ClickedCh: + h.handleQuitClick() + return + case <-h.client.mGitHub.ClickedCh: + h.handleGitHubClick() + case <-h.client.mUpdate.ClickedCh: + h.handleUpdateClick() + case <-h.client.mNetworks.ClickedCh: + h.handleNetworksClick() + case <-h.client.mNotifications.ClickedCh: + h.handleNotificationsClick() + } + } +} + +func (h *eventHandler) handleConnectClick() { + h.client.mUp.Disable() + go func() { + defer h.client.mUp.Enable() + if err := h.client.menuUpClick(); err != nil { + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) + } + }() +} + +func (h *eventHandler) handleDisconnectClick() { + h.client.mDown.Disable() + go func() { + defer h.client.mDown.Enable() + if err := h.client.menuDownClick(); err != nil { + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird daemon")) + } + }() +} + +func (h *eventHandler) handleAllowSSHClick() { + h.toggleCheckbox(h.client.mAllowSSH) + h.updateConfigWithErr() +} + +func (h *eventHandler) handleAutoConnectClick() { + h.toggleCheckbox(h.client.mAutoConnect) + h.updateConfigWithErr() +} + +func (h *eventHandler) handleRosenpassClick() { + h.toggleCheckbox(h.client.mEnableRosenpass) + h.updateConfigWithErr() +} + +func (h *eventHandler) handleLazyConnectionClick() { + h.toggleCheckbox(h.client.mLazyConnEnabled) + h.updateConfigWithErr() +} + +func (h *eventHandler) handleBlockInboundClick() { + h.toggleCheckbox(h.client.mBlockInbound) + h.updateConfigWithErr() +} + +func (h *eventHandler) handleNotificationsClick() { + h.toggleCheckbox(h.client.mNotifications) + if h.client.eventManager != nil { + h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked()) + } + h.updateConfigWithErr() +} + +func (h *eventHandler) handleAdvancedSettingsClick() { + h.client.mAdvancedSettings.Disable() + go func() { + defer h.client.mAdvancedSettings.Enable() + defer h.client.getSrvConfig() + h.runSelfCommand("settings", "true") + }() +} + +func (h *eventHandler) handleCreateDebugBundleClick() { + h.client.mCreateDebugBundle.Disable() + go func() { + defer h.client.mCreateDebugBundle.Enable() + h.runSelfCommand("debug", "true") + }() +} + +func (h *eventHandler) handleQuitClick() { + systray.Quit() +} + +func (h *eventHandler) handleGitHubClick() { + if err := openURL("https://github.com/netbirdio/netbird"); err != nil { + log.Errorf("failed to open GitHub URL: %v", err) + } +} + +func (h *eventHandler) handleUpdateClick() { + if err := openURL("https://netbird.io/download"); err != nil { + log.Errorf("failed to open download URL: %v", err) + } +} + +func (h *eventHandler) handleNetworksClick() { + h.client.mNetworks.Disable() + go func() { + defer h.client.mNetworks.Enable() + h.runSelfCommand("networks", "true") + }() +} + +func (h *eventHandler) toggleCheckbox(item *systray.MenuItem) { + if item.Checked() { + item.Uncheck() + } else { + item.Check() + } +} + +func (h *eventHandler) updateConfigWithErr() { + if err := h.client.updateConfig(); err != nil { + log.Errorf("failed to update config: %v", err) + } +} + +func (h *eventHandler) runSelfCommand(command, arg string) { + proc, err := os.Executable() + if err != nil { + log.Errorf("error getting executable path: %v", err) + return + } + + cmd := exec.Command(proc, + fmt.Sprintf("--%s=%s", command, arg), + fmt.Sprintf("--daemon-addr=%s", h.client.addr), + ) + + if out := h.client.attachOutput(cmd); out != nil { + defer func() { + if err := out.Close(); err != nil { + log.Errorf("error closing log file %s: %v", h.client.logFile, err) + } + }() + } + + log.Printf("running command: %s --%s=%s --daemon-addr=%s", proc, command, arg, h.client.addr) + + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + log.Printf("command '%s %s' failed with exit code %d", command, arg, exitErr.ExitCode()) + } + return + } + + log.Printf("command '%s %s' completed successfully", command, arg) +} From e71383dcb9ab31fc3dd6f8d0de3130ef6386bac6 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:27:58 +0200 Subject: [PATCH 071/213] [client] Add missing client meta flags (#3898) --- client/internal/connect.go | 3 + client/internal/engine.go | 9 + client/internal/login.go | 6 + client/system/info.go | 26 +- client/ui/client_ui.go | 2 +- management/client/grpc.go | 5 + management/proto/management.pb.go | 874 ++++++++++++++++-------------- management/proto/management.proto | 5 + 8 files changed, 500 insertions(+), 430 deletions(-) diff --git a/client/internal/connect.go b/client/internal/connect.go index 1cfef77f2..f59fbe34a 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -500,6 +500,9 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config.DisableServerRoutes, config.DisableDNS, config.DisableFirewall, + config.BlockLANAccess, + config.BlockInbound, + config.LazyConnectionEnabled, ) loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey, config.DNSLabels) if err != nil { diff --git a/client/internal/engine.go b/client/internal/engine.go index 034057fe0..b3b7d1062 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -786,6 +786,9 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { e.config.DisableServerRoutes, e.config.DisableDNS, e.config.DisableFirewall, + e.config.BlockLANAccess, + e.config.BlockInbound, + e.config.LazyConnectionEnabled, ) if err := e.mgmClient.SyncMeta(info); err != nil { @@ -905,6 +908,9 @@ func (e *Engine) receiveManagementEvents() { e.config.DisableServerRoutes, e.config.DisableDNS, e.config.DisableFirewall, + e.config.BlockLANAccess, + e.config.BlockInbound, + e.config.LazyConnectionEnabled, ) // err = e.mgmClient.Sync(info, e.handleSync) @@ -1493,6 +1499,9 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { e.config.DisableServerRoutes, e.config.DisableDNS, e.config.DisableFirewall, + e.config.BlockLANAccess, + e.config.BlockInbound, + e.config.LazyConnectionEnabled, ) netMap, err := e.mgmClient.GetNetworkMap(info) diff --git a/client/internal/login.go b/client/internal/login.go index 395a17199..bbf844eb3 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -116,6 +116,9 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte config.DisableServerRoutes, config.DisableDNS, config.DisableFirewall, + config.BlockLANAccess, + config.BlockInbound, + config.LazyConnectionEnabled, ) _, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey, config.DNSLabels) return serverKey, err @@ -139,6 +142,9 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm. config.DisableServerRoutes, config.DisableDNS, config.DisableFirewall, + config.BlockLANAccess, + config.BlockInbound, + config.LazyConnectionEnabled, ) loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey, config.DNSLabels) if err != nil { diff --git a/client/system/info.go b/client/system/info.go index 3a0c57156..a0a5fe8b3 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -59,30 +59,40 @@ type Info struct { Environment Environment Files []File // for posture checks - RosenpassEnabled bool - RosenpassPermissive bool - ServerSSHAllowed bool - DisableClientRoutes bool - DisableServerRoutes bool - DisableDNS bool - DisableFirewall bool + RosenpassEnabled bool + RosenpassPermissive bool + ServerSSHAllowed bool + + DisableClientRoutes bool + DisableServerRoutes bool + DisableDNS bool + DisableFirewall bool + BlockLANAccess bool + BlockInbound bool + + LazyConnectionEnabled bool } func (i *Info) SetFlags( rosenpassEnabled, rosenpassPermissive bool, serverSSHAllowed *bool, disableClientRoutes, disableServerRoutes, - disableDNS, disableFirewall bool, + disableDNS, disableFirewall, blockLANAccess, blockInbound, lazyConnectionEnabled bool, ) { i.RosenpassEnabled = rosenpassEnabled i.RosenpassPermissive = rosenpassPermissive if serverSSHAllowed != nil { i.ServerSSHAllowed = *serverSSHAllowed } + i.DisableClientRoutes = disableClientRoutes i.DisableServerRoutes = disableServerRoutes i.DisableDNS = disableDNS i.DisableFirewall = disableFirewall + i.BlockLANAccess = blockLANAccess + i.BlockInbound = blockInbound + + i.LazyConnectionEnabled = lazyConnectionEnabled } // StaticInfo is an object that contains machine information that does not change diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index a501043d2..554cfdc44 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -955,9 +955,9 @@ func (s *serviceClient) updateConfig() error { ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, - DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, BlockInbound: &blockInbound, + DisableNotifications: ¬ificationsDisabled, } if err := s.restartClient(&loginRequest); err != nil { diff --git a/management/client/grpc.go b/management/client/grpc.go index e85e16e76..ef26574bd 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -544,10 +544,15 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { RosenpassEnabled: info.RosenpassEnabled, RosenpassPermissive: info.RosenpassPermissive, ServerSSHAllowed: info.ServerSSHAllowed, + DisableClientRoutes: info.DisableClientRoutes, DisableServerRoutes: info.DisableServerRoutes, DisableDNS: info.DisableDNS, DisableFirewall: info.DisableFirewall, + BlockLANAccess: info.BlockLANAccess, + BlockInbound: info.BlockInbound, + + LazyConnectionEnabled: info.LazyConnectionEnabled, }, } } diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 11e72d6ee..8503f2e94 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -798,13 +798,16 @@ type Flags struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` - ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` - DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"` - DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"` - DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"` - DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"` + RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` + DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"` + DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"` + DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"` + DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"` + BlockLANAccess bool `protobuf:"varint,8,opt,name=blockLANAccess,proto3" json:"blockLANAccess,omitempty"` + BlockInbound bool `protobuf:"varint,9,opt,name=blockInbound,proto3" json:"blockInbound,omitempty"` + LazyConnectionEnabled bool `protobuf:"varint,10,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` } func (x *Flags) Reset() { @@ -888,6 +891,27 @@ func (x *Flags) GetDisableFirewall() bool { return false } +func (x *Flags) GetBlockLANAccess() bool { + if x != nil { + return x.BlockLANAccess + } + return false +} + +func (x *Flags) GetBlockInbound() bool { + if x != nil { + return x.BlockInbound + } + return false +} + +func (x *Flags) GetLazyConnectionEnabled() bool { + if x != nil { + return x.LazyConnectionEnabled + } + return false +} + // PeerSystemMeta is machine meta data like OS and version. type PeerSystemMeta struct { state protoimpl.MessageState @@ -3389,7 +3413,7 @@ var file_management_proto_rawDesc = []byte{ 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05, + 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xc1, 0x03, 0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, @@ -3409,425 +3433,433 @@ var file_management_proto_rawDesc = []byte{ 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xf2, 0x04, - 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, - 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, - 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62, - 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, - 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, - 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, - 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, - 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, - 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, - 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, - 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, - 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, - 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, - 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x12, 0x26, 0x0a, + 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x41, 0x4e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x41, 0x4e, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x61, 0x7a, + 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, + 0xf2, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, + 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, + 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, + 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, + 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, + 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, + 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, + 0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, + 0x6c, 0x61, 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, + 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0xff, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, + 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x66, 0x6c, 0x6f, + 0x77, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, + 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, + 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, + 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, + 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, + 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, + 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, + 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x0a, + 0x46, 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x22, 0x0a, 0x0c, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, + 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, + 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x64, 0x6e, + 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7d, 0x0a, 0x13, 0x50, + 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, + 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x02, 0x0a, 0x0a, 0x50, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, + 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, + 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, + 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x61, 0x7a, 0x79, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, + 0x05, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, + 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, - 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xff, 0x01, - 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, - 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, - 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, - 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x22, - 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, - 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, - 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, - 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, - 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, - 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, - 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, - 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x0a, 0x46, 0x6c, - 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, - 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, - 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x18, 0x0a, - 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x64, 0x6e, 0x73, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, - 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x02, 0x0a, 0x0a, 0x50, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, - 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, - 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, - 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, - 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 0x0a, - 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, - 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, - 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, - 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, - 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, - 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, - 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, - 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, + 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, + 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, + 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, + 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, + 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, + 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, + 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, + 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, + 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, + 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, - 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, - 0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, - 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, - 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, - 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, + 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, + 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, + 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x66, 0x71, 0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, + 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, - 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, - 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, - 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, - 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x22, 0xed, 0x01, - 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, - 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, - 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, - 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, - 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, - 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, - 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, - 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, - 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, - 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, - 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, - 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, - 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, - 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, - 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, - 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, - 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, - 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, - 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, - 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, - 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, - 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, - 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, - 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, - 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, - 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, - 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, - 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, - 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, - 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, - 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, - 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, - 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, - 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, - 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, + 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, + 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, + 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, - 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, + 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, + 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, + 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, + 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, + 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x22, + 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, + 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, + 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, + 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, + 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, + 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, + 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, + 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, + 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, + 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, + 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, + 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, + 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, + 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, + 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, + 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, + 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, + 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, + 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, + 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, + 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, + 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, + 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, + 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, + 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, + 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, + 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, + 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, + 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, + 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, + 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, + 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, - 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, + 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management/proto/management.proto b/management/proto/management.proto index 3f985d5fe..8e137df93 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -134,10 +134,15 @@ message Flags { bool rosenpassEnabled = 1; bool rosenpassPermissive = 2; bool serverSSHAllowed = 3; + bool disableClientRoutes = 4; bool disableServerRoutes = 5; bool disableDNS = 6; bool disableFirewall = 7; + bool blockLANAccess = 8; + bool blockInbound = 9; + + bool lazyConnectionEnabled = 10; } // PeerSystemMeta is machine meta data like OS and version. From b76d9e8e9ebc1088f7731a247b2fa2d7a7a664a5 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 10 Jun 2025 18:02:13 +0300 Subject: [PATCH 072/213] [management] Add support for port ranges in firewall rules (#3823) --- management/server/policy_test.go | 66 ++++++++++++++++++++++++++++-- management/server/types/account.go | 8 +++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 0c1160cda..679ec3b86 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -58,6 +58,11 @@ func TestAccount_getPeersByPolicy(t *testing.T) { IP: net.ParseIP("100.65.29.55"), Status: &nbpeer.PeerStatus{}, }, + "peerI": { + ID: "peerI", + IP: net.ParseIP("100.65.31.2"), + Status: &nbpeer.PeerStatus{}, + }, }, Groups: map[string]*types.Group{ "GroupAll": { @@ -99,6 +104,13 @@ func TestAccount_getPeersByPolicy(t *testing.T) { "peerH", }, }, + "GroupDMZ": { + ID: "GroupDMZ", + Name: "dmz", + Peers: []string{ + "peerI", + }, + }, }, Policies: []*types.Policy{ { @@ -148,6 +160,35 @@ func TestAccount_getPeersByPolicy(t *testing.T) { }, }, }, + { + ID: "RuleDMZ", + Name: "Dmz", + Description: "No description", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "RuleDMZ", + Name: "Dmz", + Description: "No description", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolTCP, + Action: types.PolicyTrafficActionAccept, + PortRanges: []types.RulePortRange{ + { + Start: 8080, + End: 8083, + }, + }, + Sources: []string{ + "GroupWorkstations", + }, + Destinations: []string{ + "GroupDMZ", + }, + }, + }, + }, }, } @@ -166,7 +207,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { t.Run("check first peer map details", func(t *testing.T) { peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", validatedPeers) - assert.Len(t, peers, 7) + assert.Len(t, peers, 8) assert.Contains(t, peers, account.Peers["peerA"]) assert.Contains(t, peers, account.Peers["peerC"]) assert.Contains(t, peers, account.Peers["peerD"]) @@ -174,8 +215,9 @@ func TestAccount_getPeersByPolicy(t *testing.T) { assert.Contains(t, peers, account.Peers["peerF"]) assert.Contains(t, peers, account.Peers["peerG"]) assert.Contains(t, peers, account.Peers["peerH"]) + assert.Contains(t, peers, account.Peers["peerI"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "0.0.0.0", Direction: types.FirewallRuleDirectionIN, @@ -292,12 +334,28 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Port: "", PolicyID: "RuleSwarm", }, + { + PeerIP: "100.65.31.2", + Direction: types.FirewallRuleDirectionIN, + Action: "accept", + Protocol: "tcp", + PortRange: types.RulePortRange{Start: 8080, End: 8083}, + PolicyID: "RuleDMZ", + }, + { + PeerIP: "100.65.31.2", + Direction: types.FirewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + PortRange: types.RulePortRange{Start: 8080, End: 8083}, + PolicyID: "RuleDMZ", + }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) + assert.Len(t, firewallRules, len(expectedFirewallRules)) for _, rule := range firewallRules { contains := false - for _, expectedRule := range epectedFirewallRules { + for _, expectedRule := range expectedFirewallRules { if rule.Equal(expectedRule) { contains = true break diff --git a/management/server/types/account.go b/management/server/types/account.go index 8315f5796..da230f0b2 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -1046,7 +1046,7 @@ func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, } rulesExists[ruleID] = struct{}{} - if len(rule.Ports) == 0 { + if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 { rules = append(rules, &fr) continue } @@ -1056,6 +1056,12 @@ func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, pr.Port = port rules = append(rules, &pr) } + + for _, portRange := range rule.PortRanges { + pr := fr + pr.PortRange = portRange + rules = append(rules, &pr) + } } }, func() ([]*nbpeer.Peer, []*FirewallRule) { return peers, rules From 87376afd13ddd07f577a37f1ab423c9f4eccce91 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 10 Jun 2025 18:02:45 +0300 Subject: [PATCH 073/213] [management] Enable unidirectional rules for all port policy (#3826) --- .../http/handlers/policies/policies_handler.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index 9ff7ea0ea..4d8cce3d4 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -255,23 +255,12 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s } // validate policy object - switch pr.Protocol { - case types.PolicyRuleProtocolALL, types.PolicyRuleProtocolICMP: + if pr.Protocol == types.PolicyRuleProtocolALL || pr.Protocol == types.PolicyRuleProtocolICMP { if len(pr.Ports) != 0 || len(pr.PortRanges) != 0 { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "for ALL or ICMP protocol ports is not allowed"), w) return } - if !pr.Bidirectional { - util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "for ALL or ICMP protocol type flow can be only bi-directional"), w) - return - } - case types.PolicyRuleProtocolTCP, types.PolicyRuleProtocolUDP: - if !pr.Bidirectional && (len(pr.Ports) == 0 || len(pr.PortRanges) != 0) { - util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "for ALL or ICMP protocol type flow can be only bi-directional"), w) - return - } } - policy.Rules = append(policy.Rules, &pr) } From 75feb0da8b06c5fa23595efc4bf867fc6fcc9ae3 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 11 Jun 2025 11:04:44 +0200 Subject: [PATCH 074/213] [client] Refactor context management in ConnMgr for clarity and consistency (#3951) In the conn_mgr we must distinguish two contexts. One is relevant for lazy-manager, and one (engine context) is relevant for peer creation. If we use the incorrect context, then when we disable the lazy connection feature, we cancel the peer connections too, instead of just the lazy manager. --- client/internal/conn_mgr.go | 38 ++++++++++----------- client/internal/engine.go | 2 +- client/internal/lazyconn/manager/manager.go | 14 ++++---- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index 88b47c511..aac312dc3 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -34,9 +34,9 @@ type ConnMgr struct { lazyConnMgr *manager.Manager - wg sync.WaitGroup - ctx context.Context - ctxCancel context.CancelFunc + wg sync.WaitGroup + lazyCtx context.Context + lazyCtxCancel context.CancelFunc } func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface, dispatcher *dispatcher.ConnectionDispatcher) *ConnMgr { @@ -86,7 +86,7 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er log.Infof("lazy connection manager is enabled by management feature flag") e.initLazyManager(ctx) e.statusRecorder.UpdateLazyConnection(true) - return e.addPeersToLazyConnManager(ctx) + return e.addPeersToLazyConnManager() } else { if e.lazyConnMgr == nil { return nil @@ -109,7 +109,7 @@ func (e *ConnMgr) UpdateRouteHAMap(haMap route.HAMap) { } // SetExcludeList sets the list of peer IDs that should always have permanent connections. -func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) { +func (e *ConnMgr) SetExcludeList(ctx context.Context, peerIDs map[string]bool) { if e.lazyConnMgr == nil { return } @@ -133,7 +133,7 @@ func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) { excludedPeers = append(excludedPeers, lazyPeerCfg) } - added := e.lazyConnMgr.ExcludePeer(e.ctx, excludedPeers) + added := e.lazyConnMgr.ExcludePeer(e.lazyCtx, excludedPeers) for _, peerID := range added { var peerConn *peer.Conn var exists bool @@ -143,7 +143,7 @@ func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) { } peerConn.Log.Infof("peer has been added to lazy connection exclude list, opening permanent connection") - if err := peerConn.Open(e.ctx); err != nil { + if err := peerConn.Open(ctx); err != nil { peerConn.Log.Errorf("failed to open connection: %v", err) } } @@ -221,9 +221,9 @@ func (e *ConnMgr) OnSignalMsg(ctx context.Context, peerKey string) (*peer.Conn, return conn, true } - if found := e.lazyConnMgr.ActivatePeer(ctx, peerKey); found { + if found := e.lazyConnMgr.ActivatePeer(e.lazyCtx, peerKey); found { conn.Log.Infof("activated peer from inactive state") - if err := conn.Open(e.ctx); err != nil { + if err := conn.Open(ctx); err != nil { conn.Log.Errorf("failed to open connection: %v", err) } } @@ -235,29 +235,27 @@ func (e *ConnMgr) Close() { return } - e.ctxCancel() + e.lazyCtxCancel() e.wg.Wait() e.lazyConnMgr = nil } -func (e *ConnMgr) initLazyManager(parentCtx context.Context) { +func (e *ConnMgr) initLazyManager(engineCtx context.Context) { cfg := manager.Config{ InactivityThreshold: inactivityThresholdEnv(), } - e.lazyConnMgr = manager.NewManager(cfg, e.peerStore, e.iface, e.dispatcher) + e.lazyConnMgr = manager.NewManager(cfg, engineCtx, e.peerStore, e.iface, e.dispatcher) - ctx, cancel := context.WithCancel(parentCtx) - e.ctx = ctx - e.ctxCancel = cancel + e.lazyCtx, e.lazyCtxCancel = context.WithCancel(engineCtx) e.wg.Add(1) go func() { defer e.wg.Done() - e.lazyConnMgr.Start(ctx) + e.lazyConnMgr.Start(e.lazyCtx) }() } -func (e *ConnMgr) addPeersToLazyConnManager(ctx context.Context) error { +func (e *ConnMgr) addPeersToLazyConnManager() error { peers := e.peerStore.PeersPubKey() lazyPeerCfgs := make([]lazyconn.PeerConfig, 0, len(peers)) for _, peerID := range peers { @@ -277,7 +275,7 @@ func (e *ConnMgr) addPeersToLazyConnManager(ctx context.Context) error { lazyPeerCfgs = append(lazyPeerCfgs, lazyPeerCfg) } - return e.lazyConnMgr.AddActivePeers(ctx, lazyPeerCfgs) + return e.lazyConnMgr.AddActivePeers(e.lazyCtx, lazyPeerCfgs) } func (e *ConnMgr) closeManager(ctx context.Context) { @@ -285,7 +283,7 @@ func (e *ConnMgr) closeManager(ctx context.Context) { return } - e.ctxCancel() + e.lazyCtxCancel() e.wg.Wait() e.lazyConnMgr = nil @@ -295,7 +293,7 @@ func (e *ConnMgr) closeManager(ctx context.Context) { } func (e *ConnMgr) isStartedWithLazyMgr() bool { - return e.lazyConnMgr != nil && e.ctxCancel != nil + return e.lazyConnMgr != nil && e.lazyCtxCancel != nil } func inactivityThresholdEnv() *time.Duration { diff --git a/client/internal/engine.go b/client/internal/engine.go index b3b7d1062..253ecb2a6 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1082,7 +1082,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { // must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store excludedLazyPeers := e.toExcludedLazyPeers(forwardingRules, networkMap.GetRemotePeers()) - e.connMgr.SetExcludeList(excludedLazyPeers) + e.connMgr.SetExcludeList(e.ctx, excludedLazyPeers) e.networkSerial = serial diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go index 15979d553..718bdbddf 100644 --- a/client/internal/lazyconn/manager/manager.go +++ b/client/internal/lazyconn/manager/manager.go @@ -41,6 +41,7 @@ type Config struct { // - Handling connection establishment based on peer signaling // - Managing route HA groups and activating all peers in a group when one peer is activated type Manager struct { + engineCtx context.Context peerStore *peerstore.Store connStateDispatcher *dispatcher.ConnectionDispatcher inactivityThreshold time.Duration @@ -59,13 +60,15 @@ type Manager struct { haGroupToPeers map[route.HAUniqueID][]string // HA group -> peer IDs in the group routesMu sync.RWMutex // protects route mappings - cancel context.CancelFunc onInactive chan peerid.ConnID } -func NewManager(config Config, peerStore *peerstore.Store, wgIface lazyconn.WGIface, connStateDispatcher *dispatcher.ConnectionDispatcher) *Manager { +// NewManager creates a new lazy connection manager +// engineCtx is the context for creating peer Connection +func NewManager(config Config, engineCtx context.Context, peerStore *peerstore.Store, wgIface lazyconn.WGIface, connStateDispatcher *dispatcher.ConnectionDispatcher) *Manager { log.Infof("setup lazy connection service") m := &Manager{ + engineCtx: engineCtx, peerStore: peerStore, connStateDispatcher: connStateDispatcher, inactivityThreshold: inactivity.DefaultInactivityThreshold, @@ -136,7 +139,6 @@ func (m *Manager) UpdateRouteHAMap(haMap route.HAMap) { func (m *Manager) Start(ctx context.Context) { defer m.close() - ctx, m.cancel = context.WithCancel(ctx) for { select { case <-ctx.Done(): @@ -341,7 +343,7 @@ func (m *Manager) activateHAGroupPeers(ctx context.Context, triggerPeerID string if m.activateSinglePeer(ctx, cfg, mp) { activatedCount++ cfg.Log.Infof("activated peer as part of HA group %s (triggered by %s)", haGroup, triggerPeerID) - m.peerStore.PeerConnOpen(ctx, cfg.PublicKey) + m.peerStore.PeerConnOpen(m.engineCtx, cfg.PublicKey) } } } @@ -395,8 +397,6 @@ func (m *Manager) close() { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() - m.cancel() - m.connStateDispatcher.RemoveListener(m.connStateListener) m.activityManager.Close() for _, iw := range m.inactivityMonitors { @@ -438,7 +438,7 @@ func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) m.activateHAGroupPeers(ctx, mp.peerCfg.PublicKey) - m.peerStore.PeerConnOpen(ctx, mp.peerCfg.PublicKey) + m.peerStore.PeerConnOpen(m.engineCtx, mp.peerCfg.PublicKey) } func (m *Manager) onPeerInactivityTimedOut(peerConnID peerid.ConnID) { From 4ee1635baa47a2f85a80f4856dc21b7e630a6ae0 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 11 Jun 2025 14:32:16 +0300 Subject: [PATCH 075/213] [management] Propagate user groups when group propagation setting is re-enabled (#3912) --- management/server/account.go | 249 ++++++++++++------ management/server/account/manager.go | 2 +- management/server/account_test.go | 138 +++++++++- .../handlers/accounts/accounts_handler.go | 4 +- .../accounts/accounts_handler_test.go | 6 +- management/server/mock_server/account_mock.go | 4 +- management/server/store/sql_store.go | 16 ++ management/server/store/store.go | 1 + management/server/user.go | 43 ++- 9 files changed, 352 insertions(+), 111 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 63879802a..82f5ee4a3 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -277,29 +277,11 @@ func (am *DefaultAccountManager) GetIdpManager() idp.Manager { // UpdateAccountSettings updates Account settings. // Only users with role UserRoleAdmin can update the account. // User that performs the update has to belong to the account. -// Returns an updated Account -func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) { - halfYearLimit := 180 * 24 * time.Hour - if newSettings.PeerLoginExpiration > halfYearLimit { - return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be larger than 180 days") - } - - if newSettings.PeerLoginExpiration < time.Hour { - return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour") - } - - if newSettings.DNSDomain != "" && !isDomainValid(newSettings.DNSDomain) { - return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain) - } - +// Returns an updated Settings +func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) { unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return nil, err - } - allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update) if err != nil { return nil, fmt.Errorf("failed to validate user permissions: %w", err) @@ -309,12 +291,118 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco return nil, status.NewPermissionDeniedError() } - err = am.integratedPeerValidator.ValidateExtraSettings(ctx, newSettings.Extra, account.Settings.Extra, account.Peers, userID, accountID) + var oldSettings *types.Settings + var updateAccountPeers bool + var groupChangesAffectPeers bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + var groupsUpdated bool + + oldSettings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthUpdate, accountID) + if err != nil { + return err + } + + if err = am.validateSettingsUpdate(ctx, transaction, newSettings, oldSettings, userID, accountID); err != nil { + return err + } + + if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled || + oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled || + oldSettings.DNSDomain != newSettings.DNSDomain { + updateAccountPeers = true + } + + if oldSettings.GroupsPropagationEnabled != newSettings.GroupsPropagationEnabled && newSettings.GroupsPropagationEnabled { + groupsUpdated, groupChangesAffectPeers, err = propagateUserGroupMemberships(ctx, transaction, accountID) + if err != nil { + return err + } + } + + if updateAccountPeers || groupsUpdated { + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + } + + return transaction.SaveAccountSettings(ctx, store.LockingStrengthUpdate, accountID, newSettings) + }) if err != nil { return nil, err } - oldSettings := account.Settings + extraSettingsChanged, err := am.settingsManager.UpdateExtraSettings(ctx, accountID, userID, newSettings.Extra) + if err != nil { + return nil, err + } + + am.handleRoutingPeerDNSResolutionSettings(ctx, oldSettings, newSettings, userID, accountID) + am.handleLazyConnectionSettings(ctx, oldSettings, newSettings, userID, accountID) + am.handlePeerLoginExpirationSettings(ctx, oldSettings, newSettings, userID, accountID) + am.handleGroupsPropagationSettings(ctx, oldSettings, newSettings, userID, accountID) + if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil { + return nil, err + } + if oldSettings.DNSDomain != newSettings.DNSDomain { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil) + } + + if updateAccountPeers || extraSettingsChanged || groupChangesAffectPeers { + go am.UpdateAccountPeers(ctx, accountID) + } + + return newSettings, nil +} + +func (am *DefaultAccountManager) validateSettingsUpdate(ctx context.Context, transaction store.Store, newSettings, oldSettings *types.Settings, userID, accountID string) error { + halfYearLimit := 180 * 24 * time.Hour + if newSettings.PeerLoginExpiration > halfYearLimit { + return status.Errorf(status.InvalidArgument, "peer login expiration can't be larger than 180 days") + } + + if newSettings.PeerLoginExpiration < time.Hour { + return status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour") + } + + if newSettings.DNSDomain != "" && !isDomainValid(newSettings.DNSDomain) { + return status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain) + } + + peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + if err != nil { + return err + } + + peersMap := make(map[string]*nbpeer.Peer, len(peers)) + for _, peer := range peers { + peersMap[peer.ID] = peer + } + + return am.integratedPeerValidator.ValidateExtraSettings(ctx, newSettings.Extra, oldSettings.Extra, peersMap, userID, accountID) +} + +func (am *DefaultAccountManager) handleRoutingPeerDNSResolutionSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { + if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled { + if newSettings.RoutingPeerDNSResolutionEnabled { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountRoutingPeerDNSResolutionEnabled, nil) + } else { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountRoutingPeerDNSResolutionDisabled, nil) + } + } +} + +func (am *DefaultAccountManager) handleLazyConnectionSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { + if oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled { + if newSettings.LazyConnectionEnabled { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionEnabled, nil) + } else { + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionDisabled, nil) + } + } +} + +func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled { event := activity.AccountPeerLoginExpirationEnabled if !newSettings.PeerLoginExpirationEnabled { @@ -330,82 +418,21 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerLoginExpirationDurationUpdated, nil) am.checkAndSchedulePeerLoginExpiration(ctx, accountID) } - - updateAccountPeers := false - if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled { - if newSettings.RoutingPeerDNSResolutionEnabled { - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountRoutingPeerDNSResolutionEnabled, nil) - } else { - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountRoutingPeerDNSResolutionDisabled, nil) - } - updateAccountPeers = true - } - - if oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled { - if newSettings.LazyConnectionEnabled { - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionEnabled, nil) - } else { - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionDisabled, nil) - } - updateAccountPeers = true - } - - if oldSettings.DNSDomain != newSettings.DNSDomain { - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil) - updateAccountPeers = true - } - - err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID) - if err != nil { - return nil, err - } - - err = am.handleGroupsPropagationSettings(ctx, oldSettings, newSettings, userID, accountID) - if err != nil { - return nil, fmt.Errorf("groups propagation failed: %w", err) - } - - account.UpdateSettings(newSettings) - - if updateAccountPeers { - account.Network.Serial++ - } - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - - extraSettingsChanged, err := am.settingsManager.UpdateExtraSettings(ctx, accountID, userID, newSettings.Extra) - if err != nil { - return nil, err - } - - if updateAccountPeers || extraSettingsChanged { - go am.UpdateAccountPeers(ctx, accountID) - } - - return account, nil } -func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error { +func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { if oldSettings.GroupsPropagationEnabled != newSettings.GroupsPropagationEnabled { if newSettings.GroupsPropagationEnabled { am.StoreEvent(ctx, userID, accountID, accountID, activity.UserGroupPropagationEnabled, nil) - // Todo: retroactively add user groups to all peers } else { am.StoreEvent(ctx, userID, accountID, accountID, activity.UserGroupPropagationDisabled, nil) } } - - return nil } func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error { if newSettings.PeerInactivityExpirationEnabled { if oldSettings.PeerInactivityExpiration != newSettings.PeerInactivityExpiration { - oldSettings.PeerInactivityExpiration = newSettings.PeerInactivityExpiration - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerInactivityExpirationDurationUpdated, nil) am.checkAndSchedulePeerInactivityExpiration(ctx, accountID) } @@ -1853,3 +1880,57 @@ func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, acc return account, nil } + +// propagateUserGroupMemberships propagates all account users' group memberships to their peers. +// Returns true if any groups were modified, true if those updates affect peers and an error. +func propagateUserGroupMemberships(ctx context.Context, transaction store.Store, accountID string) (groupsUpdated bool, peersAffected bool, err error) { + groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return false, false, err + } + + groupsMap := make(map[string]*types.Group, len(groups)) + for _, group := range groups { + groupsMap[group.ID] = group + } + + users, err := transaction.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return false, false, err + } + + groupsToUpdate := make(map[string]*types.Group) + + for _, user := range users { + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, user.Id) + if err != nil { + return false, false, err + } + + updatedGroups, err := updateUserPeersInGroups(groupsMap, userPeers, user.AutoGroups, nil) + if err != nil { + return false, false, err + } + + for _, group := range updatedGroups { + groupsToUpdate[group.ID] = group + groupsMap[group.ID] = group + } + } + + if len(groupsToUpdate) == 0 { + return false, false, nil + } + + peersAffected, err = areGroupChangesAffectPeers(ctx, transaction, accountID, maps.Keys(groupsToUpdate)) + if err != nil { + return false, false, err + } + + err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, maps.Values(groupsToUpdate)) + if err != nil { + return false, false, err + } + + return true, peersAffected, nil +} diff --git a/management/server/account/manager.go b/management/server/account/manager.go index 030bd94ef..de5031c03 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -88,7 +88,7 @@ type Manager interface { GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error) SaveDNSSettings(ctx context.Context, accountID string, userID string, dnsSettingsToSave *types.DNSSettings) error GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) - UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) + UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API GetAllConnectedPeers() (map[string]struct{}, error) diff --git a/management/server/account_test.go b/management/server/account_test.go index 5ada28ca3..ba0191c03 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1805,9 +1805,10 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) { err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, accountID) require.NoError(t, err, "unable to mark peer connected") - account, err := manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ + _, err = manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ PeerLoginExpiration: time.Hour, PeerLoginExpirationEnabled: true, + Extra: &types.ExtraSettings{}, }) require.NoError(t, err, "expecting to update account settings successfully but got error") @@ -1825,11 +1826,11 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) { // disable expiration first update := peer.Copy() update.LoginExpirationEnabled = false - _, err = manager.UpdatePeer(context.Background(), account.Id, userID, update) + _, err = manager.UpdatePeer(context.Background(), accountID, userID, update) require.NoError(t, err, "unable to update peer") // enabling expiration should trigger the routine update.LoginExpirationEnabled = true - _, err = manager.UpdatePeer(context.Background(), account.Id, userID, update) + _, err = manager.UpdatePeer(context.Background(), accountID, userID, update) require.NoError(t, err, "unable to update peer") failed := waitTimeout(wg, time.Second) @@ -1856,6 +1857,7 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing. _, err = manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ PeerLoginExpiration: time.Hour, PeerLoginExpirationEnabled: true, + Extra: &types.ExtraSettings{}, }) require.NoError(t, err, "expecting to update account settings successfully but got error") @@ -1919,9 +1921,10 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test }, } // enabling PeerLoginExpirationEnabled should trigger the expiration job - account, err = manager.UpdateAccountSettings(context.Background(), account.Id, userID, &types.Settings{ + _, err = manager.UpdateAccountSettings(context.Background(), account.Id, userID, &types.Settings{ PeerLoginExpiration: time.Hour, PeerLoginExpirationEnabled: true, + Extra: &types.ExtraSettings{}, }) require.NoError(t, err, "expecting to update account settings successfully but got error") @@ -1935,6 +1938,7 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test _, err = manager.UpdateAccountSettings(context.Background(), account.Id, userID, &types.Settings{ PeerLoginExpiration: time.Hour, PeerLoginExpirationEnabled: false, + Extra: &types.ExtraSettings{}, }) require.NoError(t, err, "expecting to update account settings successfully but got error") failed = waitTimeout(wg, time.Second) @@ -1950,13 +1954,14 @@ func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) { accountID, err := manager.GetAccountIDByUserID(context.Background(), userID, "") require.NoError(t, err, "unable to create an account") - updated, err := manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ + updatedSettings, err := manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ PeerLoginExpiration: time.Hour, PeerLoginExpirationEnabled: false, + Extra: &types.ExtraSettings{}, }) require.NoError(t, err, "expecting to update account settings successfully but got error") - assert.False(t, updated.Settings.PeerLoginExpirationEnabled) - assert.Equal(t, updated.Settings.PeerLoginExpiration, time.Hour) + assert.False(t, updatedSettings.PeerLoginExpirationEnabled) + assert.Equal(t, updatedSettings.PeerLoginExpiration, time.Hour) settings, err := manager.Store.GetAccountSettings(context.Background(), store.LockingStrengthShare, accountID) require.NoError(t, err, "unable to get account settings") @@ -1967,12 +1972,14 @@ func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) { _, err = manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ PeerLoginExpiration: time.Second, PeerLoginExpirationEnabled: false, + Extra: &types.ExtraSettings{}, }) require.Error(t, err, "expecting to fail when providing PeerLoginExpiration less than one hour") _, err = manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ PeerLoginExpiration: time.Hour * 24 * 181, PeerLoginExpirationEnabled: false, + Extra: &types.ExtraSettings{}, }) require.Error(t, err, "expecting to fail when providing PeerLoginExpiration more than 180 days") } @@ -3319,3 +3326,120 @@ func TestDefaultAccountManager_IsCacheCold(t *testing.T) { }) }) } + +func TestPropagateUserGroupMemberships(t *testing.T) { + manager, err := createManager(t) + require.NoError(t, err) + + ctx := context.Background() + initiatorId := "test-user" + domain := "example.com" + + account, err := manager.GetOrCreateAccountByUser(ctx, initiatorId, domain) + require.NoError(t, err) + + peer1 := &nbpeer.Peer{ID: "peer1", AccountID: account.Id, UserID: initiatorId} + err = manager.Store.AddPeerToAccount(ctx, store.LockingStrengthUpdate, peer1) + require.NoError(t, err) + + peer2 := &nbpeer.Peer{ID: "peer2", AccountID: account.Id, UserID: initiatorId} + err = manager.Store.AddPeerToAccount(ctx, store.LockingStrengthUpdate, peer2) + require.NoError(t, err) + + t.Run("should skip propagation when the user has no groups", func(t *testing.T) { + groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) + require.NoError(t, err) + assert.False(t, groupsUpdated) + assert.False(t, groupChangesAffectPeers) + }) + + t.Run("should update membership but no account peers update for unused groups", func(t *testing.T) { + group1 := &types.Group{ID: "group1", Name: "Group 1", AccountID: account.Id} + require.NoError(t, manager.Store.SaveGroup(ctx, store.LockingStrengthUpdate, group1)) + + user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) + require.NoError(t, err) + + user.AutoGroups = append(user.AutoGroups, group1.ID) + require.NoError(t, manager.Store.SaveUser(ctx, store.LockingStrengthUpdate, user)) + + groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) + require.NoError(t, err) + assert.True(t, groupsUpdated) + assert.False(t, groupChangesAffectPeers) + + group, err := manager.Store.GetGroupByID(ctx, store.LockingStrengthShare, account.Id, group1.ID) + require.NoError(t, err) + assert.Len(t, group.Peers, 2) + assert.Contains(t, group.Peers, "peer1") + assert.Contains(t, group.Peers, "peer2") + }) + + t.Run("should update membership and account peers for used groups", func(t *testing.T) { + group2 := &types.Group{ID: "group2", Name: "Group 2", AccountID: account.Id} + require.NoError(t, manager.Store.SaveGroup(ctx, store.LockingStrengthUpdate, group2)) + + user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) + require.NoError(t, err) + + user.AutoGroups = append(user.AutoGroups, group2.ID) + require.NoError(t, manager.Store.SaveUser(ctx, store.LockingStrengthUpdate, user)) + + _, err = manager.SavePolicy(context.Background(), account.Id, initiatorId, &types.Policy{ + Name: "Group1 Policy", + AccountID: account.Id, + Enabled: true, + Rules: []*types.PolicyRule{ + { + Enabled: true, + Sources: []string{"group1"}, + Destinations: []string{"group2"}, + Bidirectional: true, + Action: types.PolicyTrafficActionAccept, + }, + }, + }, true) + require.NoError(t, err) + + groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) + require.NoError(t, err) + assert.True(t, groupsUpdated) + assert.True(t, groupChangesAffectPeers) + + groups, err := manager.Store.GetGroupsByIDs(ctx, store.LockingStrengthShare, account.Id, []string{"group1", "group2"}) + require.NoError(t, err) + for _, group := range groups { + assert.Len(t, group.Peers, 2) + assert.Contains(t, group.Peers, "peer1") + assert.Contains(t, group.Peers, "peer2") + } + }) + + t.Run("should not update membership or account peers when no changes", func(t *testing.T) { + groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) + require.NoError(t, err) + assert.False(t, groupsUpdated) + assert.False(t, groupChangesAffectPeers) + }) + + t.Run("should not remove peers when groups are removed from user", func(t *testing.T) { + user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) + require.NoError(t, err) + + user.AutoGroups = []string{"group1"} + require.NoError(t, manager.Store.SaveUser(ctx, store.LockingStrengthUpdate, user)) + + groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) + require.NoError(t, err) + assert.False(t, groupsUpdated) + assert.False(t, groupChangesAffectPeers) + + groups, err := manager.Store.GetGroupsByIDs(ctx, store.LockingStrengthShare, account.Id, []string{"group1", "group2"}) + require.NoError(t, err) + for _, group := range groups { + assert.Len(t, group.Peers, 2) + assert.Contains(t, group.Peers, "peer1") + assert.Contains(t, group.Peers, "peer2") + } + }) +} diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index 638524e31..dfc782b3f 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -126,7 +126,7 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { settings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled } - updatedAccount, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings) + updatedSettings, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings) if err != nil { util.WriteError(r.Context(), err, w) return @@ -138,7 +138,7 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { return } - resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings, meta) + resp := toAccountResponse(accountID, updatedSettings, meta) util.WriteJSONObject(r.Context(), w, &resp) } diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index fec5140f4..a18798743 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -36,7 +36,7 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler { GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) { return account.Settings, nil }, - UpdateAccountSettingsFunc: func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) { + UpdateAccountSettingsFunc: func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) { halfYearLimit := 180 * 24 * time.Hour if newSettings.PeerLoginExpiration > halfYearLimit { return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be larger than 180 days") @@ -46,9 +46,7 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler { return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour") } - accCopy := account.Copy() - accCopy.UpdateSettings(newSettings) - return accCopy, nil + return newSettings, nil }, GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) { return account.Copy(), nil diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index ed47d3914..3caa6744a 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -90,7 +90,7 @@ type MockAccountManager struct { GetDNSSettingsFunc func(ctx context.Context, accountID, userID string) (*types.DNSSettings, error) SaveDNSSettingsFunc func(ctx context.Context, accountID, userID string, dnsSettingsToSave *types.DNSSettings) error GetPeerFunc func(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) - UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) + UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error @@ -662,7 +662,7 @@ func (am *MockAccountManager) GetPeer(ctx context.Context, accountID, peerID, us } // UpdateAccountSettings mocks UpdateAccountSettings of the AccountManager interface -func (am *MockAccountManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) { +func (am *MockAccountManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) { if am.UpdateAccountSettingsFunc != nil { return am.UpdateAccountSettingsFunc(ctx, accountID, userID, newSettings) } diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 6c3104ef0..d81890775 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -2163,6 +2163,22 @@ func (s *SqlStore) SaveDNSSettings(ctx context.Context, lockStrength LockingStre return nil } +// SaveAccountSettings stores the account settings in DB. +func (s *SqlStore) SaveAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.Settings) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + Select("*").Where(idQueryCondition, accountID).Updates(&types.AccountSettings{Settings: settings}) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save account settings to store: %v", result.Error) + return status.Errorf(status.Internal, "failed to save account settings to store") + } + + if result.RowsAffected == 0 { + return status.NewAccountNotFoundError(accountID) + } + + return nil +} + func (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) { tx := s.db if lockStrength != LockingStrengthNone { diff --git a/management/server/store/store.go b/management/server/store/store.go index fff809247..c7b103454 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -72,6 +72,7 @@ type Store interface { DeleteAccount(ctx context.Context, account *types.Account) error UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error + SaveAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.Settings) error CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) diff --git a/management/server/user.go b/management/server/user.go index 6d780cda3..a1f1c46d5 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -1153,8 +1153,9 @@ func updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbp if !ok { return nil, errors.New("group not found") } - addUserPeersToGroup(userPeerIDMap, group) - groupsToUpdate = append(groupsToUpdate, group) + if changed := addUserPeersToGroup(userPeerIDMap, group); changed { + groupsToUpdate = append(groupsToUpdate, group) + } } for _, gid := range groupsToRemove { @@ -1162,45 +1163,65 @@ func updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbp if !ok { return nil, errors.New("group not found") } - removeUserPeersFromGroup(userPeerIDMap, group) - groupsToUpdate = append(groupsToUpdate, group) + if changed := removeUserPeersFromGroup(userPeerIDMap, group); changed { + groupsToUpdate = append(groupsToUpdate, group) + } } return groupsToUpdate, nil } // addUserPeersToGroup adds the user's peers to the group. -func addUserPeersToGroup(userPeerIDs map[string]struct{}, group *types.Group) { +func addUserPeersToGroup(userPeerIDs map[string]struct{}, group *types.Group) bool { groupPeers := make(map[string]struct{}, len(group.Peers)) for _, pid := range group.Peers { groupPeers[pid] = struct{}{} } + changed := false for pid := range userPeerIDs { - groupPeers[pid] = struct{}{} + if _, exists := groupPeers[pid]; !exists { + groupPeers[pid] = struct{}{} + changed = true + } } group.Peers = make([]string, 0, len(groupPeers)) for pid := range groupPeers { group.Peers = append(group.Peers, pid) } + + if changed { + group.Peers = make([]string, 0, len(groupPeers)) + for pid := range groupPeers { + group.Peers = append(group.Peers, pid) + } + } + return changed } // removeUserPeersFromGroup removes user's peers from the group. -func removeUserPeersFromGroup(userPeerIDs map[string]struct{}, group *types.Group) { +func removeUserPeersFromGroup(userPeerIDs map[string]struct{}, group *types.Group) bool { // skip removing peers from group All if group.Name == "All" { - return + return false } updatedPeers := make([]string, 0, len(group.Peers)) + changed := false + for _, pid := range group.Peers { - if _, found := userPeerIDs[pid]; !found { - updatedPeers = append(updatedPeers, pid) + if _, owned := userPeerIDs[pid]; owned { + changed = true + continue } + updatedPeers = append(updatedPeers, pid) } - group.Peers = updatedPeers + if changed { + group.Peers = updatedPeers + } + return changed } func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserData, bool) { From 9d11257b1afe89b848099867e6e485bc79550952 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 11 Jun 2025 13:33:38 +0200 Subject: [PATCH 076/213] [client] Carry the peer's actual state with the notification. (#3929) - Removed separate thread execution of GetStates during notifications. - Updated notification handler to rely on state data included in the notification payload. --- client/internal/peer/status.go | 49 +++++++++++++------ client/internal/routemanager/client/client.go | 39 ++++++++++----- .../routemanager/client/client_bench_test.go | 5 +- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index abeafd757..629afec9b 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -41,6 +41,13 @@ type EventListener interface { OnEvent(event *proto.SystemEvent) } +// RouterState status for router peers. This contains relevant fields for route manager +type RouterState struct { + Status ConnStatus + Relayed bool + Latency time.Duration +} + // State contains the latest state of a peer type State struct { Mux *sync.RWMutex @@ -155,20 +162,21 @@ type FullStatus struct { type StatusChangeSubscription struct { peerID string id string - eventsChan chan struct{} + eventsChan chan map[string]RouterState ctx context.Context } func newStatusChangeSubscription(ctx context.Context, peerID string) *StatusChangeSubscription { return &StatusChangeSubscription{ - ctx: ctx, - peerID: peerID, - id: uuid.New().String(), - eventsChan: make(chan struct{}, 1), + ctx: ctx, + peerID: peerID, + id: uuid.New().String(), + // it is a buffer for notifications to block less the status recorded + eventsChan: make(chan map[string]RouterState, 8), } } -func (s *StatusChangeSubscription) Events() chan struct{} { +func (s *StatusChangeSubscription) Events() chan map[string]RouterState { return s.eventsChan } @@ -995,15 +1003,28 @@ func (d *Status) notifyPeerStateChangeListeners(peerID string) { if !ok { return } + + // collect the relevant data for router peers + routerPeers := make(map[string]RouterState, len(d.changeNotify)) + for pid := range d.changeNotify { + s, ok := d.peers[pid] + if !ok { + log.Warnf("router peer not found in peers list: %s", pid) + continue + } + + routerPeers[pid] = RouterState{ + Status: s.ConnStatus, + Relayed: s.Relayed, + Latency: s.Latency, + } + } + for _, sub := range subs { - // block the write because we do not want to miss notification - // must have to be sure we will run the GetPeerState() on separated thread - go func() { - select { - case sub.eventsChan <- struct{}{}: - case <-sub.ctx.Done(): - } - }() + select { + case sub.eventsChan <- routerPeers: + case <-sub.ctx.Done(): + } } } diff --git a/client/internal/routemanager/client/client.go b/client/internal/routemanager/client/client.go index 6e3cf61c9..11c0f5708 100644 --- a/client/internal/routemanager/client/client.go +++ b/client/internal/routemanager/client/client.go @@ -76,7 +76,7 @@ type Watcher struct { wgInterface iface.WGIface routes map[route.ID]*route.Route routeUpdate chan RoutesUpdate - peerStateUpdate chan struct{} + peerStateUpdate chan map[string]peer.RouterState routePeersNotifiers map[string]chan struct{} // map of peer key to channel for peer state changes currentChosen *route.Route currentChosenStatus *routerPeerStatus @@ -95,7 +95,7 @@ func NewWatcher(config WatcherConfig) *Watcher { routes: make(map[route.ID]*route.Route), routePeersNotifiers: make(map[string]chan struct{}), routeUpdate: make(chan RoutesUpdate), - peerStateUpdate: make(chan struct{}), + peerStateUpdate: make(chan map[string]peer.RouterState), handler: config.Handler, currentChosenStatus: nil, } @@ -119,6 +119,23 @@ func (w *Watcher) getRouterPeerStatuses() map[route.ID]routerPeerStatus { return routePeerStatuses } +func (w *Watcher) convertRouterPeerStatuses(states map[string]peer.RouterState) map[route.ID]routerPeerStatus { + routePeerStatuses := make(map[route.ID]routerPeerStatus) + for _, r := range w.routes { + peerStatus, ok := states[r.Peer] + if !ok { + log.Warnf("couldn't fetch peer state: %v", r.Peer) + continue + } + routePeerStatuses[r.ID] = routerPeerStatus{ + status: peerStatus.Status, + relayed: peerStatus.Relayed, + latency: peerStatus.Latency, + } + } + return routePeerStatuses +} + // getBestRouteFromStatuses determines the most optimal route from the available routes // within a Watcher, taking into account peer connection status, route metrics, and // preference for non-relayed and direct connections. @@ -237,7 +254,7 @@ func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]router return chosen, chosenStatus } -func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) { +func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan map[string]peer.RouterState, closer chan struct{}) { subscription := w.statusRecorder.SubscribeToPeerStateChanges(ctx, peerKey) defer w.statusRecorder.UnsubscribePeerStateChanges(subscription) @@ -247,8 +264,8 @@ func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, pe return case <-closer: return - case <-subscription.Events(): - peerStateUpdate <- struct{}{} + case routerStates := <-subscription.Events(): + peerStateUpdate <- routerStates log.Debugf("triggered route state update for Peer: %s", peerKey) } } @@ -312,9 +329,7 @@ func (w *Watcher) shouldSkipRecalculation(newChosenID route.ID, newStatus router return true } -func (w *Watcher) recalculateRoutes(rsn reason) error { - routerPeerStatuses := w.getRouterPeerStatuses() - +func (w *Watcher) recalculateRoutes(rsn reason, routerPeerStatuses map[route.ID]routerPeerStatus) error { newChosenID, newStatus := w.getBestRouteFromStatuses(routerPeerStatuses) // If no route is chosen, remove the route from the peer @@ -487,8 +502,9 @@ func (w *Watcher) Start() { select { case <-w.ctx.Done(): return - case <-w.peerStateUpdate: - if err := w.recalculateRoutes(reasonPeerUpdate); err != nil { + case routersStates := <-w.peerStateUpdate: + routerPeerStatuses := w.convertRouterPeerStatuses(routersStates) + if err := w.recalculateRoutes(reasonPeerUpdate, routerPeerStatuses); err != nil { log.Errorf("Failed to recalculate routes for network [%v]: %v", w.handler, err) } case update := <-w.routeUpdate: @@ -512,7 +528,8 @@ func (w *Watcher) handleRouteUpdate(update RoutesUpdate) { if isTrueRouteUpdate { log.Debugf("client network update %v for [%v] contains different routes, recalculating routes", update.UpdateSerial, w.handler) - if err := w.recalculateRoutes(reasonRouteUpdate); err != nil { + routePeerStatuses := w.getRouterPeerStatuses() + if err := w.recalculateRoutes(reasonRouteUpdate, routePeerStatuses); err != nil { log.Errorf("failed to recalculate routes for network [%v]: %v", w.handler, err) } } else { diff --git a/client/internal/routemanager/client/client_bench_test.go b/client/internal/routemanager/client/client_bench_test.go index 1fc41ec33..591042ac5 100644 --- a/client/internal/routemanager/client/client_bench_test.go +++ b/client/internal/routemanager/client/client_bench_test.go @@ -136,7 +136,7 @@ func BenchmarkRecalculateRoutes(b *testing.B) { routes: routes, routePeersNotifiers: make(map[string]chan struct{}), routeUpdate: make(chan RoutesUpdate), - peerStateUpdate: make(chan struct{}), + peerStateUpdate: make(chan map[string]peer.RouterState), handler: &mockRouteHandler{network: "benchmark"}, currentChosenStatus: nil, } @@ -144,8 +144,9 @@ func BenchmarkRecalculateRoutes(b *testing.B) { b.ResetTimer() b.ReportAllocs() + routePeerStatuses := watcher.getRouterPeerStatuses() for i := 0; i < b.N; i++ { - err := watcher.recalculateRoutes(reasonPeerUpdate) + err := watcher.recalculateRoutes(reasonPeerUpdate, routePeerStatuses) if err != nil { b.Fatalf("recalculateRoutes failed: %v", err) } From 0ad25909743d1baf5613c80857e4e4c6b5642e6f Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 11 Jun 2025 16:28:30 +0300 Subject: [PATCH 077/213] [misc] Push all docker images to ghcr in releases (#3954) This PR refactors the release process to push all release images to the GitHub Container Registry. Updated image naming in .goreleaser.yaml to include new registry references. Added a GitHub Actions step in .github/workflows/release.yml to log in to the GitHub Container Registry. --- .github/workflows/release.yml | 7 ++ .goreleaser.yaml | 134 +++++++++++++++++++++++++++++----- 2 files changed, 124 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4806b5676..4fc7f3154 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,6 +65,13 @@ jobs: with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to the GitHub container registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CI_DOCKER_PUSH_GITHUB_TOKEN }} - name: Install OS build dependencies run: sudo apt update && sudo apt install -y -q gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 112659d1c..693fbfe01 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -175,10 +175,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-arm + - ghcr.io/netbirdio/netbird:{{ .Version }}-arm ids: - netbird goarch: arm @@ -191,11 +192,12 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-rootless-amd64 + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64 ids: - netbird goarch: amd64 @@ -207,9 +209,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-rootless-arm64v8 + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8 ids: - netbird goarch: arm64 @@ -221,9 +225,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-rootless-arm + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm ids: - netbird goarch: arm @@ -236,10 +242,12 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/relay:{{ .Version }}-amd64 + - ghcr.io/netbirdio/relay:{{ .Version }}-amd64 ids: - netbird-relay goarch: amd64 @@ -251,10 +259,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/relay:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8 ids: - netbird-relay goarch: arm64 @@ -266,10 +275,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/relay:{{ .Version }}-arm + - ghcr.io/netbirdio/relay:{{ .Version }}-arm ids: - netbird-relay goarch: arm @@ -282,10 +292,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/signal:{{ .Version }}-amd64 + - ghcr.io/netbirdio/signal:{{ .Version }}-amd64 ids: - netbird-signal goarch: amd64 @@ -297,10 +308,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/signal:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8 ids: - netbird-signal goarch: arm64 @@ -312,10 +324,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/signal:{{ .Version }}-arm + - ghcr.io/netbirdio/signal:{{ .Version }}-arm ids: - netbird-signal goarch: arm @@ -328,10 +341,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/management:{{ .Version }}-amd64 + - ghcr.io/netbirdio/management:{{ .Version }}-amd64 ids: - netbird-mgmt goarch: amd64 @@ -343,10 +357,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/management:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/management:{{ .Version }}-arm64v8 ids: - netbird-mgmt goarch: arm64 @@ -358,10 +373,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/management:{{ .Version }}-arm + - ghcr.io/netbirdio/management:{{ .Version }}-arm ids: - netbird-mgmt goarch: arm @@ -374,10 +390,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/management:{{ .Version }}-debug-amd64 + - ghcr.io/netbirdio/management:{{ .Version }}-debug-amd64 ids: - netbird-mgmt goarch: amd64 @@ -389,10 +406,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/management:{{ .Version }}-debug-arm64v8 + - ghcr.io/netbirdio/management:{{ .Version }}-debug-arm64v8 ids: - netbird-mgmt goarch: arm64 @@ -404,11 +422,12 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/management:{{ .Version }}-debug-arm + - ghcr.io/netbirdio/management:{{ .Version }}-debug-arm ids: - netbird-mgmt goarch: arm @@ -421,10 +440,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/upload:{{ .Version }}-amd64 + - ghcr.io/netbirdio/upload:{{ .Version }}-amd64 ids: - netbird-upload goarch: amd64 @@ -436,10 +456,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/upload:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8 ids: - netbird-upload goarch: arm64 @@ -451,10 +472,11 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/upload:{{ .Version }}-arm + - ghcr.io/netbirdio/upload:{{ .Version }}-arm ids: - netbird-upload goarch: arm @@ -467,7 +489,7 @@ dockers: - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" docker_manifests: - name_template: netbirdio/netbird:{{ .Version }} @@ -546,6 +568,84 @@ docker_manifests: - netbirdio/upload:{{ .Version }}-arm64v8 - netbirdio/upload:{{ .Version }}-arm - netbirdio/upload:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/netbird:{{ .Version }} + image_templates: + - ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/netbird:{{ .Version }}-arm + - ghcr.io/netbirdio/netbird:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/netbird:latest + image_templates: + - ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/netbird:{{ .Version }}-arm + - ghcr.io/netbirdio/netbird:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/netbird:{{ .Version }}-rootless + image_templates: + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8 + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64 + + - name_template: ghcr.io/netbirdio/netbird:rootless-latest + image_templates: + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8 + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm + - ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64 + + - name_template: ghcr.io/netbirdio/relay:{{ .Version }} + image_templates: + - ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/relay:{{ .Version }}-arm + - ghcr.io/netbirdio/relay:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/relay:latest + image_templates: + - ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/relay:{{ .Version }}-arm + - ghcr.io/netbirdio/relay:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/signal:{{ .Version }} + image_templates: + - ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/signal:{{ .Version }}-arm + - ghcr.io/netbirdio/signal:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/signal:latest + image_templates: + - ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/signal:{{ .Version }}-arm + - ghcr.io/netbirdio/signal:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/management:{{ .Version }} + image_templates: + - ghcr.io/netbirdio/management:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/management:{{ .Version }}-arm + - ghcr.io/netbirdio/management:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/management:latest + image_templates: + - ghcr.io/netbirdio/management:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/management:{{ .Version }}-arm + - ghcr.io/netbirdio/management:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/management:debug-latest + image_templates: + - ghcr.io/netbirdio/management:{{ .Version }}-debug-arm64v8 + - ghcr.io/netbirdio/management:{{ .Version }}-debug-arm + - ghcr.io/netbirdio/management:{{ .Version }}-debug-amd64 + + - name_template: ghcr.io/netbirdio/upload:{{ .Version }} + image_templates: + - ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/upload:{{ .Version }}-arm + - ghcr.io/netbirdio/upload:{{ .Version }}-amd64 + + - name_template: ghcr.io/netbirdio/upload:latest + image_templates: + - ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/upload:{{ .Version }}-arm + - ghcr.io/netbirdio/upload:{{ .Version }}-amd64 brews: - ids: - default From 3e43298471b3788e38fb11857f9059a58ef1bf31 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:08:45 +0200 Subject: [PATCH 078/213] [client] Fix local resolver returning error for existing domains with other types (#3959) --- client/internal/dns/local/local.go | 22 ++++- client/internal/dns/local/local_test.go | 112 ++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/client/internal/dns/local/local.go b/client/internal/dns/local/local.go index de3d8514b..c19356a7e 100644 --- a/client/internal/dns/local/local.go +++ b/client/internal/dns/local/local.go @@ -12,16 +12,19 @@ import ( "github.com/netbirdio/netbird/client/internal/dns/types" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/domain" ) type Resolver struct { mu sync.RWMutex records map[dns.Question][]dns.RR + domains map[domain.Domain]struct{} } func NewResolver() *Resolver { return &Resolver{ records: make(map[dns.Question][]dns.RR), + domains: make(map[domain.Domain]struct{}), } } @@ -64,8 +67,12 @@ func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { replyMessage.Rcode = dns.RcodeSuccess replyMessage.Answer = append(replyMessage.Answer, records...) } else { - // TODO: return success if we have a different record type for the same name, relevant for search domains - replyMessage.Rcode = dns.RcodeNameError + // Check if we have any records for this domain name with different types + if d.hasRecordsForDomain(domain.Domain(question.Name)) { + replyMessage.Rcode = dns.RcodeSuccess // NOERROR with 0 records + } else { + replyMessage.Rcode = dns.RcodeNameError // NXDOMAIN + } } if err := w.WriteMsg(replyMessage); err != nil { @@ -73,6 +80,15 @@ func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } } +// hasRecordsForDomain checks if any records exist for the given domain name regardless of type +func (d *Resolver) hasRecordsForDomain(domainName domain.Domain) bool { + d.mu.RLock() + defer d.mu.RUnlock() + + _, exists := d.domains[domainName] + return exists +} + // lookupRecords fetches *all* DNS records matching the first question in r. func (d *Resolver) lookupRecords(question dns.Question) []dns.RR { d.mu.RLock() @@ -111,6 +127,7 @@ func (d *Resolver) Update(update []nbdns.SimpleRecord) { defer d.mu.Unlock() maps.Clear(d.records) + maps.Clear(d.domains) for _, rec := range update { if err := d.registerRecord(rec); err != nil { @@ -144,6 +161,7 @@ func (d *Resolver) registerRecord(record nbdns.SimpleRecord) error { } d.records[q] = append(d.records[q], rr) + d.domains[domain.Domain(q.Name)] = struct{}{} return nil } diff --git a/client/internal/dns/local/local_test.go b/client/internal/dns/local/local_test.go index 1d38191e7..8b13b69ff 100644 --- a/client/internal/dns/local/local_test.go +++ b/client/internal/dns/local/local_test.go @@ -470,3 +470,115 @@ func TestLocalResolver_CNAMEFallback(t *testing.T) { }) } } + +// TestLocalResolver_NoErrorWithDifferentRecordType verifies that querying for a record type +// that doesn't exist but where other record types exist for the same domain returns NOERROR +// with 0 records instead of NXDOMAIN +func TestLocalResolver_NoErrorWithDifferentRecordType(t *testing.T) { + resolver := NewResolver() + + recordA := nbdns.SimpleRecord{ + Name: "example.netbird.cloud.", + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: 300, + RData: "192.168.1.100", + } + + recordCNAME := nbdns.SimpleRecord{ + Name: "alias.netbird.cloud.", + Type: int(dns.TypeCNAME), + Class: nbdns.DefaultClass, + TTL: 300, + RData: "target.example.com.", + } + + resolver.Update([]nbdns.SimpleRecord{recordA, recordCNAME}) + + testCases := []struct { + name string + queryName string + queryType uint16 + expectedRcode int + shouldHaveData bool + }{ + { + name: "Query A record that exists", + queryName: "example.netbird.cloud.", + queryType: dns.TypeA, + expectedRcode: dns.RcodeSuccess, + shouldHaveData: true, + }, + { + name: "Query AAAA for domain with only A record", + queryName: "example.netbird.cloud.", + queryType: dns.TypeAAAA, + expectedRcode: dns.RcodeSuccess, + shouldHaveData: false, + }, + { + name: "Query other record with different case and non-fqdn", + queryName: "EXAMPLE.netbird.cloud", + queryType: dns.TypeAAAA, + expectedRcode: dns.RcodeSuccess, + shouldHaveData: false, + }, + { + name: "Query TXT for domain with only A record", + queryName: "example.netbird.cloud.", + queryType: dns.TypeTXT, + expectedRcode: dns.RcodeSuccess, + shouldHaveData: false, + }, + { + name: "Query A for domain with only CNAME record", + queryName: "alias.netbird.cloud.", + queryType: dns.TypeA, + expectedRcode: dns.RcodeSuccess, + shouldHaveData: true, + }, + { + name: "Query AAAA for domain with only CNAME record", + queryName: "alias.netbird.cloud.", + queryType: dns.TypeAAAA, + expectedRcode: dns.RcodeSuccess, + shouldHaveData: true, + }, + { + name: "Query for completely non-existent domain", + queryName: "nonexistent.netbird.cloud.", + queryType: dns.TypeA, + expectedRcode: dns.RcodeNameError, + shouldHaveData: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var responseMSG *dns.Msg + + msg := new(dns.Msg).SetQuestion(tc.queryName, tc.queryType) + + responseWriter := &test.MockResponseWriter{ + WriteMsgFunc: func(m *dns.Msg) error { + responseMSG = m + return nil + }, + } + + resolver.ServeDNS(responseWriter, msg) + + require.NotNil(t, responseMSG, "Should have received a response message") + + assert.Equal(t, tc.expectedRcode, responseMSG.Rcode, + "Response code should be %d (%s)", + tc.expectedRcode, dns.RcodeToString[tc.expectedRcode]) + + if tc.shouldHaveData { + assert.Greater(t, len(responseMSG.Answer), 0, "Response should contain answers") + } else { + assert.Equal(t, 0, len(responseMSG.Answer), "Response should contain no answers") + } + }) + } +} From 6d654acbad4ed3f76285bb540c7bcf54a69501ae Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 11 Jun 2025 23:39:59 +0300 Subject: [PATCH 079/213] [management] Persist peer flags in meta updates (#3958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds persistence for peer feature flags when updating metadata, including equality checks, gRPC extraction, and corresponding unit tests. - Introduce a new `Flags` struct with `isEqual` and incorporate it into `PeerSystemMeta`. - Update `UpdateMetaIfNew` logic to consider flag changes. - Extend gRPC server’s `extractPeerMeta` to populate `Flags` and add tests for `Flags.isEqual`. --- management/server/grpcserver.go | 12 ++++++ management/server/peer/peer.go | 33 +++++++++++++++- management/server/peer/peer_test.go | 58 +++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 5786dc871..2b27f9e0f 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -392,6 +392,18 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee Cloud: meta.GetEnvironment().GetCloud(), Platform: meta.GetEnvironment().GetPlatform(), }, + Flags: nbpeer.Flags{ + RosenpassEnabled: meta.GetFlags().GetRosenpassEnabled(), + RosenpassPermissive: meta.GetFlags().GetRosenpassPermissive(), + ServerSSHAllowed: meta.GetFlags().GetServerSSHAllowed(), + DisableClientRoutes: meta.GetFlags().GetDisableClientRoutes(), + DisableServerRoutes: meta.GetFlags().GetDisableServerRoutes(), + DisableDNS: meta.GetFlags().GetDisableDNS(), + DisableFirewall: meta.GetFlags().GetDisableFirewall(), + BlockLANAccess: meta.GetFlags().GetBlockLANAccess(), + BlockInbound: meta.GetFlags().GetBlockInbound(), + LazyConnectionEnabled: meta.GetFlags().GetLazyConnectionEnabled(), + }, Files: files, } } diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index afda55d17..8ce1dfb4e 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -94,6 +94,22 @@ type File struct { ProcessIsRunning bool } +// Flags defines a set of options to control feature behavior +type Flags struct { + RosenpassEnabled bool + RosenpassPermissive bool + ServerSSHAllowed bool + + DisableClientRoutes bool + DisableServerRoutes bool + DisableDNS bool + DisableFirewall bool + BlockLANAccess bool + BlockInbound bool + + LazyConnectionEnabled bool +} + // PeerSystemMeta is a metadata of a Peer machine system type PeerSystemMeta struct { //nolint:revive Hostname string @@ -111,6 +127,7 @@ type PeerSystemMeta struct { //nolint:revive SystemProductName string SystemManufacturer string Environment Environment `gorm:"serializer:json"` + Flags Flags `gorm:"serializer:json"` Files []File `gorm:"serializer:json"` } @@ -155,7 +172,8 @@ func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { p.SystemProductName == other.SystemProductName && p.SystemManufacturer == other.SystemManufacturer && p.Environment.Cloud == other.Environment.Cloud && - p.Environment.Platform == other.Environment.Platform + p.Environment.Platform == other.Environment.Platform && + p.Flags.isEqual(other.Flags) } func (p PeerSystemMeta) isEmpty() bool { @@ -315,3 +333,16 @@ func (p *Peer) UpdateLastLogin() *Peer { p.Status = newStatus return p } + +func (f Flags) isEqual(other Flags) bool { + return f.RosenpassEnabled == other.RosenpassEnabled && + f.RosenpassPermissive == other.RosenpassPermissive && + f.ServerSSHAllowed == other.ServerSSHAllowed && + f.DisableClientRoutes == other.DisableClientRoutes && + f.DisableServerRoutes == other.DisableServerRoutes && + f.DisableDNS == other.DisableDNS && + f.DisableFirewall == other.DisableFirewall && + f.BlockLANAccess == other.BlockLANAccess && + f.BlockInbound == other.BlockInbound && + f.LazyConnectionEnabled == other.LazyConnectionEnabled +} diff --git a/management/server/peer/peer_test.go b/management/server/peer/peer_test.go index 3d3a2e311..1aa3f6ffc 100644 --- a/management/server/peer/peer_test.go +++ b/management/server/peer/peer_test.go @@ -4,6 +4,8 @@ import ( "fmt" "net/netip" "testing" + + "github.com/stretchr/testify/require" ) // FQDNOld is the original implementation for benchmarking purposes @@ -83,3 +85,59 @@ func TestIsEqual(t *testing.T) { t.Error("meta1 should be equal to meta2") } } + +func TestFlags_IsEqual(t *testing.T) { + tests := []struct { + name string + f1 Flags + f2 Flags + expect bool + }{ + { + name: "should be equal when all fields are identical", + f1: Flags{ + RosenpassEnabled: true, RosenpassPermissive: false, ServerSSHAllowed: true, + DisableClientRoutes: false, DisableServerRoutes: true, DisableDNS: false, + DisableFirewall: true, BlockLANAccess: false, BlockInbound: true, LazyConnectionEnabled: true, + }, + f2: Flags{ + RosenpassEnabled: true, RosenpassPermissive: false, ServerSSHAllowed: true, + DisableClientRoutes: false, DisableServerRoutes: true, DisableDNS: false, + DisableFirewall: true, BlockLANAccess: false, BlockInbound: true, LazyConnectionEnabled: true, + }, + expect: true, + }, + { + name: "shouldn't be equal when fields are different", + f1: Flags{ + RosenpassEnabled: true, RosenpassPermissive: false, ServerSSHAllowed: true, + DisableClientRoutes: false, DisableServerRoutes: true, DisableDNS: false, + DisableFirewall: true, BlockLANAccess: false, BlockInbound: true, LazyConnectionEnabled: true, + }, + f2: Flags{ + RosenpassEnabled: false, RosenpassPermissive: true, ServerSSHAllowed: false, + DisableClientRoutes: true, DisableServerRoutes: false, DisableDNS: true, + DisableFirewall: false, BlockLANAccess: true, BlockInbound: false, LazyConnectionEnabled: false, + }, + expect: false, + }, + { + name: "should be equal when both are empty", + f1: Flags{}, + f2: Flags{}, + expect: true, + }, + { + name: "shouldn't be equal when at least one field differs", + f1: Flags{RosenpassEnabled: true}, + f2: Flags{RosenpassEnabled: false}, + expect: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expect, tt.f1.isEqual(tt.f2)) + }) + } +} From bdf2994e9705bc5491bc04dd22604caae90f1bff Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 12 Jun 2025 09:41:12 +0200 Subject: [PATCH 080/213] [client] Feature/android preferences (#3957) Propagate Rosenpass preferences for Android --- client/android/preferences.go | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/client/android/preferences.go b/client/android/preferences.go index 08485eafc..2a8b197e7 100644 --- a/client/android/preferences.go +++ b/client/android/preferences.go @@ -71,6 +71,42 @@ func (p *Preferences) SetPreSharedKey(key string) { p.configInput.PreSharedKey = &key } +// SetRosenpassEnabled store if rosenpass is enabled +func (p *Preferences) SetRosenpassEnabled(enabled bool) { + p.configInput.RosenpassEnabled = &enabled +} + +// GetRosenpassEnabled read rosenpass enabled from config file +func (p *Preferences) GetRosenpassEnabled() (bool, error) { + if p.configInput.RosenpassEnabled != nil { + return *p.configInput.RosenpassEnabled, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.RosenpassEnabled, err +} + +// SetRosenpassPermissive store the given permissive and wait for commit +func (p *Preferences) SetRosenpassPermissive(permissive bool) { + p.configInput.RosenpassPermissive = &permissive +} + +// GetRosenpassPermissive read rosenpass permissive from config file +func (p *Preferences) GetRosenpassPermissive() (bool, error) { + if p.configInput.RosenpassPermissive != nil { + return *p.configInput.RosenpassPermissive, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.RosenpassPermissive, err +} + // Commit write out the changes into config file func (p *Preferences) Commit() error { _, err := internal.UpdateOrCreateConfig(p.configInput) From 4f63996ae8118d1017fd221d6f792000fcbc4a98 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:48:54 +0100 Subject: [PATCH 081/213] [management] added events streaming metrics (#3814) --- go.mod | 2 +- go.sum | 4 ++-- management/cmd/management.go | 8 +++++++- management/server/telemetry/app_metrics.go | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 11dc88c43..a12058278 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c + github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65 github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index f887cee94..6ce503dd1 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c h1:SdZxYjR9XXHLyRsTbS1EHBr6+RI15oie1K9Q8yvi3FY= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65 h1:5OfYiLjpr4dbQYJI5ouZaylkVdi2KlErLFOwBeBo5Hw= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/cmd/management.go b/management/cmd/management.go index 5fb07890f..bce09efdd 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -159,6 +159,12 @@ var ( if err != nil { return err } + + integrationMetrics, err := integrations.InitIntegrationMetrics(ctx, appMetrics) + if err != nil { + return err + } + store, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, appMetrics, false) if err != nil { return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err) @@ -176,7 +182,7 @@ var ( if disableSingleAccMode { mgmtSingleAccModeDomain = "" } - eventStore, key, err := integrations.InitEventStore(ctx, config.Datadir, config.DataStoreEncryptionKey) + eventStore, key, err := integrations.InitEventStore(ctx, config.Datadir, config.DataStoreEncryptionKey, integrationMetrics) if err != nil { return fmt.Errorf("failed to initialize database: %s", err) } diff --git a/management/server/telemetry/app_metrics.go b/management/server/telemetry/app_metrics.go index 09deb8127..988f91779 100644 --- a/management/server/telemetry/app_metrics.go +++ b/management/server/telemetry/app_metrics.go @@ -184,10 +184,10 @@ func (appMetrics *defaultAppMetrics) Expose(ctx context.Context, port int, endpo } appMetrics.listener = listener go func() { - err := http.Serve(listener, rootRouter) - if err != nil { - return + if err := http.Serve(listener, rootRouter); err != nil && err != http.ErrServerClosed { + log.WithContext(ctx).Errorf("metrics server error: %v", err) } + log.WithContext(ctx).Info("metrics server stopped") }() log.WithContext(ctx).Infof("enabled application metrics and exposing on http://%s", listener.Addr().String()) @@ -204,7 +204,7 @@ func (appMetrics *defaultAppMetrics) GetMeter() metric2.Meter { func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) { exporter, err := prometheus.New() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create prometheus exporter: %w", err) } provider := metric.NewMeterProvider(metric.WithReader(exporter)) @@ -213,32 +213,32 @@ func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) { idpMetrics, err := NewIDPMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize IDP metrics: %w", err) } middleware, err := NewMetricsMiddleware(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize HTTP middleware metrics: %w", err) } grpcMetrics, err := NewGRPCMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize gRPC metrics: %w", err) } storeMetrics, err := NewStoreMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize store metrics: %w", err) } updateChannelMetrics, err := NewUpdateChannelMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize update channel metrics: %w", err) } accountManagerMetrics, err := NewAccountManagerMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize account manager metrics: %w", err) } return &defaultAppMetrics{ From d24d8328f95ef3f687302e47992890a6d91729a3 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 13 Jun 2025 11:04:17 +0200 Subject: [PATCH 082/213] [client] Propagation networks for Android client (#3966) Add networks propagation --- client/android/client.go | 57 ++++++++++++++++++++++++++++++--- client/android/networks.go | 27 ++++++++++++++++ client/android/peer_notifier.go | 15 +++------ 3 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 client/android/networks.go diff --git a/client/android/client.go b/client/android/client.go index 229bcd974..3b8a5bd0f 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -59,6 +59,8 @@ type Client struct { deviceName string uiVersion string networkChangeListener listener.NetworkChangeListener + + connectClient *internal.ConnectClient } // NewClient instantiate a new Client @@ -106,8 +108,8 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - connectClient := internal.NewConnectClient(ctx, cfg, c.recorder) - return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) + c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder) + return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot). @@ -132,8 +134,8 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - connectClient := internal.NewConnectClient(ctx, cfg, c.recorder) - return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) + c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder) + return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // Stop the internal client and free the resources @@ -174,6 +176,53 @@ func (c *Client) PeersList() *PeerInfoArray { return &PeerInfoArray{items: peerInfos} } +func (c *Client) Networks() *NetworkArray { + if c.connectClient == nil { + log.Error("not connected") + return nil + } + + engine := c.connectClient.Engine() + if engine == nil { + log.Error("could not get engine") + return nil + } + + routeManager := engine.GetRouteManager() + if routeManager == nil { + log.Error("could not get route manager") + return nil + } + + networkArray := &NetworkArray{ + items: make([]Network, 0), + } + + for id, routes := range routeManager.GetClientRoutesWithNetID() { + if len(routes) == 0 { + continue + } + + if routes[0].IsDynamic() { + continue + } + + peer, err := c.recorder.GetPeer(routes[0].Peer) + if err != nil { + log.Errorf("could not get peer info for %s: %v", routes[0].Peer, err) + continue + } + network := Network{ + Name: string(id), + Network: routes[0].Network.String(), + Peer: peer.FQDN, + Status: peer.ConnStatus.String(), + } + networkArray.Add(network) + } + return networkArray +} + // OnUpdatedHostDNS update the DNS servers addresses for root zones func (c *Client) OnUpdatedHostDNS(list *DNSList) error { dnsServer, err := dns.GetServerDns() diff --git a/client/android/networks.go b/client/android/networks.go new file mode 100644 index 000000000..aa130420b --- /dev/null +++ b/client/android/networks.go @@ -0,0 +1,27 @@ +//go:build android + +package android + +type Network struct { + Name string + Network string + Peer string + Status string +} + +type NetworkArray struct { + items []Network +} + +func (array *NetworkArray) Add(s Network) *NetworkArray { + array.items = append(array.items, s) + return array +} + +func (array *NetworkArray) Get(i int) *Network { + return &array.items[i] +} + +func (array *NetworkArray) Size() int { + return len(array.items) +} diff --git a/client/android/peer_notifier.go b/client/android/peer_notifier.go index 9f6fcddd6..1f5564c72 100644 --- a/client/android/peer_notifier.go +++ b/client/android/peer_notifier.go @@ -7,30 +7,23 @@ type PeerInfo struct { ConnStatus string // Todo replace to enum } -// PeerInfoCollection made for Java layer to get non default types as collection -type PeerInfoCollection interface { - Add(s string) PeerInfoCollection - Get(i int) string - Size() int -} - -// PeerInfoArray is the implementation of the PeerInfoCollection +// PeerInfoArray is a wrapper of []PeerInfo type PeerInfoArray struct { items []PeerInfo } // Add new PeerInfo to the collection -func (array PeerInfoArray) Add(s PeerInfo) PeerInfoArray { +func (array *PeerInfoArray) Add(s PeerInfo) *PeerInfoArray { array.items = append(array.items, s) return array } // Get return an element of the collection -func (array PeerInfoArray) Get(i int) *PeerInfo { +func (array *PeerInfoArray) Get(i int) *PeerInfo { return &array.items[i] } // Size return with the size of the collection -func (array PeerInfoArray) Size() int { +func (array *PeerInfoArray) Size() int { return len(array.items) } From 04a376539198ad511e17d109d798380b6736897f Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:38:50 +0200 Subject: [PATCH 083/213] [client] Fix unncessary UI updates (#3785) --- client/ui/client_ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 554cfdc44..ea3349cd1 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -565,7 +565,7 @@ func (s *serviceClient) updateStatus() error { var systrayIconState bool switch { - case status.Status == string(internal.StatusConnected): + case status.Status == string(internal.StatusConnected) && !s.mUp.Disabled(): s.connected = true s.sendNotification = true if s.isUpdateIconActive { From 089d442fb2607d513311492985c0d23df8855442 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Sat, 14 Jun 2025 00:51:57 +0300 Subject: [PATCH 084/213] [client] Display login popup on session expiration (#3955) This PR implements a feature enhancement to display a login popup when the session expires. Key changes include updating flag handling and client construction to support a new login URL popup, revising login and notification handling logic to use the new popup, and updating status and server-side session state management accordingly. --- client/cmd/status.go | 5 +- client/internal/state.go | 9 +-- client/server/server.go | 13 +++- client/ui/client_ui.go | 131 +++++++++++++++++++++++++++++++-------- client/ui/network.go | 2 - 5 files changed, 124 insertions(+), 36 deletions(-) diff --git a/client/cmd/status.go b/client/cmd/status.go index e466f73ab..a85ee925e 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -69,7 +69,10 @@ func statusFunc(cmd *cobra.Command, args []string) error { return err } - if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) { + status := resp.GetStatus() + + if status == string(internal.StatusNeedsLogin) || status == string(internal.StatusLoginFailed) || + status == string(internal.StatusSessionExpired) { cmd.Printf("Daemon status: %s\n\n"+ "Run UP command to log in with SSO (interactive login):\n\n"+ " netbird up \n\n"+ diff --git a/client/internal/state.go b/client/internal/state.go index 4ae99d944..041cb73f8 100644 --- a/client/internal/state.go +++ b/client/internal/state.go @@ -10,10 +10,11 @@ type StatusType string const ( StatusIdle StatusType = "Idle" - StatusConnecting StatusType = "Connecting" - StatusConnected StatusType = "Connected" - StatusNeedsLogin StatusType = "NeedsLogin" - StatusLoginFailed StatusType = "LoginFailed" + StatusConnecting StatusType = "Connecting" + StatusConnected StatusType = "Connected" + StatusNeedsLogin StatusType = "NeedsLogin" + StatusLoginFailed StatusType = "LoginFailed" + StatusSessionExpired StatusType = "SessionExpired" ) // CtxInitState setup context state into the context tree. diff --git a/client/server/server.go b/client/server/server.go index 72837b59d..31a437c99 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -8,6 +8,7 @@ import ( "runtime" "strconv" "sync" + "sync/atomic" "time" "github.com/cenkalti/backoff/v4" @@ -66,6 +67,7 @@ type Server struct { lastProbe time.Time persistNetworkMap bool + isSessionActive atomic.Bool } type oauthAuthFlow struct { @@ -567,9 +569,6 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin tokenInfo, err := s.oauthAuthFlow.flow.WaitToken(waitCTX, flowInfo) if err != nil { - if err == context.Canceled { - return nil, nil //nolint:nilnil - } s.mutex.Lock() s.oauthAuthFlow.expiresAt = time.Now() s.mutex.Unlock() @@ -640,6 +639,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes for { select { case <-runningChan: + s.isSessionActive.Store(true) return &proto.UpResponse{}, nil case <-callerCtx.Done(): log.Debug("context done, stopping the wait for engine to become ready") @@ -668,6 +668,7 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes log.Errorf("failed to shut down properly: %v", err) return nil, err } + s.isSessionActive.Store(false) state := internal.CtxGetState(s.rootCtx) state.Set(internal.StatusIdle) @@ -694,6 +695,12 @@ func (s *Server) Status( return nil, err } + if status == internal.StatusNeedsLogin && s.isSessionActive.Load() { + log.Debug("status requested while session is active, returning SessionExpired") + status = internal.StatusSessionExpired + s.isSessionActive.Store(false) + } + statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: version.NetbirdVersion()} s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index ea3349cd1..a1ca4120f 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -20,7 +20,10 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyne.io/systray" @@ -51,7 +54,7 @@ const ( ) func main() { - daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags() + daemonAddr, showSettings, showNetworks, showLoginURL, showDebug, errorMsg, saveLogsInFile := parseFlags() // Initialize file logging if needed. var logFile string @@ -77,13 +80,13 @@ func main() { } // Create the service client (this also builds the settings or networks UI if requested). - client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showDebug) + client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showLoginURL, showDebug) // Watch for theme/settings changes to update the icon. go watchSettingsChanges(a, client) // Run in window mode if any UI flag was set. - if showSettings || showNetworks || showDebug { + if showSettings || showNetworks || showDebug || showLoginURL { a.Run() return } @@ -104,7 +107,7 @@ func main() { } // parseFlags reads and returns all needed command-line flags. -func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool, errorMsg string, saveLogsInFile bool) { +func parseFlags() (daemonAddr string, showSettings, showNetworks, showLoginURL, showDebug bool, errorMsg string, saveLogsInFile bool) { defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" @@ -112,6 +115,7 @@ func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool flag.StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") flag.BoolVar(&showSettings, "settings", false, "run settings window") flag.BoolVar(&showNetworks, "networks", false, "run networks window") + flag.BoolVar(&showLoginURL, "login-url", false, "show login URL in a popup window") flag.BoolVar(&showDebug, "debug", false, "run debug window") flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window") flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) @@ -253,6 +257,7 @@ type serviceClient struct { exitNodeStates []exitNodeState mExitNodeDeselectAll *systray.MenuItem logFile string + wLoginURL fyne.Window } type menuHandler struct { @@ -263,7 +268,7 @@ type menuHandler struct { // newServiceClient instance constructor // // This constructor also builds the UI elements for the settings window. -func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showDebug bool) *serviceClient { +func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showLoginURL bool, showDebug bool) *serviceClient { ctx, cancel := context.WithCancel(context.Background()) s := &serviceClient{ ctx: ctx, @@ -286,6 +291,8 @@ func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool s.showSettingsUI() case showNetworks: s.showNetworksUI() + case showLoginURL: + s.showLoginURL() case showDebug: s.showDebugUI() } @@ -445,11 +452,11 @@ func (s *serviceClient) getSettingsForm() *widget.Form { } } -func (s *serviceClient) login() error { +func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) { conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) - return err + return nil, err } loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{ @@ -457,24 +464,24 @@ func (s *serviceClient) login() error { }) if err != nil { log.Errorf("login to management URL with: %v", err) - return err + return nil, err } - if loginResp.NeedsSSOLogin { + if loginResp.NeedsSSOLogin && openURL { err = open.Run(loginResp.VerificationURIComplete) if err != nil { log.Errorf("opening the verification uri in the browser failed: %v", err) - return err + return nil, err } _, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) if err != nil { log.Errorf("waiting sso login failed with: %v", err) - return err + return nil, err } } - return nil + return loginResp, nil } func (s *serviceClient) menuUpClick() error { @@ -486,7 +493,7 @@ func (s *serviceClient) menuUpClick() error { return err } - err = s.login() + _, err = s.login(true) if err != nil { log.Errorf("login failed with: %v", err) return err @@ -558,7 +565,7 @@ func (s *serviceClient) updateStatus() error { defer s.updateIndicationLock.Unlock() // notify the user when the session has expired - if status.Status == string(internal.StatusNeedsLogin) { + if status.Status == string(internal.StatusSessionExpired) { s.onSessionExpire() } @@ -732,7 +739,6 @@ func (s *serviceClient) onTrayReady() { go s.eventHandler.listen(s.ctx) } - func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File { if s.logFile == "" { // attach child's streams to parent's streams @@ -871,17 +877,9 @@ func (s *serviceClient) onUpdateAvailable() { // onSessionExpire sends a notification to the user when the session expires. func (s *serviceClient) onSessionExpire() { + s.sendNotification = true if s.sendNotification { - title := "Connection session expired" - if runtime.GOOS == "darwin" { - title = "NetBird connection session expired" - } - s.app.SendNotification( - fyne.NewNotification( - title, - "Please re-authenticate to connect to the network", - ), - ) + s.eventHandler.runSelfCommand("login-url", "true") s.sendNotification = false } } @@ -955,9 +953,9 @@ func (s *serviceClient) updateConfig() error { ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, + DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, BlockInbound: &blockInbound, - DisableNotifications: ¬ificationsDisabled, } if err := s.restartClient(&loginRequest); err != nil { @@ -991,6 +989,87 @@ func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { return nil } +// showLoginURL creates a borderless window styled like a pop-up in the top-right corner using s.wLoginURL. +func (s *serviceClient) showLoginURL() { + + resp, err := s.login(false) + if err != nil { + log.Errorf("failed to fetch login URL: %v", err) + return + } + verificationURL := resp.VerificationURIComplete + if verificationURL == "" { + verificationURL = resp.VerificationURI + } + + if verificationURL == "" { + log.Error("no verification URL provided in the login response") + return + } + + resIcon := fyne.NewStaticResource("netbird.png", iconAbout) + + if s.wLoginURL == nil { + s.wLoginURL = s.app.NewWindow("NetBird Session Expired") + s.wLoginURL.Resize(fyne.NewSize(400, 200)) + s.wLoginURL.SetIcon(resIcon) + } + // add a description label + label := widget.NewLabel("Your NetBird session has expired.\nPlease re-authenticate to continue using NetBird.") + + btn := widget.NewButtonWithIcon("Re-authenticate", theme.ViewRefreshIcon(), func() { + + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + if err := openURL(verificationURL); err != nil { + log.Errorf("failed to open login URL: %v", err) + return + } + + _, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: resp.UserCode}) + if err != nil { + log.Errorf("Waiting sso login failed with: %v", err) + label.SetText("Waiting login failed, please create \na debug bundle in the settings and contact support.") + return + } + + label.SetText("Re-authentication successful.\nReconnecting") + time.Sleep(300 * time.Millisecond) + _, err = conn.Up(s.ctx, &proto.UpRequest{}) + if err != nil { + label.SetText("Reconnecting failed, please create \na debug bundle in the settings and contact support.") + log.Errorf("Reconnecting failed with: %v", err) + return + } + + label.SetText("Connection successful.\nClosing this window.") + time.Sleep(time.Second) + + s.wLoginURL.Close() + }) + + img := canvas.NewImageFromResource(resIcon) + img.FillMode = canvas.ImageFillContain + img.SetMinSize(fyne.NewSize(64, 64)) + img.Resize(fyne.NewSize(64, 64)) + + // center the content vertically + content := container.NewVBox( + layout.NewSpacer(), + img, + label, + btn, + layout.NewSpacer(), + ) + s.wLoginURL.SetContent(container.NewCenter(content)) + + s.wLoginURL.Show() +} + func openURL(url string) error { var err error switch runtime.GOOS { diff --git a/client/ui/network.go b/client/ui/network.go index b3748a89d..fb73efd7b 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -358,8 +358,6 @@ func (s *serviceClient) updateExitNodes() { } else { s.mExitNode.Disable() } - - log.Debugf("Exit nodes updated: %d", len(s.mExitNodeItems)) } func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { From f595057a0be7473ab058180b3f11abed6c71b920 Mon Sep 17 00:00:00 2001 From: Philippe Vaucher Date: Sat, 14 Jun 2025 00:08:34 +0200 Subject: [PATCH 085/213] [signal] Set flags from environment variables (#3972) --- signal/cmd/env.go | 35 +++++++++++++++++++++++++++++++++++ signal/cmd/run.go | 1 + 2 files changed, 36 insertions(+) create mode 100644 signal/cmd/env.go diff --git a/signal/cmd/env.go b/signal/cmd/env.go new file mode 100644 index 000000000..3c15ebe1f --- /dev/null +++ b/signal/cmd/env.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "os" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// setFlagsFromEnvVars reads and updates flag values from environment variables with prefix NB_ +func setFlagsFromEnvVars(cmd *cobra.Command) { + flags := cmd.PersistentFlags() + flags.VisitAll(func(f *pflag.Flag) { + newEnvVar := flagNameToEnvVar(f.Name, "NB_") + value, present := os.LookupEnv(newEnvVar) + if !present { + return + } + + err := flags.Set(f.Name, value) + if err != nil { + log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err) + } + }) +} + +// flagNameToEnvVar converts flag name to environment var name adding a prefix, +// replacing dashes and making all uppercase (e.g. setup-keys is converted to NB_SETUP_KEYS according to the input prefix) +func flagNameToEnvVar(cmdFlag string, prefix string) string { + parsed := strings.ReplaceAll(cmdFlag, "-", "_") + upper := strings.ToUpper(parsed) + return prefix + upper +} diff --git a/signal/cmd/run.go b/signal/cmd/run.go index 3a671a848..39bc8331f 100644 --- a/signal/cmd/run.go +++ b/signal/cmd/run.go @@ -303,4 +303,5 @@ func init() { runCmd.Flags().StringVar(&signalLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS") runCmd.Flags().StringVar(&signalCertFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect") runCmd.Flags().StringVar(&signalCertKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect") + setFlagsFromEnvVars(runCmd) } From b1247a14ba93294ee4484cc01476234bfd686894 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 14 Jun 2025 12:24:16 +0100 Subject: [PATCH 086/213] [management] Use xID for setup key IDs to avoid id collisions (#3977) This PR addresses potential ID collisions by switching the setup key ID generation from a hash-based approach to using xid-generated IDs. Replace the hash function with xid.New().String() Remove obsolete imports and the Hash() function --- management/server/setupkey_test.go | 11 +++++------ management/server/types/setupkey.go | 14 ++------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index a561de40d..cecf55200 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "fmt" - "strconv" "strings" "testing" "time" @@ -182,7 +181,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { } assertKey(t, key, tCase.expectedKeyName, false, tCase.expectedType, tCase.expectedUsedTimes, - tCase.expectedCreatedAt, tCase.expectedExpiresAt, strconv.Itoa(int(types.Hash(key.Key))), + tCase.expectedCreatedAt, tCase.expectedExpiresAt, key.Id, tCase.expectedUpdatedAt, tCase.expectedGroups, false) // check the corresponding events that should have been generated @@ -258,10 +257,10 @@ func TestGenerateDefaultSetupKey(t *testing.T) { expectedExpiresAt := time.Now().UTC().Add(24 * 30 * time.Hour) var expectedAutoGroups []string - key, plainKey := types.GenerateDefaultSetupKey() + key, _ := types.GenerateDefaultSetupKey() assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, - expectedExpiresAt, strconv.Itoa(int(types.Hash(plainKey))), expectedUpdatedAt, expectedAutoGroups, true) + expectedExpiresAt, key.Id, expectedUpdatedAt, expectedAutoGroups, true) } @@ -275,10 +274,10 @@ func TestGenerateSetupKey(t *testing.T) { expectedUpdatedAt := time.Now().UTC() var expectedAutoGroups []string - key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) + key, _ := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, - expectedExpiresAt, strconv.Itoa(int(types.Hash(plain))), expectedUpdatedAt, expectedAutoGroups, true) + expectedExpiresAt, key.Id, expectedUpdatedAt, expectedAutoGroups, true) } diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go index ab8e46bea..69b381ae5 100644 --- a/management/server/types/setupkey.go +++ b/management/server/types/setupkey.go @@ -3,13 +3,12 @@ package types import ( "crypto/sha256" b64 "encoding/base64" - "hash/fnv" - "strconv" "strings" "time" "unicode/utf8" "github.com/google/uuid" + "github.com/rs/xid" "github.com/netbirdio/netbird/management/server/util" ) @@ -170,7 +169,7 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:]) return &SetupKey{ - Id: strconv.Itoa(int(Hash(key))), + Id: xid.New().String(), Key: encodedHashedKey, KeySecret: HiddenKey(key, 4), Name: name, @@ -192,12 +191,3 @@ func GenerateDefaultSetupKey() (*SetupKey, string) { return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{}, SetupKeyUnlimitedUsage, false, false) } - -func Hash(s string) uint32 { - h := fnv.New32a() - _, err := h.Write([]byte(s)) - if err != nil { - panic(err) - } - return h.Sum32() -} From bfa5c21d2d608ff65887e4068604175f32ef6fe5 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:12:59 +0200 Subject: [PATCH 087/213] [client] Improve icmp conntrack log (#3963) --- client/cmd/trace.go | 2 +- client/firewall/uspfilter/conntrack/common.go | 2 +- client/firewall/uspfilter/conntrack/icmp.go | 121 ++++++++++++++++-- .../firewall/uspfilter/conntrack/icmp_test.go | 4 +- .../firewall/uspfilter/forwarder/endpoint.go | 2 +- client/firewall/uspfilter/forwarder/tcp.go | 4 +- client/firewall/uspfilter/forwarder/udp.go | 4 +- client/firewall/uspfilter/uspfilter.go | 4 +- .../internal/netflow/conntrack/conntrack.go | 2 +- 9 files changed, 125 insertions(+), 20 deletions(-) diff --git a/client/cmd/trace.go b/client/cmd/trace.go index abb73b646..655838260 100644 --- a/client/cmd/trace.go +++ b/client/cmd/trace.go @@ -118,7 +118,7 @@ func tracePacket(cmd *cobra.Command, args []string) error { } func printTrace(cmd *cobra.Command, src, dst, proto string, sport, dport uint16, resp *proto.TracePacketResponse) { - cmd.Printf("Packet trace %s:%d -> %s:%d (%s)\n\n", src, sport, dst, dport, strings.ToUpper(proto)) + cmd.Printf("Packet trace %s:%d → %s:%d (%s)\n\n", src, sport, dst, dport, strings.ToUpper(proto)) for _, stage := range resp.Stages { if stage.ForwardingDetails != nil { diff --git a/client/firewall/uspfilter/conntrack/common.go b/client/firewall/uspfilter/conntrack/common.go index 3de0bb3f4..bcf6d894b 100644 --- a/client/firewall/uspfilter/conntrack/common.go +++ b/client/firewall/uspfilter/conntrack/common.go @@ -62,5 +62,5 @@ type ConnKey struct { } func (c ConnKey) String() string { - return fmt.Sprintf("%s:%d -> %s:%d", c.SrcIP.Unmap(), c.SrcPort, c.DstIP.Unmap(), c.DstPort) + return fmt.Sprintf("%s:%d → %s:%d", c.SrcIP.Unmap(), c.SrcPort, c.DstIP.Unmap(), c.DstPort) } diff --git a/client/firewall/uspfilter/conntrack/icmp.go b/client/firewall/uspfilter/conntrack/icmp.go index c8ea159da..509c1549b 100644 --- a/client/firewall/uspfilter/conntrack/icmp.go +++ b/client/firewall/uspfilter/conntrack/icmp.go @@ -3,6 +3,7 @@ package conntrack import ( "context" "fmt" + "net" "net/netip" "sync" "time" @@ -19,6 +20,10 @@ const ( DefaultICMPTimeout = 30 * time.Second // ICMPCleanupInterval is how often we check for stale ICMP connections ICMPCleanupInterval = 15 * time.Second + + // MaxICMPPayloadLength is the maximum length of ICMP payload we consider for original packet info, + // which includes the IP header (20 bytes) and transport header (8 bytes) + MaxICMPPayloadLength = 28 ) // ICMPConnKey uniquely identifies an ICMP connection @@ -29,7 +34,7 @@ type ICMPConnKey struct { } func (i ICMPConnKey) String() string { - return fmt.Sprintf("%s -> %s (id %d)", i.SrcIP, i.DstIP, i.ID) + return fmt.Sprintf("%s → %s (id %d)", i.SrcIP, i.DstIP, i.ID) } // ICMPConnTrack represents an ICMP connection state @@ -50,6 +55,72 @@ type ICMPTracker struct { flowLogger nftypes.FlowLogger } +// ICMPInfo holds ICMP type, code, and payload for lazy string formatting in logs +type ICMPInfo struct { + TypeCode layers.ICMPv4TypeCode + PayloadData [MaxICMPPayloadLength]byte + // actual length of valid data + PayloadLen int +} + +// String implements fmt.Stringer for lazy evaluation in log messages +func (info ICMPInfo) String() string { + if info.isErrorMessage() && info.PayloadLen >= MaxICMPPayloadLength { + if origInfo := info.parseOriginalPacket(); origInfo != "" { + return fmt.Sprintf("%s (original: %s)", info.TypeCode, origInfo) + } + } + + return info.TypeCode.String() +} + +// isErrorMessage returns true if this ICMP type carries original packet info +func (info ICMPInfo) isErrorMessage() bool { + typ := info.TypeCode.Type() + return typ == 3 || // Destination Unreachable + typ == 5 || // Redirect + typ == 11 || // Time Exceeded + typ == 12 // Parameter Problem +} + +// parseOriginalPacket extracts info about the original packet from ICMP payload +func (info ICMPInfo) parseOriginalPacket() string { + if info.PayloadLen < MaxICMPPayloadLength { + return "" + } + + // TODO: handle IPv6 + if version := (info.PayloadData[0] >> 4) & 0xF; version != 4 { + return "" + } + + protocol := info.PayloadData[9] + srcIP := net.IP(info.PayloadData[12:16]) + dstIP := net.IP(info.PayloadData[16:20]) + + transportData := info.PayloadData[20:] + + switch nftypes.Protocol(protocol) { + case nftypes.TCP: + srcPort := uint16(transportData[0])<<8 | uint16(transportData[1]) + dstPort := uint16(transportData[2])<<8 | uint16(transportData[3]) + return fmt.Sprintf("TCP %s:%d → %s:%d", srcIP, srcPort, dstIP, dstPort) + + case nftypes.UDP: + srcPort := uint16(transportData[0])<<8 | uint16(transportData[1]) + dstPort := uint16(transportData[2])<<8 | uint16(transportData[3]) + return fmt.Sprintf("UDP %s:%d → %s:%d", srcIP, srcPort, dstIP, dstPort) + + case nftypes.ICMP: + icmpType := transportData[0] + icmpCode := transportData[1] + return fmt.Sprintf("ICMP %s → %s (type %d code %d)", srcIP, dstIP, icmpType, icmpCode) + + default: + return fmt.Sprintf("Proto %d %s → %s", protocol, srcIP, dstIP) + } +} + // NewICMPTracker creates a new ICMP connection tracker func NewICMPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *ICMPTracker { if timeout == 0 { @@ -93,30 +164,64 @@ func (t *ICMPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, id uint } // TrackOutbound records an outbound ICMP connection -func (t *ICMPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, id uint16, typecode layers.ICMPv4TypeCode, size int) { +func (t *ICMPTracker) TrackOutbound( + srcIP netip.Addr, + dstIP netip.Addr, + id uint16, + typecode layers.ICMPv4TypeCode, + payload []byte, + size int, +) { if _, exists := t.updateIfExists(dstIP, srcIP, id, nftypes.Egress, size); !exists { // if (inverted direction) conn is not tracked, track this direction - t.track(srcIP, dstIP, id, typecode, nftypes.Egress, nil, size) + t.track(srcIP, dstIP, id, typecode, nftypes.Egress, nil, payload, size) } } // TrackInbound records an inbound ICMP Echo Request -func (t *ICMPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, id uint16, typecode layers.ICMPv4TypeCode, ruleId []byte, size int) { - t.track(srcIP, dstIP, id, typecode, nftypes.Ingress, ruleId, size) +func (t *ICMPTracker) TrackInbound( + srcIP netip.Addr, + dstIP netip.Addr, + id uint16, + typecode layers.ICMPv4TypeCode, + ruleId []byte, + payload []byte, + size int, +) { + t.track(srcIP, dstIP, id, typecode, nftypes.Ingress, ruleId, payload, size) } // track is the common implementation for tracking both inbound and outbound ICMP connections -func (t *ICMPTracker) track(srcIP netip.Addr, dstIP netip.Addr, id uint16, typecode layers.ICMPv4TypeCode, direction nftypes.Direction, ruleId []byte, size int) { +func (t *ICMPTracker) track( + srcIP netip.Addr, + dstIP netip.Addr, + id uint16, + typecode layers.ICMPv4TypeCode, + direction nftypes.Direction, + ruleId []byte, + payload []byte, + size int, +) { key, exists := t.updateIfExists(srcIP, dstIP, id, direction, size) if exists { return } typ, code := typecode.Type(), typecode.Code() + icmpInfo := ICMPInfo{ + TypeCode: typecode, + } + if len(payload) > 0 { + icmpInfo.PayloadLen = len(payload) + if icmpInfo.PayloadLen > MaxICMPPayloadLength { + icmpInfo.PayloadLen = MaxICMPPayloadLength + } + copy(icmpInfo.PayloadData[:], payload[:icmpInfo.PayloadLen]) + } // non echo requests don't need tracking if typ != uint8(layers.ICMPv4TypeEchoRequest) { - t.logger.Trace("New %s ICMP connection %s type %d code %d", direction, key, typ, code) + t.logger.Trace("New %s ICMP connection %s - %s", direction, key, icmpInfo) t.sendStartEvent(direction, srcIP, dstIP, typ, code, ruleId, size) return } @@ -138,7 +243,7 @@ func (t *ICMPTracker) track(srcIP netip.Addr, dstIP netip.Addr, id uint16, typec t.connections[key] = conn t.mutex.Unlock() - t.logger.Trace("New %s ICMP connection %s type %d code %d", direction, key, typ, code) + t.logger.Trace("New %s ICMP connection %s - %s", direction, key, icmpInfo) t.sendEvent(nftypes.TypeStart, conn, ruleId) } diff --git a/client/firewall/uspfilter/conntrack/icmp_test.go b/client/firewall/uspfilter/conntrack/icmp_test.go index 5a7b36a36..b15b42cf0 100644 --- a/client/firewall/uspfilter/conntrack/icmp_test.go +++ b/client/firewall/uspfilter/conntrack/icmp_test.go @@ -15,7 +15,7 @@ func BenchmarkICMPTracker(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 0, 0) + tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 0, []byte{}, 0) } }) @@ -28,7 +28,7 @@ func BenchmarkICMPTracker(b *testing.B) { // Pre-populate some connections for i := 0; i < 1000; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i), 0, 0) + tracker.TrackOutbound(srcIP, dstIP, uint16(i), 0, []byte{}, 0) } b.ResetTimer() diff --git a/client/firewall/uspfilter/forwarder/endpoint.go b/client/firewall/uspfilter/forwarder/endpoint.go index 3720eedfa..e18c083b9 100644 --- a/client/firewall/uspfilter/forwarder/endpoint.go +++ b/client/firewall/uspfilter/forwarder/endpoint.go @@ -86,5 +86,5 @@ type epID stack.TransportEndpointID func (i epID) String() string { // src and remote is swapped - return fmt.Sprintf("%s:%d -> %s:%d", i.RemoteAddress, i.RemotePort, i.LocalAddress, i.LocalPort) + return fmt.Sprintf("%s:%d → %s:%d", i.RemoteAddress, i.RemotePort, i.LocalAddress, i.LocalPort) } diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go index 64e54e293..aa42f811b 100644 --- a/client/firewall/uspfilter/forwarder/tcp.go +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -111,12 +111,12 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn if errInToOut != nil { if !isClosedError(errInToOut) { - f.logger.Error("proxyTCP: copy error (in -> out) for %s: %v", epID(id), errInToOut) + f.logger.Error("proxyTCP: copy error (in → out) for %s: %v", epID(id), errInToOut) } } if errOutToIn != nil { if !isClosedError(errOutToIn) { - f.logger.Error("proxyTCP: copy error (out -> in) for %s: %v", epID(id), errOutToIn) + f.logger.Error("proxyTCP: copy error (out → in) for %s: %v", epID(id), errOutToIn) } } diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go index f237a313d..3a761d06b 100644 --- a/client/firewall/uspfilter/forwarder/udp.go +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -250,10 +250,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack wg.Wait() if outboundErr != nil && !isClosedError(outboundErr) { - f.logger.Error("proxyUDP: copy error (outbound->inbound) for %s: %v", epID(id), outboundErr) + f.logger.Error("proxyUDP: copy error (outbound→inbound) for %s: %v", epID(id), outboundErr) } if inboundErr != nil && !isClosedError(inboundErr) { - f.logger.Error("proxyUDP: copy error (inbound->outbound) for %s: %v", epID(id), inboundErr) + f.logger.Error("proxyUDP: copy error (inbound→outbound) for %s: %v", epID(id), inboundErr) } var rxPackets, txPackets uint64 diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index c216bc302..dcff92c61 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -671,7 +671,7 @@ func (m *Manager) trackOutbound(d *decoder, srcIP, dstIP netip.Addr, size int) { flags := getTCPFlags(&d.tcp) m.tcpTracker.TrackOutbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, size) case layers.LayerTypeICMPv4: - m.icmpTracker.TrackOutbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, size) + m.icmpTracker.TrackOutbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, d.icmp4.Payload, size) } } @@ -684,7 +684,7 @@ func (m *Manager) trackInbound(d *decoder, srcIP, dstIP netip.Addr, ruleID []byt flags := getTCPFlags(&d.tcp) m.tcpTracker.TrackInbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, ruleID, size) case layers.LayerTypeICMPv4: - m.icmpTracker.TrackInbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, ruleID, size) + m.icmpTracker.TrackInbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, ruleID, d.icmp4.Payload, size) } } diff --git a/client/internal/netflow/conntrack/conntrack.go b/client/internal/netflow/conntrack/conntrack.go index d01adf135..dbb4747a5 100644 --- a/client/internal/netflow/conntrack/conntrack.go +++ b/client/internal/netflow/conntrack/conntrack.go @@ -204,7 +204,7 @@ func (c *ConnTrack) handleEvent(event nfct.Event) { eventStr = "Ended" } - log.Tracef("%s %s %s connection: %s:%d -> %s:%d", eventStr, direction, proto, srcIP, srcPort, dstIP, dstPort) + log.Tracef("%s %s %s connection: %s:%d → %s:%d", eventStr, direction, proto, srcIP, srcPort, dstIP, dstPort) c.flowLogger.StoreEvent(nftypes.EventFields{ FlowID: flowID, From 8df8c1012f647f1947a4a1d673051825240b8f2f Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:33:51 +0200 Subject: [PATCH 088/213] [client] Support wildcard DNS on iOS (#3979) --- client/internal/dns/upstream_android.go | 7 +++++++ client/internal/dns/upstream_general.go | 7 +++++++ client/internal/routemanager/client/client.go | 10 +++++----- .../routemanager/dnsinterceptor/handler.go | 20 +++++++++++++++---- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go index 52d2ba58b..e7db581b1 100644 --- a/client/internal/dns/upstream_android.go +++ b/client/internal/dns/upstream_android.go @@ -84,3 +84,10 @@ func (u *upstreamResolver) isLocalResolver(upstream string) bool { } return false } + +func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) { + return &dns.Client{ + Timeout: dialTimeout, + Net: "udp", + }, nil +} diff --git a/client/internal/dns/upstream_general.go b/client/internal/dns/upstream_general.go index 1bc06a7c1..317588a27 100644 --- a/client/internal/dns/upstream_general.go +++ b/client/internal/dns/upstream_general.go @@ -36,3 +36,10 @@ func newUpstreamResolver( func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) { return ExchangeWithFallback(ctx, &dns.Client{}, r, upstream) } + +func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) { + return &dns.Client{ + Timeout: dialTimeout, + Net: "udp", + }, nil +} diff --git a/client/internal/routemanager/client/client.go b/client/internal/routemanager/client/client.go index 11c0f5708..46bff96db 100644 --- a/client/internal/routemanager/client/client.go +++ b/client/internal/routemanager/client/client.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "reflect" - "runtime" "time" log "github.com/sirupsen/logrus" @@ -23,7 +22,7 @@ import ( const ( handlerTypeDynamic = iota - handlerTypeDomain + handlerTypeDnsInterceptor handlerTypeStatic ) @@ -566,13 +565,14 @@ func HandlerFromRoute( useNewDNSRoute bool, ) RouteHandler { switch handlerType(rt, useNewDNSRoute) { - case handlerTypeDomain: + case handlerTypeDnsInterceptor: return dnsinterceptor.New( rt, routeRefCounter, allowedIPsRefCounter, statusRecorder, dnsServer, + wgInterface, peerStore, ) case handlerTypeDynamic: @@ -596,8 +596,8 @@ func handlerType(rt *route.Route, useNewDNSRoute bool) int { return handlerTypeStatic } - if useNewDNSRoute && runtime.GOOS != "ios" { - return handlerTypeDomain + if useNewDNSRoute { + return handlerTypeDnsInterceptor } return handlerTypeDynamic } diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index 78d5e3b30..23478c88c 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -12,6 +12,7 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" + "github.com/netbirdio/netbird/client/iface/wgaddr" nbdns "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dnsfwd" "github.com/netbirdio/netbird/client/internal/peer" @@ -23,6 +24,11 @@ import ( type domainMap map[domain.Domain][]netip.Prefix +type wgInterface interface { + Name() string + Address() wgaddr.Address +} + type DnsInterceptor struct { mu sync.RWMutex route *route.Route @@ -32,6 +38,7 @@ type DnsInterceptor struct { dnsServer nbdns.Server currentPeerKey string interceptedDomains domainMap + wgInterface wgInterface peerStore *peerstore.Store } @@ -41,6 +48,7 @@ func New( allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, statusRecorder *peer.Status, dnsServer nbdns.Server, + wgInterface wgInterface, peerStore *peerstore.Store, ) *DnsInterceptor { return &DnsInterceptor{ @@ -49,6 +57,7 @@ func New( allowedIPsRefcounter: allowedIPsRefCounter, statusRecorder: statusRecorder, dnsServer: dnsServer, + wgInterface: wgInterface, interceptedDomains: make(domainMap), peerStore: peerStore, } @@ -162,13 +171,16 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { return } + client, err := nbdns.GetClientPrivate(d.wgInterface.Address().IP, d.wgInterface.Name(), nbdns.UpstreamTimeout) + if err != nil { + d.writeDNSError(w, r, fmt.Sprintf("create DNS client: %v", err)) + return + } + if r.Extra == nil { r.MsgHdr.AuthenticatedData = true } - client := &dns.Client{ - Timeout: nbdns.UpstreamTimeout, - Net: "udp", - } + upstream := fmt.Sprintf("%s:%d", upstreamIP.String(), dnsfwd.ListenPort) reply, _, err := nbdns.ExchangeWithFallback(context.TODO(), client, r, upstream) if err != nil { From dd9917f1a84cc48d8c6de77e8128484c2f1fc44f Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Mon, 16 Jun 2025 20:05:49 +0100 Subject: [PATCH 089/213] [misc] add missing images (#3987) --- .goreleaser.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 693fbfe01..ca5eafa62 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -149,6 +149,7 @@ nfpms: dockers: - image_templates: - netbirdio/netbird:{{ .Version }}-amd64 + - ghcr.io/netbirdio/netbird:{{ .Version }}-amd64 ids: - netbird goarch: amd64 @@ -164,6 +165,7 @@ dockers: - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-arm64v8 + - ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8 ids: - netbird goarch: arm64 From d4a800edd5dc0e5a830ef9bfe17c863294de605b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 17 Jun 2025 01:20:26 +0200 Subject: [PATCH 090/213] [client] Fix status recorder panic (#3988) --- client/internal/peer/status.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 629afec9b..e290ef75f 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -575,13 +575,12 @@ func (d *Status) UpdatePeerFQDN(peerPubKey, fqdn string) error { // FinishPeerListModifications this event invoke the notification func (d *Status) FinishPeerListModifications() { d.mux.Lock() + defer d.mux.Unlock() if !d.peerListChangedForNotification { - d.mux.Unlock() return } d.peerListChangedForNotification = false - d.mux.Unlock() d.notifyPeerListChanged() From 424ae28de994d7549f76c4f84328c200a943c7a3 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:55:48 +0300 Subject: [PATCH 091/213] [client] Fix UI Download URL (#3990) [client] Fix UI Download URL --- client/ui/event_handler.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go index f7072c6b8..5441f3481 100644 --- a/client/ui/event_handler.go +++ b/client/ui/event_handler.go @@ -12,6 +12,8 @@ import ( "fyne.io/fyne/v2" "fyne.io/systray" log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/version" ) type eventHandler struct { @@ -143,7 +145,7 @@ func (h *eventHandler) handleGitHubClick() { } func (h *eventHandler) handleUpdateClick() { - if err := openURL("https://netbird.io/download"); err != nil { + if err := openURL(version.DownloadUrl()); err != nil { log.Errorf("failed to open download URL: %v", err) } } From 75c1be69cfba17293d085841d58a939e7936c6c7 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:02:30 +0200 Subject: [PATCH 092/213] [client] Prioritze the local resolver in the dns handler chain (#3965) --- client/internal/dns/handler_chain.go | 7 +- client/internal/dns/handler_chain_test.go | 62 ++++---- client/internal/dns/server.go | 14 +- client/internal/dns/server_test.go | 170 ++++++++++++++++++---- 4 files changed, 183 insertions(+), 70 deletions(-) diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 22caaa761..7e7e7cc2d 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -11,9 +11,10 @@ import ( ) const ( - PriorityDNSRoute = 100 - PriorityMatchDomain = 50 - PriorityDefault = 1 + PriorityLocal = 100 + PriorityDNSRoute = 75 + PriorityUpstream = 50 + PriorityDefault = 1 ) type SubdomainMatcher interface { diff --git a/client/internal/dns/handler_chain_test.go b/client/internal/dns/handler_chain_test.go index 5f03e0758..72c0004d5 100644 --- a/client/internal/dns/handler_chain_test.go +++ b/client/internal/dns/handler_chain_test.go @@ -22,7 +22,7 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) { // Setup handlers with different priorities chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault) - chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain) + chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityUpstream) chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute) // Create test request @@ -200,7 +200,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) { priority int }{ {pattern: "*.example.com.", priority: nbdns.PriorityDefault}, - {pattern: "*.example.com.", priority: nbdns.PriorityMatchDomain}, + {pattern: "*.example.com.", priority: nbdns.PriorityUpstream}, {pattern: "*.example.com.", priority: nbdns.PriorityDNSRoute}, }, queryDomain: "test.example.com.", @@ -214,7 +214,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) { priority int }{ {pattern: "*.example.com.", priority: nbdns.PriorityDefault}, - {pattern: "test.example.com.", priority: nbdns.PriorityMatchDomain}, + {pattern: "test.example.com.", priority: nbdns.PriorityUpstream}, {pattern: "*.test.example.com.", priority: nbdns.PriorityDNSRoute}, }, queryDomain: "sub.test.example.com.", @@ -281,7 +281,7 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) { // Add handlers in priority order chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute) - chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain) + chain.AddHandler("example.com.", handler2, nbdns.PriorityUpstream) chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault) // Create test request @@ -344,13 +344,13 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) { priority int }{ {"add", "example.com.", nbdns.PriorityDNSRoute}, - {"add", "example.com.", nbdns.PriorityMatchDomain}, + {"add", "example.com.", nbdns.PriorityUpstream}, {"remove", "example.com.", nbdns.PriorityDNSRoute}, }, query: "example.com.", expectedCalls: map[int]bool{ - nbdns.PriorityDNSRoute: false, - nbdns.PriorityMatchDomain: true, + nbdns.PriorityDNSRoute: false, + nbdns.PriorityUpstream: true, }, }, { @@ -361,13 +361,13 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) { priority int }{ {"add", "example.com.", nbdns.PriorityDNSRoute}, - {"add", "example.com.", nbdns.PriorityMatchDomain}, - {"remove", "example.com.", nbdns.PriorityMatchDomain}, + {"add", "example.com.", nbdns.PriorityUpstream}, + {"remove", "example.com.", nbdns.PriorityUpstream}, }, query: "example.com.", expectedCalls: map[int]bool{ - nbdns.PriorityDNSRoute: true, - nbdns.PriorityMatchDomain: false, + nbdns.PriorityDNSRoute: true, + nbdns.PriorityUpstream: false, }, }, { @@ -378,16 +378,16 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) { priority int }{ {"add", "example.com.", nbdns.PriorityDNSRoute}, - {"add", "example.com.", nbdns.PriorityMatchDomain}, + {"add", "example.com.", nbdns.PriorityUpstream}, {"add", "example.com.", nbdns.PriorityDefault}, {"remove", "example.com.", nbdns.PriorityDNSRoute}, - {"remove", "example.com.", nbdns.PriorityMatchDomain}, + {"remove", "example.com.", nbdns.PriorityUpstream}, }, query: "example.com.", expectedCalls: map[int]bool{ - nbdns.PriorityDNSRoute: false, - nbdns.PriorityMatchDomain: false, - nbdns.PriorityDefault: true, + nbdns.PriorityDNSRoute: false, + nbdns.PriorityUpstream: false, + nbdns.PriorityDefault: true, }, }, } @@ -454,7 +454,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) { // Add handlers in mixed order chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault) chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute) - chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain) + chain.AddHandler(testDomain, matchHandler, nbdns.PriorityUpstream) // Test 1: Initial state w1 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}} @@ -490,7 +490,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) { defaultHandler.Calls = nil // Test 3: Remove middle priority handler - chain.RemoveHandler(testDomain, nbdns.PriorityMatchDomain) + chain.RemoveHandler(testDomain, nbdns.PriorityUpstream) w3 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}} // Now lowest priority handler (defaultHandler) should be called @@ -607,7 +607,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) { shouldMatch bool }{ {"EXAMPLE.COM.", nbdns.PriorityDefault, false, false}, - {"example.com.", nbdns.PriorityMatchDomain, false, false}, + {"example.com.", nbdns.PriorityUpstream, false, false}, {"Example.Com.", nbdns.PriorityDNSRoute, false, true}, }, query: "example.com.", @@ -702,8 +702,8 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { priority int subdomain bool }{ - {"add", "example.com.", nbdns.PriorityMatchDomain, true}, - {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"add", "example.com.", nbdns.PriorityUpstream, true}, + {"add", "sub.example.com.", nbdns.PriorityUpstream, false}, }, query: "sub.example.com.", expectedMatch: "sub.example.com.", @@ -717,8 +717,8 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { priority int subdomain bool }{ - {"add", "example.com.", nbdns.PriorityMatchDomain, true}, - {"add", "sub.example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "example.com.", nbdns.PriorityUpstream, true}, + {"add", "sub.example.com.", nbdns.PriorityUpstream, true}, }, query: "sub.example.com.", expectedMatch: "sub.example.com.", @@ -732,10 +732,10 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { priority int subdomain bool }{ - {"add", "example.com.", nbdns.PriorityMatchDomain, true}, - {"add", "sub.example.com.", nbdns.PriorityMatchDomain, true}, - {"add", "test.sub.example.com.", nbdns.PriorityMatchDomain, false}, - {"remove", "test.sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"add", "example.com.", nbdns.PriorityUpstream, true}, + {"add", "sub.example.com.", nbdns.PriorityUpstream, true}, + {"add", "test.sub.example.com.", nbdns.PriorityUpstream, false}, + {"remove", "test.sub.example.com.", nbdns.PriorityUpstream, false}, }, query: "test.sub.example.com.", expectedMatch: "sub.example.com.", @@ -749,7 +749,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { priority int subdomain bool }{ - {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"add", "sub.example.com.", nbdns.PriorityUpstream, false}, {"add", "example.com.", nbdns.PriorityDNSRoute, true}, }, query: "sub.example.com.", @@ -764,9 +764,9 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { priority int subdomain bool }{ - {"add", "example.com.", nbdns.PriorityMatchDomain, true}, - {"add", "other.example.com.", nbdns.PriorityMatchDomain, true}, - {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"add", "example.com.", nbdns.PriorityUpstream, true}, + {"add", "other.example.com.", nbdns.PriorityUpstream, true}, + {"add", "sub.example.com.", nbdns.PriorityUpstream, false}, }, query: "sub.example.com.", expectedMatch: "sub.example.com.", diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 7b845235c..e81aebf98 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -527,7 +527,7 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) muxUpdates = append(muxUpdates, handlerWrapper{ domain: customZone.Domain, handler: s.localResolver, - priority: PriorityMatchDomain, + priority: PriorityLocal, }) for _, record := range customZone.Records { @@ -566,7 +566,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam groupedNS := groupNSGroupsByDomain(nameServerGroups) for _, domainGroup := range groupedNS { - basePriority := PriorityMatchDomain + basePriority := PriorityUpstream if domainGroup.domain == nbdns.RootZone { basePriority = PriorityDefault } @@ -588,10 +588,14 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai // Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts priority := basePriority - i - // Check if we're about to overlap with the next priority tier - if basePriority == PriorityMatchDomain && priority <= PriorityDefault { + // Check if we're about to overlap with the next priority tier. + // This boundary check ensures that the priority of upstream handlers does not conflict + // with the default priority tier. By decrementing the priority for each handler, we avoid + // overlaps, but if the calculated priority falls into the default tier, we skip the remaining + // handlers to maintain the integrity of the priority system. + if basePriority == PriorityUpstream && priority <= PriorityDefault { log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers", - domainGroup.domain, PriorityMatchDomain-PriorityDefault) + domainGroup.domain, PriorityUpstream-PriorityDefault) break } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index e55b27910..1cf59fb5b 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -164,12 +164,12 @@ func TestUpdateDNSServer(t *testing.T) { generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{ domain: "netbird.io", handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, dummyHandler.ID(): handlerWrapper{ domain: "netbird.cloud", handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityLocal, }, generateDummyHandler(".", nameServers).ID(): handlerWrapper{ domain: nbdns.RootZone, @@ -186,7 +186,7 @@ func TestUpdateDNSServer(t *testing.T) { generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{ domain: "netbird.cloud", handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, }, initSerial: 0, @@ -210,12 +210,12 @@ func TestUpdateDNSServer(t *testing.T) { generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{ domain: "netbird.io", handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, "local-resolver": handlerWrapper{ domain: "netbird.cloud", handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityLocal, }, }, expectedLocalQs: []dns.Question{{Name: zoneRecords[0].Name, Qtype: 1, Qclass: 1}}, @@ -305,7 +305,7 @@ func TestUpdateDNSServer(t *testing.T) { generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{ domain: zoneRecords[0].Name, handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, }, initSerial: 0, @@ -321,7 +321,7 @@ func TestUpdateDNSServer(t *testing.T) { generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{ domain: zoneRecords[0].Name, handler: dummyHandler, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, }, initSerial: 0, @@ -495,7 +495,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { "id1": handlerWrapper{ domain: zoneRecords[0].Name, handler: &local.Resolver{}, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, } //dnsServer.localResolver.RegisteredMap = local.RegistrationMap{local.BuildRecordKey("netbird.cloud", dns.ClassINET, dns.TypeA): struct{}{}} @@ -978,7 +978,7 @@ func TestHandlerChain_DomainPriorities(t *testing.T) { } chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute) - chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain) + chain.AddHandler("example.com.", upstreamHandler, PriorityUpstream) testCases := []struct { name string @@ -1059,14 +1059,14 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, "upstream-group2": { domain: "example.com", handler: &mockHandler{ Id: "upstream-group2", }, - priority: PriorityMatchDomain - 1, + priority: PriorityUpstream - 1, }, } @@ -1093,21 +1093,21 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, "upstream-group2": { domain: "example.com", handler: &mockHandler{ Id: "upstream-group2", }, - priority: PriorityMatchDomain - 1, + priority: PriorityUpstream - 1, }, "upstream-other": { domain: "other.com", handler: &mockHandler{ Id: "upstream-other", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, } @@ -1128,7 +1128,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group2", }, - priority: PriorityMatchDomain - 1, + priority: PriorityUpstream - 1, }, }, expectedHandlers: map[string]string{ @@ -1146,7 +1146,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, }, expectedHandlers: map[string]string{ @@ -1164,7 +1164,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group3", }, - priority: PriorityMatchDomain + 1, + priority: PriorityUpstream + 1, }, // Keep existing groups with their original priorities { @@ -1172,14 +1172,14 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, { domain: "example.com", handler: &mockHandler{ Id: "upstream-group2", }, - priority: PriorityMatchDomain - 1, + priority: PriorityUpstream - 1, }, }, expectedHandlers: map[string]string{ @@ -1199,14 +1199,14 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, { domain: "example.com", handler: &mockHandler{ Id: "upstream-group2", }, - priority: PriorityMatchDomain - 1, + priority: PriorityUpstream - 1, }, // Add group3 with lowest priority { @@ -1214,7 +1214,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group3", }, - priority: PriorityMatchDomain - 2, + priority: PriorityUpstream - 2, }, }, expectedHandlers: map[string]string{ @@ -1335,14 +1335,14 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, { domain: "other.com", handler: &mockHandler{ Id: "upstream-other", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, }, expectedHandlers: map[string]string{ @@ -1360,28 +1360,28 @@ func TestDefaultServer_UpdateMux(t *testing.T) { handler: &mockHandler{ Id: "upstream-group1", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, { domain: "example.com", handler: &mockHandler{ Id: "upstream-group2", }, - priority: PriorityMatchDomain - 1, + priority: PriorityUpstream - 1, }, { domain: "other.com", handler: &mockHandler{ Id: "upstream-other", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, { domain: "new.com", handler: &mockHandler{ Id: "upstream-new", }, - priority: PriorityMatchDomain, + priority: PriorityUpstream, }, }, expectedHandlers: map[string]string{ @@ -1791,14 +1791,14 @@ func TestExtraDomainsRefCounting(t *testing.T) { // Register domains from different handlers with same domain server.RegisterHandler(domain.List{"*.shared.example.com"}, &MockHandler{}, PriorityDNSRoute) - server.RegisterHandler(domain.List{"shared.example.com."}, &MockHandler{}, PriorityMatchDomain) + server.RegisterHandler(domain.List{"shared.example.com."}, &MockHandler{}, PriorityUpstream) // Verify refcount is 2 zoneKey := toZone("shared.example.com") assert.Equal(t, 2, server.extraDomains[zoneKey], "Refcount should be 2 after registering same domain twice") // Deregister one handler - server.DeregisterHandler(domain.List{"shared.example.com"}, PriorityMatchDomain) + server.DeregisterHandler(domain.List{"shared.example.com"}, PriorityUpstream) // Verify refcount is 1 assert.Equal(t, 1, server.extraDomains[zoneKey], "Refcount should be 1 after deregistering one handler") @@ -1925,7 +1925,7 @@ func TestDomainCaseHandling(t *testing.T) { } server.RegisterHandler(domain.List{"MIXED.example.com"}, &MockHandler{}, PriorityDefault) - server.RegisterHandler(domain.List{"mixed.EXAMPLE.com"}, &MockHandler{}, PriorityMatchDomain) + server.RegisterHandler(domain.List{"mixed.EXAMPLE.com"}, &MockHandler{}, PriorityUpstream) assert.Equal(t, 1, len(server.extraDomains), "Case differences should be normalized") @@ -1945,3 +1945,111 @@ func TestDomainCaseHandling(t *testing.T) { assert.Contains(t, domains, "config.example.com.", "Mixed case domain should be normalized and pre.sent") assert.Contains(t, domains, "mixed.example.com.", "Mixed case domain should be normalized and present") } + +func TestLocalResolverPriorityInServer(t *testing.T) { + server := &DefaultServer{ + ctx: context.Background(), + wgInterface: &mocWGIface{}, + handlerChain: NewHandlerChain(), + localResolver: local.NewResolver(), + service: &mockService{}, + extraDomains: make(map[domain.Domain]int), + } + + config := nbdns.Config{ + ServiceEnable: true, + CustomZones: []nbdns.CustomZone{ + { + Domain: "local.example.com", + Records: []nbdns.SimpleRecord{ + { + Name: "test.local.example.com", + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: 300, + RData: "192.168.1.100", + }, + }, + }, + }, + NameServerGroups: []*nbdns.NameServerGroup{ + { + Domains: []string{"local.example.com"}, // Same domain as local records + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("8.8.8.8"), + NSType: nbdns.UDPNameServerType, + Port: 53, + }, + }, + }, + }, + } + + localMuxUpdates, _, err := server.buildLocalHandlerUpdate(config.CustomZones) + assert.NoError(t, err) + + upstreamMuxUpdates, err := server.buildUpstreamHandlerUpdate(config.NameServerGroups) + assert.NoError(t, err) + + // Verify that local handler has higher priority than upstream for same domain + var localPriority, upstreamPriority int + localFound, upstreamFound := false, false + + for _, update := range localMuxUpdates { + if update.domain == "local.example.com" { + localPriority = update.priority + localFound = true + } + } + + for _, update := range upstreamMuxUpdates { + if update.domain == "local.example.com" { + upstreamPriority = update.priority + upstreamFound = true + } + } + + assert.True(t, localFound, "Local handler should be found") + assert.True(t, upstreamFound, "Upstream handler should be found") + assert.Greater(t, localPriority, upstreamPriority, + "Local handler priority (%d) should be higher than upstream priority (%d)", + localPriority, upstreamPriority) + assert.Equal(t, PriorityLocal, localPriority, "Local handler should use PriorityLocal") + assert.Equal(t, PriorityUpstream, upstreamPriority, "Upstream handler should use PriorityUpstream") +} + +func TestLocalResolverPriorityConstants(t *testing.T) { + // Test that priority constants are ordered correctly + assert.Greater(t, PriorityLocal, PriorityDNSRoute, "Local priority should be higher than DNS route") + assert.Greater(t, PriorityLocal, PriorityUpstream, "Local priority should be higher than upstream") + assert.Greater(t, PriorityUpstream, PriorityDefault, "Upstream priority should be higher than default") + + // Test that local resolver uses the correct priority + server := &DefaultServer{ + localResolver: local.NewResolver(), + } + + config := nbdns.Config{ + CustomZones: []nbdns.CustomZone{ + { + Domain: "local.example.com", + Records: []nbdns.SimpleRecord{ + { + Name: "test.local.example.com", + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: 300, + RData: "192.168.1.100", + }, + }, + }, + }, + } + + localMuxUpdates, _, err := server.buildLocalHandlerUpdate(config.CustomZones) + assert.NoError(t, err) + assert.Len(t, localMuxUpdates, 1) + assert.Equal(t, PriorityLocal, localMuxUpdates[0].priority, "Local handler should use PriorityLocal") + assert.Equal(t, "local.example.com", localMuxUpdates[0].domain) +} From de7384e8ea363815f7a05dc3c96f7a4eaca73073 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:03:00 +0200 Subject: [PATCH 093/213] [client] Tighten allowed domains for dns forwarder (#3978) --- client/internal/dns/upstream.go | 31 +- client/internal/dnsfwd/forwarder.go | 71 +- client/internal/dnsfwd/forwarder_test.go | 630 +++++++++++++++++- .../routemanager/dnsinterceptor/handler.go | 31 +- 4 files changed, 697 insertions(+), 66 deletions(-) diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index 2fbfb3b91..c44d36599 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -2,6 +2,7 @@ package dns import ( "context" + "crypto/rand" "crypto/sha256" "encoding/hex" "errors" @@ -103,19 +104,21 @@ func (u *upstreamResolverBase) Stop() { // ServeDNS handles a DNS request func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + requestID := GenerateRequestID() + logger := log.WithField("request_id", requestID) var err error defer func() { u.checkUpstreamFails(err) }() - log.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) + logger.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) if r.Extra == nil { r.MsgHdr.AuthenticatedData = true } select { case <-u.ctx.Done(): - log.Tracef("%s has been stopped", u) + logger.Tracef("%s has been stopped", u) return default: } @@ -132,35 +135,35 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if err != nil { if errors.Is(err, context.DeadlineExceeded) || isTimeout(err) { - log.Warnf("upstream %s timed out for question domain=%s", upstream, r.Question[0].Name) + logger.Warnf("upstream %s timed out for question domain=%s", upstream, r.Question[0].Name) continue } - log.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, r.Question[0].Name, err) + logger.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, r.Question[0].Name, err) continue } if rm == nil || !rm.Response { - log.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name) + logger.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name) continue } u.successCount.Add(1) - log.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, r.Question[0].Name) + logger.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, r.Question[0].Name) if err = w.WriteMsg(rm); err != nil { - log.Errorf("failed to write DNS response for question domain=%s: %s", r.Question[0].Name, err) + logger.Errorf("failed to write DNS response for question domain=%s: %s", r.Question[0].Name, err) } // count the fails only if they happen sequentially u.failsCount.Store(0) return } u.failsCount.Add(1) - log.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name) + logger.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name) m := new(dns.Msg) m.SetRcode(r, dns.RcodeServerFailure) if err := w.WriteMsg(m); err != nil { - log.Errorf("failed to write error response for %s for question domain=%s: %s", u, r.Question[0].Name, err) + logger.Errorf("failed to write error response for %s for question domain=%s: %s", u, r.Question[0].Name, err) } } @@ -385,3 +388,13 @@ func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, u return rm, t, nil } + +func GenerateRequestID() string { + bytes := make([]byte, 4) + _, err := rand.Read(bytes) + if err != nil { + log.Errorf("failed to generate request ID: %v", err) + return "" + } + return hex.EncodeToString(bytes) +} diff --git a/client/internal/dnsfwd/forwarder.go b/client/internal/dnsfwd/forwarder.go index 45b479632..506c429cd 100644 --- a/client/internal/dnsfwd/forwarder.go +++ b/client/internal/dnsfwd/forwarder.go @@ -18,14 +18,20 @@ import ( nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/internal/peer" - nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/route" ) const errResolveFailed = "failed to resolve query for domain=%s: %v" const upstreamTimeout = 15 * time.Second +type resolver interface { + LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) +} + +type firewaller interface { + UpdateSet(set firewall.Set, prefixes []netip.Prefix) error +} + type DNSForwarder struct { listenAddress string ttl uint32 @@ -38,16 +44,18 @@ type DNSForwarder struct { mutex sync.RWMutex fwdEntries []*ForwarderEntry - firewall firewall.Manager + firewall firewaller + resolver resolver } -func NewDNSForwarder(listenAddress string, ttl uint32, firewall firewall.Manager, statusRecorder *peer.Status) *DNSForwarder { +func NewDNSForwarder(listenAddress string, ttl uint32, firewall firewaller, statusRecorder *peer.Status) *DNSForwarder { log.Debugf("creating DNS forwarder with listen_address=%s ttl=%d", listenAddress, ttl) return &DNSForwarder{ listenAddress: listenAddress, ttl: ttl, firewall: firewall, statusRecorder: statusRecorder, + resolver: net.DefaultResolver, } } @@ -57,14 +65,17 @@ func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error { // UDP server mux := dns.NewServeMux() f.mux = mux + mux.HandleFunc(".", f.handleDNSQueryUDP) f.dnsServer = &dns.Server{ Addr: f.listenAddress, Net: "udp", Handler: mux, } + // TCP server tcpMux := dns.NewServeMux() f.tcpMux = tcpMux + tcpMux.HandleFunc(".", f.handleDNSQueryTCP) f.tcpServer = &dns.Server{ Addr: f.listenAddress, Net: "tcp", @@ -87,30 +98,13 @@ func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error { // return the first error we get (e.g. bind failure or shutdown) return <-errCh } + func (f *DNSForwarder) UpdateDomains(entries []*ForwarderEntry) { f.mutex.Lock() defer f.mutex.Unlock() - if f.mux == nil { - log.Debug("DNS mux is nil, skipping domain update") - f.fwdEntries = entries - return - } - - oldDomains := filterDomains(f.fwdEntries) - for _, d := range oldDomains { - f.mux.HandleRemove(d.PunycodeString()) - f.tcpMux.HandleRemove(d.PunycodeString()) - } - - newDomains := filterDomains(entries) - for _, d := range newDomains { - f.mux.HandleFunc(d.PunycodeString(), f.handleDNSQueryUDP) - f.tcpMux.HandleFunc(d.PunycodeString(), f.handleDNSQueryTCP) - } - f.fwdEntries = entries - log.Debugf("Updated domains from %v to %v", oldDomains, newDomains) + log.Debugf("Updated DNS forwarder with %d domains", len(entries)) } func (f *DNSForwarder) Close(ctx context.Context) error { @@ -157,22 +151,31 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) *dns return nil } + mostSpecificResId, matchingEntries := f.getMatchingEntries(strings.TrimSuffix(domain, ".")) + // query doesn't match any configured domain + if mostSpecificResId == "" { + resp.Rcode = dns.RcodeRefused + if err := w.WriteMsg(resp); err != nil { + log.Errorf("failed to write DNS response: %v", err) + } + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), upstreamTimeout) defer cancel() - ips, err := net.DefaultResolver.LookupNetIP(ctx, network, domain) + ips, err := f.resolver.LookupNetIP(ctx, network, domain) if err != nil { f.handleDNSError(w, query, resp, domain, err) return nil } - f.updateInternalState(domain, ips) + f.updateInternalState(ips, mostSpecificResId, matchingEntries) f.addIPsToResponse(resp, domain, ips) return resp } func (f *DNSForwarder) handleDNSQueryUDP(w dns.ResponseWriter, query *dns.Msg) { - resp := f.handleDNSQuery(w, query) if resp == nil { return @@ -206,9 +209,8 @@ func (f *DNSForwarder) handleDNSQueryTCP(w dns.ResponseWriter, query *dns.Msg) { } } -func (f *DNSForwarder) updateInternalState(domain string, ips []netip.Addr) { +func (f *DNSForwarder) updateInternalState(ips []netip.Addr, mostSpecificResId route.ResID, matchingEntries []*ForwarderEntry) { var prefixes []netip.Prefix - mostSpecificResId, matchingEntries := f.getMatchingEntries(strings.TrimSuffix(domain, ".")) if mostSpecificResId != "" { for _, ip := range ips { var prefix netip.Prefix @@ -339,16 +341,3 @@ func (f *DNSForwarder) getMatchingEntries(domain string) (route.ResID, []*Forwar return selectedResId, matches } - -// filterDomains returns a list of normalized domains -func filterDomains(entries []*ForwarderEntry) domain.List { - newDomains := make(domain.List, 0, len(entries)) - for _, d := range entries { - if d.Domain == "" { - log.Warn("empty domain in DNS forwarder") - continue - } - newDomains = append(newDomains, domain.Domain(nbdns.NormalizeZone(d.Domain.PunycodeString()))) - } - return newDomains -} diff --git a/client/internal/dnsfwd/forwarder_test.go b/client/internal/dnsfwd/forwarder_test.go index f0829bbbd..d8228c733 100644 --- a/client/internal/dnsfwd/forwarder_test.go +++ b/client/internal/dnsfwd/forwarder_test.go @@ -1,11 +1,21 @@ package dnsfwd import ( + "context" + "fmt" + "net/netip" + "strings" "testing" + "time" + "github.com/miekg/dns" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + firewall "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/internal/dns/test" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/route" ) @@ -13,7 +23,7 @@ import ( func Test_getMatchingEntries(t *testing.T) { testCases := []struct { name string - storedMappings map[string]route.ResID // key: domain pattern, value: resId + storedMappings map[string]route.ResID queryDomain string expectedResId route.ResID }{ @@ -44,7 +54,7 @@ func Test_getMatchingEntries(t *testing.T) { { name: "Wildcard pattern does not match different domain", storedMappings: map[string]route.ResID{"*.example.com": "res4"}, - queryDomain: "foo.notexample.com", + queryDomain: "foo.example.org", expectedResId: "", }, { @@ -101,3 +111,619 @@ func Test_getMatchingEntries(t *testing.T) { }) } } + +type MockFirewall struct { + mock.Mock +} + +func (m *MockFirewall) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error { + args := m.Called(set, prefixes) + return args.Error(0) +} + +type MockResolver struct { + mock.Mock +} + +func (m *MockResolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) { + args := m.Called(ctx, network, host) + return args.Get(0).([]netip.Addr), args.Error(1) +} + +func TestDNSForwarder_SubdomainAccessLogic(t *testing.T) { + tests := []struct { + name string + configuredDomain string + queryDomain string + shouldMatch bool + expectedResID route.ResID + description string + }{ + { + name: "exact domain match should be allowed", + configuredDomain: "example.com", + queryDomain: "example.com", + shouldMatch: true, + expectedResID: "test-res-id", + description: "Direct match to configured domain should work", + }, + { + name: "subdomain access should be restricted", + configuredDomain: "example.com", + queryDomain: "mail.example.com", + shouldMatch: false, + expectedResID: "", + description: "Subdomain should not be accessible unless explicitly configured", + }, + { + name: "wildcard should allow subdomains", + configuredDomain: "*.example.com", + queryDomain: "mail.example.com", + shouldMatch: true, + expectedResID: "test-res-id", + description: "Wildcard domains should allow subdomain access", + }, + { + name: "wildcard should allow base domain", + configuredDomain: "*.example.com", + queryDomain: "example.com", + shouldMatch: true, + expectedResID: "test-res-id", + description: "Wildcard should also match the base domain", + }, + { + name: "deep subdomain should be restricted", + configuredDomain: "example.com", + queryDomain: "deep.mail.example.com", + shouldMatch: false, + expectedResID: "", + description: "Deep subdomains should not be accessible", + }, + { + name: "wildcard allows deep subdomains", + configuredDomain: "*.example.com", + queryDomain: "deep.mail.example.com", + shouldMatch: true, + expectedResID: "test-res-id", + description: "Wildcard should allow deep subdomains", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + forwarder := &DNSForwarder{} + + d, err := domain.FromString(tt.configuredDomain) + require.NoError(t, err) + + entries := []*ForwarderEntry{ + { + Domain: d, + ResID: "test-res-id", + }, + } + + forwarder.UpdateDomains(entries) + + resID, matchingEntries := forwarder.getMatchingEntries(tt.queryDomain) + + if tt.shouldMatch { + assert.Equal(t, tt.expectedResID, resID, "Expected matching ResID") + assert.NotEmpty(t, matchingEntries, "Expected matching entries") + t.Logf("✓ Domain %s correctly matches pattern %s", tt.queryDomain, tt.configuredDomain) + } else { + assert.Equal(t, tt.expectedResID, resID, "Expected no ResID match") + assert.Empty(t, matchingEntries, "Expected no matching entries") + t.Logf("✓ Domain %s correctly does NOT match pattern %s", tt.queryDomain, tt.configuredDomain) + } + }) + } +} + +func TestDNSForwarder_UnauthorizedDomainAccess(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + tests := []struct { + name string + configuredDomain string + queryDomain string + shouldResolve bool + description string + }{ + { + name: "configured exact domain resolves", + configuredDomain: "example.com", + queryDomain: "example.com", + shouldResolve: true, + description: "Exact match should resolve", + }, + { + name: "unauthorized subdomain blocked", + configuredDomain: "example.com", + queryDomain: "mail.example.com", + shouldResolve: false, + description: "Subdomain should be blocked without wildcard", + }, + { + name: "wildcard allows subdomain", + configuredDomain: "*.example.com", + queryDomain: "mail.example.com", + shouldResolve: true, + description: "Wildcard should allow subdomain", + }, + { + name: "wildcard allows base domain", + configuredDomain: "*.example.com", + queryDomain: "example.com", + shouldResolve: true, + description: "Wildcard should allow base domain", + }, + { + name: "unrelated domain blocked", + configuredDomain: "example.com", + queryDomain: "example.org", + shouldResolve: false, + description: "Unrelated domain should be blocked", + }, + { + name: "deep subdomain blocked", + configuredDomain: "example.com", + queryDomain: "deep.mail.example.com", + shouldResolve: false, + description: "Deep subdomain should be blocked", + }, + { + name: "wildcard allows deep subdomain", + configuredDomain: "*.example.com", + queryDomain: "deep.mail.example.com", + shouldResolve: true, + description: "Wildcard should allow deep subdomain", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockFirewall := &MockFirewall{} + mockResolver := &MockResolver{} + + if tt.shouldResolve { + mockFirewall.On("UpdateSet", mock.AnythingOfType("manager.Set"), mock.AnythingOfType("[]netip.Prefix")).Return(nil) + + // Mock successful DNS resolution + fakeIP := netip.MustParseAddr("1.2.3.4") + mockResolver.On("LookupNetIP", mock.Anything, "ip4", dns.Fqdn(tt.queryDomain)).Return([]netip.Addr{fakeIP}, nil) + } + + forwarder := NewDNSForwarder("127.0.0.1:0", 300, mockFirewall, &peer.Status{}) + forwarder.resolver = mockResolver + + d, err := domain.FromString(tt.configuredDomain) + require.NoError(t, err) + + entries := []*ForwarderEntry{ + { + Domain: d, + ResID: "test-res-id", + Set: firewall.NewDomainSet([]domain.Domain{d}), + }, + } + + forwarder.UpdateDomains(entries) + + query := &dns.Msg{} + query.SetQuestion(dns.Fqdn(tt.queryDomain), dns.TypeA) + + mockWriter := &test.MockResponseWriter{} + resp := forwarder.handleDNSQuery(mockWriter, query) + + if tt.shouldResolve { + require.NotNil(t, resp, "Expected response for authorized domain") + require.Equal(t, dns.RcodeSuccess, resp.Rcode, "Expected successful response") + assert.NotEmpty(t, resp.Answer, "Expected DNS answer records") + + time.Sleep(10 * time.Millisecond) + mockFirewall.AssertExpectations(t) + mockResolver.AssertExpectations(t) + } else { + if resp != nil { + assert.True(t, len(resp.Answer) == 0 || resp.Rcode != dns.RcodeSuccess, + "Unauthorized domain should not return successful answers") + } + mockFirewall.AssertNotCalled(t, "UpdateSet") + mockResolver.AssertNotCalled(t, "LookupNetIP") + } + }) + } +} + +func TestDNSForwarder_FirewallSetUpdates(t *testing.T) { + tests := []struct { + name string + configuredDomains []string + query string + mockIP string + shouldResolve bool + expectedSetCount int // How many sets should be updated + description string + }{ + { + name: "exact domain gets firewall update", + configuredDomains: []string{"example.com"}, + query: "example.com", + mockIP: "1.1.1.1", + shouldResolve: true, + expectedSetCount: 1, + description: "Single exact match updates one set", + }, + { + name: "wildcard domain gets firewall update", + configuredDomains: []string{"*.example.com"}, + query: "mail.example.com", + mockIP: "1.1.1.2", + shouldResolve: true, + expectedSetCount: 1, + description: "Wildcard match updates one set", + }, + { + name: "overlapping exact and wildcard both get updates", + configuredDomains: []string{"*.example.com", "mail.example.com"}, + query: "mail.example.com", + mockIP: "1.1.1.3", + shouldResolve: true, + expectedSetCount: 2, + description: "Both exact and wildcard sets should be updated", + }, + { + name: "unauthorized domain gets no firewall update", + configuredDomains: []string{"example.com"}, + query: "mail.example.com", + mockIP: "1.1.1.4", + shouldResolve: false, + expectedSetCount: 0, + description: "No firewall update for unauthorized domains", + }, + { + name: "multiple wildcards matching get all updated", + configuredDomains: []string{"*.example.com", "*.sub.example.com"}, + query: "test.sub.example.com", + mockIP: "1.1.1.5", + shouldResolve: true, + expectedSetCount: 2, + description: "All matching wildcard sets should be updated", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockFirewall := &MockFirewall{} + mockResolver := &MockResolver{} + + // Set up forwarder + forwarder := NewDNSForwarder("127.0.0.1:0", 300, mockFirewall, &peer.Status{}) + forwarder.resolver = mockResolver + + // Create entries and track sets + var entries []*ForwarderEntry + sets := make([]firewall.Set, 0) + + for i, configDomain := range tt.configuredDomains { + d, err := domain.FromString(configDomain) + require.NoError(t, err) + + set := firewall.NewDomainSet([]domain.Domain{d}) + sets = append(sets, set) + + entries = append(entries, &ForwarderEntry{ + Domain: d, + ResID: route.ResID(fmt.Sprintf("res-%d", i)), + Set: set, + }) + } + + forwarder.UpdateDomains(entries) + + // Set up mocks + if tt.shouldResolve { + fakeIP := netip.MustParseAddr(tt.mockIP) + mockResolver.On("LookupNetIP", mock.Anything, "ip4", dns.Fqdn(tt.query)). + Return([]netip.Addr{fakeIP}, nil).Once() + + expectedPrefixes := []netip.Prefix{netip.PrefixFrom(fakeIP, 32)} + + // Count how many sets should actually match + updateCount := 0 + for i, entry := range entries { + domain := strings.ToLower(tt.query) + pattern := entry.Domain.PunycodeString() + + matches := false + if strings.HasPrefix(pattern, "*.") { + baseDomain := strings.TrimPrefix(pattern, "*.") + if domain == baseDomain || strings.HasSuffix(domain, "."+baseDomain) { + matches = true + } + } else if domain == pattern { + matches = true + } + + if matches { + mockFirewall.On("UpdateSet", sets[i], expectedPrefixes).Return(nil).Once() + updateCount++ + } + } + + assert.Equal(t, tt.expectedSetCount, updateCount, + "Expected %d sets to be updated, but mock expects %d", + tt.expectedSetCount, updateCount) + } + + // Execute query + dnsQuery := &dns.Msg{} + dnsQuery.SetQuestion(dns.Fqdn(tt.query), dns.TypeA) + + mockWriter := &test.MockResponseWriter{} + resp := forwarder.handleDNSQuery(mockWriter, dnsQuery) + + // Verify response + if tt.shouldResolve { + require.NotNil(t, resp, "Expected response for authorized domain") + require.Equal(t, dns.RcodeSuccess, resp.Rcode) + require.NotEmpty(t, resp.Answer) + } else if resp != nil { + assert.True(t, resp.Rcode == dns.RcodeRefused || len(resp.Answer) == 0, + "Unauthorized domain should be refused or have no answers") + } + + // Verify all mock expectations were met + mockFirewall.AssertExpectations(t) + mockResolver.AssertExpectations(t) + }) + } +} + +// Test to verify that multiple IPs for one domain result in all prefixes being sent together +func TestDNSForwarder_MultipleIPsInSingleUpdate(t *testing.T) { + mockFirewall := &MockFirewall{} + mockResolver := &MockResolver{} + + forwarder := NewDNSForwarder("127.0.0.1:0", 300, mockFirewall, &peer.Status{}) + forwarder.resolver = mockResolver + + // Configure a single domain + d, err := domain.FromString("example.com") + require.NoError(t, err) + + set := firewall.NewDomainSet([]domain.Domain{d}) + entries := []*ForwarderEntry{{ + Domain: d, + ResID: "test-res", + Set: set, + }} + + forwarder.UpdateDomains(entries) + + // Mock resolver returns multiple IPs + ips := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("1.1.1.2"), + netip.MustParseAddr("1.1.1.3"), + } + mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com."). + Return(ips, nil).Once() + + // Expect ONE UpdateSet call with ALL prefixes + expectedPrefixes := []netip.Prefix{ + netip.PrefixFrom(ips[0], 32), + netip.PrefixFrom(ips[1], 32), + netip.PrefixFrom(ips[2], 32), + } + mockFirewall.On("UpdateSet", set, expectedPrefixes).Return(nil).Once() + + // Execute query + query := &dns.Msg{} + query.SetQuestion("example.com.", dns.TypeA) + + mockWriter := &test.MockResponseWriter{} + resp := forwarder.handleDNSQuery(mockWriter, query) + + // Verify response contains all IPs + require.NotNil(t, resp) + require.Equal(t, dns.RcodeSuccess, resp.Rcode) + require.Len(t, resp.Answer, 3, "Should have 3 answer records") + + // Verify mocks + mockFirewall.AssertExpectations(t) + mockResolver.AssertExpectations(t) +} + +func TestDNSForwarder_ResponseCodes(t *testing.T) { + tests := []struct { + name string + queryType uint16 + queryDomain string + configured string + expectedCode int + description string + }{ + { + name: "unauthorized domain returns REFUSED", + queryType: dns.TypeA, + queryDomain: "evil.com", + configured: "example.com", + expectedCode: dns.RcodeRefused, + description: "RFC compliant REFUSED for unauthorized queries", + }, + { + name: "unsupported query type returns NOTIMP", + queryType: dns.TypeMX, + queryDomain: "example.com", + configured: "example.com", + expectedCode: dns.RcodeNotImplemented, + description: "RFC compliant NOTIMP for unsupported types", + }, + { + name: "CNAME query returns NOTIMP", + queryType: dns.TypeCNAME, + queryDomain: "example.com", + configured: "example.com", + expectedCode: dns.RcodeNotImplemented, + description: "CNAME queries not supported", + }, + { + name: "TXT query returns NOTIMP", + queryType: dns.TypeTXT, + queryDomain: "example.com", + configured: "example.com", + expectedCode: dns.RcodeNotImplemented, + description: "TXT queries not supported", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + forwarder := NewDNSForwarder("127.0.0.1:0", 300, nil, &peer.Status{}) + + d, err := domain.FromString(tt.configured) + require.NoError(t, err) + + entries := []*ForwarderEntry{{Domain: d, ResID: "test-res"}} + forwarder.UpdateDomains(entries) + + query := &dns.Msg{} + query.SetQuestion(dns.Fqdn(tt.queryDomain), tt.queryType) + + // Capture the written response + var writtenResp *dns.Msg + mockWriter := &test.MockResponseWriter{ + WriteMsgFunc: func(m *dns.Msg) error { + writtenResp = m + return nil + }, + } + + _ = forwarder.handleDNSQuery(mockWriter, query) + + // Check the response written to the writer + require.NotNil(t, writtenResp, "Expected response to be written") + assert.Equal(t, tt.expectedCode, writtenResp.Rcode, tt.description) + }) + } +} + +func TestDNSForwarder_TCPTruncation(t *testing.T) { + // Test that large UDP responses are truncated with TC bit set + mockResolver := &MockResolver{} + forwarder := NewDNSForwarder("127.0.0.1:0", 300, nil, &peer.Status{}) + forwarder.resolver = mockResolver + + d, _ := domain.FromString("example.com") + entries := []*ForwarderEntry{{Domain: d, ResID: "test-res"}} + forwarder.UpdateDomains(entries) + + // Mock many IPs to create a large response + var manyIPs []netip.Addr + for i := 0; i < 100; i++ { + manyIPs = append(manyIPs, netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i%256))) + } + mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com.").Return(manyIPs, nil) + + // Query without EDNS0 + query := &dns.Msg{} + query.SetQuestion("example.com.", dns.TypeA) + + var writtenResp *dns.Msg + mockWriter := &test.MockResponseWriter{ + WriteMsgFunc: func(m *dns.Msg) error { + writtenResp = m + return nil + }, + } + forwarder.handleDNSQueryUDP(mockWriter, query) + + require.NotNil(t, writtenResp) + assert.True(t, writtenResp.Truncated, "Large response should be truncated") + assert.LessOrEqual(t, writtenResp.Len(), dns.MinMsgSize, "Response should fit in minimum UDP size") +} + +func TestDNSForwarder_MultipleOverlappingPatterns(t *testing.T) { + // Test complex overlapping pattern scenarios + mockFirewall := &MockFirewall{} + mockResolver := &MockResolver{} + + forwarder := NewDNSForwarder("127.0.0.1:0", 300, mockFirewall, &peer.Status{}) + forwarder.resolver = mockResolver + + // Set up complex overlapping patterns + patterns := []string{ + "*.example.com", // Matches all subdomains + "*.mail.example.com", // More specific wildcard + "smtp.mail.example.com", // Exact match + "example.com", // Base domain + } + + var entries []*ForwarderEntry + sets := make(map[string]firewall.Set) + + for _, pattern := range patterns { + d, _ := domain.FromString(pattern) + set := firewall.NewDomainSet([]domain.Domain{d}) + sets[pattern] = set + entries = append(entries, &ForwarderEntry{ + Domain: d, + ResID: route.ResID("res-" + pattern), + Set: set, + }) + } + + forwarder.UpdateDomains(entries) + + // Test smtp.mail.example.com - should match 3 patterns + fakeIP := netip.MustParseAddr("1.2.3.4") + mockResolver.On("LookupNetIP", mock.Anything, "ip4", "smtp.mail.example.com.").Return([]netip.Addr{fakeIP}, nil) + + expectedPrefix := netip.PrefixFrom(fakeIP, 32) + // All three matching patterns should get firewall updates + mockFirewall.On("UpdateSet", sets["smtp.mail.example.com"], []netip.Prefix{expectedPrefix}).Return(nil) + mockFirewall.On("UpdateSet", sets["*.mail.example.com"], []netip.Prefix{expectedPrefix}).Return(nil) + mockFirewall.On("UpdateSet", sets["*.example.com"], []netip.Prefix{expectedPrefix}).Return(nil) + + query := &dns.Msg{} + query.SetQuestion("smtp.mail.example.com.", dns.TypeA) + + mockWriter := &test.MockResponseWriter{} + resp := forwarder.handleDNSQuery(mockWriter, query) + + require.NotNil(t, resp) + assert.Equal(t, dns.RcodeSuccess, resp.Rcode) + + // Verify all three sets were updated + mockFirewall.AssertExpectations(t) + + // Verify the most specific ResID was selected + // (exact match should win over wildcards) + resID, matches := forwarder.getMatchingEntries("smtp.mail.example.com") + assert.Equal(t, route.ResID("res-smtp.mail.example.com"), resID) + assert.Len(t, matches, 3, "Should match 3 patterns") +} + +func TestDNSForwarder_EmptyQuery(t *testing.T) { + // Test handling of malformed query with no questions + forwarder := NewDNSForwarder("127.0.0.1:0", 300, nil, &peer.Status{}) + + query := &dns.Msg{} + // Don't set any question + + writeCalled := false + mockWriter := &test.MockResponseWriter{ + WriteMsgFunc: func(m *dns.Msg) error { + writeCalled = true + return nil + }, + } + resp := forwarder.handleDNSQuery(mockWriter, query) + + assert.Nil(t, resp, "Should return nil for empty query") + assert.False(t, writeCalled, "Should not write response for empty query") +} diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index 23478c88c..d643d1e32 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -144,15 +144,18 @@ func (d *DnsInterceptor) RemoveAllowedIPs() error { // ServeDNS implements the dns.Handler interface func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + requestID := nbdns.GenerateRequestID() + logger := log.WithField("request_id", requestID) + if len(r.Question) == 0 { return } - log.Tracef("received DNS request for domain=%s type=%v class=%v", + logger.Tracef("received DNS request for domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) // pass if non A/AAAA query if r.Question[0].Qtype != dns.TypeA && r.Question[0].Qtype != dns.TypeAAAA { - d.continueToNextHandler(w, r, "non A/AAAA query") + d.continueToNextHandler(w, r, logger, "non A/AAAA query") return } @@ -161,13 +164,13 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { d.mu.RUnlock() if peerKey == "" { - d.writeDNSError(w, r, "no current peer key") + d.writeDNSError(w, r, logger, "no current peer key") return } upstreamIP, err := d.getUpstreamIP(peerKey) if err != nil { - d.writeDNSError(w, r, fmt.Sprintf("get upstream IP: %v", err)) + d.writeDNSError(w, r, logger, fmt.Sprintf("get upstream IP: %v", err)) return } @@ -184,9 +187,9 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { upstream := fmt.Sprintf("%s:%d", upstreamIP.String(), dnsfwd.ListenPort) reply, _, err := nbdns.ExchangeWithFallback(context.TODO(), client, r, upstream) if err != nil { - log.Errorf("failed to exchange DNS request with %s (%s) for domain=%s: %v", upstreamIP.String(), peerKey, r.Question[0].Name, err) + logger.Errorf("failed to exchange DNS request with %s (%s) for domain=%s: %v", upstreamIP.String(), peerKey, r.Question[0].Name, err) if err := w.WriteMsg(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure, Id: r.Id}}); err != nil { - log.Errorf("failed writing DNS response: %v", err) + logger.Errorf("failed writing DNS response: %v", err) } return } @@ -196,34 +199,34 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { answer = reply.Answer } - log.Tracef("upstream %s (%s) DNS response for domain=%s answers=%v", upstreamIP.String(), peerKey, r.Question[0].Name, answer) + logger.Tracef("upstream %s (%s) DNS response for domain=%s answers=%v", upstreamIP.String(), peerKey, r.Question[0].Name, answer) reply.Id = r.Id if err := d.writeMsg(w, reply); err != nil { - log.Errorf("failed writing DNS response: %v", err) + logger.Errorf("failed writing DNS response: %v", err) } } -func (d *DnsInterceptor) writeDNSError(w dns.ResponseWriter, r *dns.Msg, reason string) { - log.Warnf("failed to query upstream for domain=%s: %s", r.Question[0].Name, reason) +func (d *DnsInterceptor) writeDNSError(w dns.ResponseWriter, r *dns.Msg, logger *log.Entry, reason string) { + logger.Warnf("failed to query upstream for domain=%s: %s", r.Question[0].Name, reason) resp := new(dns.Msg) resp.SetRcode(r, dns.RcodeServerFailure) if err := w.WriteMsg(resp); err != nil { - log.Errorf("failed to write DNS error response: %v", err) + logger.Errorf("failed to write DNS error response: %v", err) } } // continueToNextHandler signals the handler chain to try the next handler -func (d *DnsInterceptor) continueToNextHandler(w dns.ResponseWriter, r *dns.Msg, reason string) { - log.Tracef("continuing to next handler for domain=%s reason=%s", r.Question[0].Name, reason) +func (d *DnsInterceptor) continueToNextHandler(w dns.ResponseWriter, r *dns.Msg, logger *log.Entry, reason string) { + logger.Tracef("continuing to next handler for domain=%s reason=%s", r.Question[0].Name, reason) resp := new(dns.Msg) resp.SetRcode(r, dns.RcodeNameError) // Set Zero bit to signal handler chain to continue resp.MsgHdr.Zero = true if err := w.WriteMsg(resp); err != nil { - log.Errorf("failed writing DNS continue response: %v", err) + logger.Errorf("failed writing DNS continue response: %v", err) } } From f61ebdb3bc07b35d2b5335d383269dee53ac67d5 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:07:14 +0300 Subject: [PATCH 094/213] [client] Fix DNS Interceptor Build Error (#3993) [client] Fix DNS Interceptor Build Error --- client/internal/routemanager/dnsinterceptor/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index d643d1e32..66557e888 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -176,7 +176,7 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { client, err := nbdns.GetClientPrivate(d.wgInterface.Address().IP, d.wgInterface.Name(), nbdns.UpstreamTimeout) if err != nil { - d.writeDNSError(w, r, fmt.Sprintf("create DNS client: %v", err)) + d.writeDNSError(w, r, logger, fmt.Sprintf("create DNS client: %v", err)) return } From af2b427751c7bbe6f74c532a2b38dcd15bd6025c Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 17 Jun 2025 14:14:11 +0100 Subject: [PATCH 095/213] [management] Avoid recalculating next peer expiration (#3991) * Avoid recalculating next peer expiration - Check if an account schedule is already running - Cancel executing schedules only when changes occurs - Add more context info to logs * fix tests --- management/server/account.go | 17 +++++++++++++---- management/server/account_test.go | 5 +---- management/server/peer.go | 5 +++-- management/server/scheduler.go | 19 +++++++++++++++++-- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 82f5ee4a3..daeaf6e55 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -24,6 +24,7 @@ import ( "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/formatter/hook" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbcache "github.com/netbirdio/netbird/management/server/cache" @@ -409,14 +410,15 @@ func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.C event = activity.AccountPeerLoginExpirationDisabled am.peerLoginExpiry.Cancel(ctx, []string{accountID}) } else { - am.checkAndSchedulePeerLoginExpiration(ctx, accountID) + am.schedulePeerLoginExpiration(ctx, accountID) } am.StoreEvent(ctx, userID, accountID, accountID, event, nil) } if oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration { am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerLoginExpirationDurationUpdated, nil) - am.checkAndSchedulePeerLoginExpiration(ctx, accountID) + am.peerLoginExpiry.Cancel(ctx, []string{accountID}) + am.schedulePeerLoginExpiration(ctx, accountID) } } @@ -454,6 +456,10 @@ func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context. func (am *DefaultAccountManager) peerLoginExpirationJob(ctx context.Context, accountID string) func() (time.Duration, bool) { return func() (time.Duration, bool) { + //nolint + ctx := context.WithValue(ctx, nbcontext.AccountIDKey, accountID) + //nolint + ctx = context.WithValue(ctx, hook.ExecutionContextKey, fmt.Sprintf("%s-PEER-EXPIRATION", hook.SystemSource)) unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() @@ -478,8 +484,11 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(ctx context.Context, acc } } -func (am *DefaultAccountManager) checkAndSchedulePeerLoginExpiration(ctx context.Context, accountID string) { - am.peerLoginExpiry.Cancel(ctx, []string{accountID}) +func (am *DefaultAccountManager) schedulePeerLoginExpiration(ctx context.Context, accountID string) { + if am.peerLoginExpiry.IsSchedulerRunning(accountID) { + log.WithContext(ctx).Tracef("peer login expiration job for account %s is already scheduled", accountID) + return + } if nextRun, ok := am.getNextPeerExpiration(ctx, accountID); ok { go am.peerLoginExpiry.Schedule(ctx, nextRun, accountID, am.peerLoginExpirationJob(ctx, accountID)) } diff --git a/management/server/account_test.go b/management/server/account_test.go index ba0191c03..c3b1f31a6 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1862,11 +1862,8 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing. require.NoError(t, err, "expecting to update account settings successfully but got error") wg := &sync.WaitGroup{} - wg.Add(2) + wg.Add(1) manager.peerLoginExpiry = &MockScheduler{ - CancelFunc: func(ctx context.Context, IDs []string) { - wg.Done() - }, ScheduleFunc: func(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) { wg.Done() }, diff --git a/management/server/peer.go b/management/server/peer.go index 4a468a6cd..f2469e09b 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -133,7 +133,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK } if peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled { - am.checkAndSchedulePeerLoginExpiration(ctx, accountID) + am.schedulePeerLoginExpiration(ctx, accountID) } if peer.InactivityExpirationEnabled && settings.PeerInactivityExpirationEnabled { @@ -296,7 +296,8 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain)) if peer.AddedWithSSOLogin() && peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled { - am.checkAndSchedulePeerLoginExpiration(ctx, accountID) + am.peerLoginExpiry.Cancel(ctx, []string{accountID}) + am.schedulePeerLoginExpiration(ctx, accountID) } } diff --git a/management/server/scheduler.go b/management/server/scheduler.go index 147b50fc6..df73c9a1d 100644 --- a/management/server/scheduler.go +++ b/management/server/scheduler.go @@ -12,6 +12,7 @@ import ( type Scheduler interface { Cancel(ctx context.Context, IDs []string) Schedule(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) + IsSchedulerRunning(ID string) bool } // MockScheduler is a mock implementation of Scheduler @@ -26,7 +27,7 @@ func (mock *MockScheduler) Cancel(ctx context.Context, IDs []string) { mock.CancelFunc(ctx, IDs) return } - log.WithContext(ctx).Errorf("MockScheduler doesn't have Cancel function defined ") + log.WithContext(ctx).Warnf("MockScheduler doesn't have Cancel function defined ") } // Schedule mocks the Schedule function of the Scheduler interface @@ -35,7 +36,13 @@ func (mock *MockScheduler) Schedule(ctx context.Context, in time.Duration, ID st mock.ScheduleFunc(ctx, in, ID, job) return } - log.WithContext(ctx).Errorf("MockScheduler doesn't have Schedule function defined") + log.WithContext(ctx).Warnf("MockScheduler doesn't have Schedule function defined") +} + +func (mock *MockScheduler) IsSchedulerRunning(ID string) bool { + // MockScheduler does not implement IsSchedulerRunning, so we return false + log.Warnf("MockScheduler doesn't have IsSchedulerRunning function defined") + return false } // DefaultScheduler is a generic structure that allows to schedule jobs (functions) to run in the future and cancel them. @@ -124,3 +131,11 @@ func (wm *DefaultScheduler) Schedule(ctx context.Context, in time.Duration, ID s }() } + +// IsSchedulerRunning checks if a job with the provided ID is scheduled to run +func (wm *DefaultScheduler) IsSchedulerRunning(ID string) bool { + wm.mu.Lock() + defer wm.mu.Unlock() + _, ok := wm.jobs[ID] + return ok +} From 81d83245e177aa0aee8841c6489b73d58dc9ca09 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:02:04 +0300 Subject: [PATCH 096/213] [client] Fix logic in updateStatus to correctly handle connection state (#3994) [client] Fix logic in updateStatus to correctly handle connection state (#3994) --- client/ui/client_ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index a1ca4120f..a55152216 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -572,7 +572,7 @@ func (s *serviceClient) updateStatus() error { var systrayIconState bool switch { - case status.Status == string(internal.StatusConnected) && !s.mUp.Disabled(): + case status.Status == string(internal.StatusConnected) && s.mUp.Disabled(): s.connected = true s.sendNotification = true if s.isUpdateIconActive { From 08c9107c61653002d9804cdf197e4d1723f6d29f Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:14:08 +0300 Subject: [PATCH 097/213] [client] fix connection state handling (#3995) [client] fix connection state handling (#3995) --- client/ui/client_ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index a55152216..00a535dd6 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -572,7 +572,7 @@ func (s *serviceClient) updateStatus() error { var systrayIconState bool switch { - case status.Status == string(internal.StatusConnected) && s.mUp.Disabled(): + case status.Status == string(internal.StatusConnected): s.connected = true s.sendNotification = true if s.isUpdateIconActive { From 7c6b85b4cb89252ee389454d3ef070dfbdea7ee1 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 18 Jun 2025 16:40:29 +0300 Subject: [PATCH 098/213] [management] Refactor routes to use store methods (#2928) --- client/cmd/system.go | 2 +- client/internal/routemanager/mock.go | 2 +- client/system/info.go | 18 +- management/server/group.go | 9 - management/server/route.go | 436 ++++++++++-------- management/server/status/error.go | 4 + management/server/store/sql_store.go | 98 ++-- management/server/store/sql_store_test.go | 140 +++++- management/server/store/store.go | 4 +- management/server/testdata/extended-store.sql | 1 + 10 files changed, 443 insertions(+), 271 deletions(-) diff --git a/client/cmd/system.go b/client/cmd/system.go index 83ce8d215..f63432401 100644 --- a/client/cmd/system.go +++ b/client/cmd/system.go @@ -38,5 +38,5 @@ func init() { upCmd.PersistentFlags().BoolVar(&blockInbound, blockInboundFlag, false, "Block inbound connections. If enabled, the client will not allow any inbound connections to the local machine nor routed networks.\n"+ - "This overrides any policies received from the management service.") + "This overrides any policies received from the management service.") } diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index 63bad689e..742294cdf 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -15,7 +15,7 @@ import ( // MockManager is the mock instance of a route manager type MockManager struct { ClassifyRoutesFunc func(routes []*route.Route) (map[route.ID]*route.Route, route.HAMap) - UpdateRoutesFunc func (updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error + UpdateRoutesFunc func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error TriggerSelectionFunc func(haMap route.HAMap) GetRouteSelectorFunc func() *routeselector.RouteSelector GetClientRoutesFunc func() route.HAMap diff --git a/client/system/info.go b/client/system/info.go index a0a5fe8b3..aff10ece3 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -59,16 +59,16 @@ type Info struct { Environment Environment Files []File // for posture checks - RosenpassEnabled bool - RosenpassPermissive bool - ServerSSHAllowed bool + RosenpassEnabled bool + RosenpassPermissive bool + ServerSSHAllowed bool - DisableClientRoutes bool - DisableServerRoutes bool - DisableDNS bool - DisableFirewall bool - BlockLANAccess bool - BlockInbound bool + DisableClientRoutes bool + DisableServerRoutes bool + DisableDNS bool + DisableFirewall bool + BlockLANAccess bool + BlockInbound bool LazyConnectionEnabled bool } diff --git a/management/server/group.go b/management/server/group.go index c26a0cfc1..130a67145 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -664,15 +664,6 @@ func areGroupChangesAffectPeers(ctx context.Context, transaction store.Store, ac return false, nil } -func (am *DefaultAccountManager) anyGroupHasPeers(account *types.Account, groupIDs []string) bool { - for _, groupID := range groupIDs { - if group, exists := account.Groups[groupID]; exists && group.HasPeers() { - return true - } - } - return false -} - // anyGroupHasPeersOrResources checks if any of the given groups in the account have peers or resources. func anyGroupHasPeersOrResources(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) (bool, error) { groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, groupIDs) diff --git a/management/server/route.go b/management/server/route.go index 02755a708..32ff39977 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -4,19 +4,19 @@ import ( "context" "fmt" "net/netip" + "slices" "unicode/utf8" "github.com/rs/xid" - "github.com/netbirdio/netbird/management/server/permissions/modules" - "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/store" - "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/permissions/modules" + "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" ) @@ -30,13 +30,19 @@ func (am *DefaultAccountManager) GetRoute(ctx context.Context, accountID string, return nil, status.NewPermissionDeniedError() } - return am.Store.GetRouteByID(ctx, store.LockingStrengthShare, string(routeID), accountID) + return am.Store.GetRouteByID(ctx, store.LockingStrengthShare, accountID, string(routeID)) } // checkRoutePrefixOrDomainsExistForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups. -func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account *types.Account, peerID string, routeID route.ID, peerGroupIDs []string, prefix netip.Prefix, domains domain.List) error { +func checkRoutePrefixOrDomainsExistForPeers(ctx context.Context, transaction store.Store, accountID string, checkRoute *route.Route, groupsMap map[string]*types.Group) error { // routes can have both peer and peer_groups - routesWithPrefix := account.GetRoutesByPrefixOrDomains(prefix, domains) + prefix := checkRoute.Network + domains := checkRoute.Domains + + routesWithPrefix, err := getRoutesByPrefixOrDomains(ctx, transaction, accountID, prefix, domains) + if err != nil { + return err + } // lets remember all the peers and the peer groups from routesWithPrefix seenPeers := make(map[string]bool) @@ -45,18 +51,24 @@ func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account for _, prefixRoute := range routesWithPrefix { // we skip route(s) with the same network ID as we want to allow updating of the existing route // when creating a new route routeID is newly generated so nothing will be skipped - if routeID == prefixRoute.ID { + if checkRoute.ID == prefixRoute.ID { continue } if prefixRoute.Peer != "" { seenPeers[string(prefixRoute.ID)] = true } + + peerGroupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, prefixRoute.PeerGroups) + if err != nil { + return err + } + for _, groupID := range prefixRoute.PeerGroups { seenPeerGroups[groupID] = true - group := account.GetGroup(groupID) - if group == nil { + group, ok := peerGroupsMap[groupID] + if !ok || group == nil { return status.Errorf( status.InvalidArgument, "failed to add route with %s - peer group %s doesn't exist", getRouteDescriptor(prefix, domains), groupID, @@ -69,12 +81,13 @@ func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account } } - if peerID != "" { + if peerID := checkRoute.Peer; peerID != "" { // check that peerID exists and is not in any route as single peer or part of the group - peer := account.GetPeer(peerID) - if peer == nil { + _, err = transaction.GetPeerByID(context.Background(), store.LockingStrengthShare, accountID, peerID) + if err != nil { return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) } + if _, ok := seenPeers[peerID]; ok { return status.Errorf(status.AlreadyExists, "failed to add route with %s - peer %s already has this route", getRouteDescriptor(prefix, domains), peerID) @@ -82,9 +95,8 @@ func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account } // check that peerGroupIDs are not in any route peerGroups list - for _, groupID := range peerGroupIDs { - group := account.GetGroup(groupID) // we validated the group existence before entering this function, no need to check again. - + for _, groupID := range checkRoute.PeerGroups { + group := groupsMap[groupID] // we validated the group existence before entering this function, no need to check again. if _, ok := seenPeerGroups[groupID]; ok { return status.Errorf( status.AlreadyExists, "failed to add route with %s - peer group %s already has this route", @@ -92,12 +104,18 @@ func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account } // check that the peers from peerGroupIDs groups are not the same peers we saw in routesWithPrefix + peersMap, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthShare, accountID, group.Peers) + if err != nil { + return err + } + for _, id := range group.Peers { if _, ok := seenPeers[id]; ok { - peer := account.GetPeer(id) - if peer == nil { - return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) + peer, ok := peersMap[id] + if !ok || peer == nil { + return status.Errorf(status.InvalidArgument, "peer with ID %s not found", id) } + return status.Errorf(status.AlreadyExists, "failed to add route with %s - peer %s from the group %s already has this route", getRouteDescriptor(prefix, domains), peer.Name, group.Name) @@ -128,97 +146,58 @@ func (am *DefaultAccountManager) CreateRoute(ctx context.Context, accountID stri return nil, status.NewPermissionDeniedError() } - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return nil, err - } - if len(domains) > 0 && prefix.IsValid() { return nil, status.Errorf(status.InvalidArgument, "domains and network should not be provided at the same time") } - if len(domains) == 0 && !prefix.IsValid() { - return nil, status.Errorf(status.InvalidArgument, "invalid Prefix") - } + var newRoute *route.Route + var updateAccountPeers bool - if len(domains) > 0 { - prefix = getPlaceholderIP() - } - - if peerID != "" && len(peerGroupIDs) != 0 { - return nil, status.Errorf( - status.InvalidArgument, - "peer with ID %s and peers group %s should not be provided at the same time", - peerID, peerGroupIDs) - } - - var newRoute route.Route - newRoute.ID = route.ID(xid.New().String()) - - if len(peerGroupIDs) > 0 { - err = validateGroups(peerGroupIDs, account.Groups) - if err != nil { - return nil, err + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + newRoute = &route.Route{ + ID: route.ID(xid.New().String()), + AccountID: accountID, + Network: prefix, + Domains: domains, + KeepRoute: keepRoute, + NetID: netID, + Description: description, + Peer: peerID, + PeerGroups: peerGroupIDs, + NetworkType: networkType, + Masquerade: masquerade, + Metric: metric, + Enabled: enabled, + Groups: groups, + AccessControlGroups: accessControlGroupIDs, } - } - if len(accessControlGroupIDs) > 0 { - err = validateGroups(accessControlGroupIDs, account.Groups) - if err != nil { - return nil, err + if err = validateRoute(ctx, transaction, accountID, newRoute); err != nil { + return err } - } - err = am.checkRoutePrefixOrDomainsExistForPeers(account, peerID, newRoute.ID, peerGroupIDs, prefix, domains) + updateAccountPeers, err = areRouteChangesAffectPeers(ctx, transaction, newRoute) + if err != nil { + return err + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + return transaction.SaveRoute(ctx, store.LockingStrengthUpdate, newRoute) + }) if err != nil { return nil, err } - if metric < route.MinMetric || metric > route.MaxMetric { - return nil, status.Errorf(status.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric) - } - - if utf8.RuneCountInString(string(netID)) > route.MaxNetIDChar || netID == "" { - return nil, status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar) - } - - err = validateGroups(groups, account.Groups) - if err != nil { - return nil, err - } - - newRoute.Peer = peerID - newRoute.PeerGroups = peerGroupIDs - newRoute.Network = prefix - newRoute.Domains = domains - newRoute.NetworkType = networkType - newRoute.Description = description - newRoute.NetID = netID - newRoute.Masquerade = masquerade - newRoute.Metric = metric - newRoute.Enabled = enabled - newRoute.Groups = groups - newRoute.KeepRoute = keepRoute - newRoute.AccessControlGroups = accessControlGroupIDs - - if account.Routes == nil { - account.Routes = make(map[route.ID]*route.Route) - } - - account.Routes[newRoute.ID] = &newRoute - - account.Network.IncSerial() - if err = am.Store.SaveAccount(ctx, account); err != nil { - return nil, err - } - - if am.isRouteChangeAffectPeers(account, &newRoute) { - am.UpdateAccountPeers(ctx, accountID) - } - am.StoreEvent(ctx, userID, string(newRoute.ID), accountID, activity.RouteCreated, newRoute.EventMeta()) - return &newRoute, nil + if updateAccountPeers { + am.UpdateAccountPeers(ctx, accountID) + } + + return newRoute, nil } // SaveRoute saves route @@ -226,6 +205,115 @@ func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userI unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Update) + if err != nil { + return status.NewPermissionValidationError(err) + } + if !allowed { + return status.NewPermissionDeniedError() + } + + var oldRoute *route.Route + var oldRouteAffectsPeers bool + var newRouteAffectsPeers bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + if err = validateRoute(ctx, transaction, accountID, routeToSave); err != nil { + return err + } + + oldRoute, err = transaction.GetRouteByID(ctx, store.LockingStrengthUpdate, accountID, string(routeToSave.ID)) + if err != nil { + return err + } + + oldRouteAffectsPeers, err = areRouteChangesAffectPeers(ctx, transaction, oldRoute) + if err != nil { + return err + } + + newRouteAffectsPeers, err = areRouteChangesAffectPeers(ctx, transaction, routeToSave) + if err != nil { + return err + } + routeToSave.AccountID = accountID + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + return transaction.SaveRoute(ctx, store.LockingStrengthUpdate, routeToSave) + }) + if err != nil { + return err + } + + am.StoreEvent(ctx, userID, string(routeToSave.ID), accountID, activity.RouteUpdated, routeToSave.EventMeta()) + + if oldRouteAffectsPeers || newRouteAffectsPeers { + am.UpdateAccountPeers(ctx, accountID) + } + + return nil +} + +// DeleteRoute deletes route with routeID +func (am *DefaultAccountManager) DeleteRoute(ctx context.Context, accountID string, routeID route.ID, userID string) error { + unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) + defer unlock() + + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Delete) + if err != nil { + return status.NewPermissionValidationError(err) + } + if !allowed { + return status.NewPermissionDeniedError() + } + + var route *route.Route + var updateAccountPeers bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + route, err = transaction.GetRouteByID(ctx, store.LockingStrengthUpdate, accountID, string(routeID)) + if err != nil { + return err + } + + updateAccountPeers, err = areRouteChangesAffectPeers(ctx, transaction, route) + if err != nil { + return err + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + return transaction.DeleteRoute(ctx, store.LockingStrengthUpdate, accountID, string(routeID)) + }) + + am.StoreEvent(ctx, userID, string(route.ID), accountID, activity.RouteRemoved, route.EventMeta()) + + if updateAccountPeers { + am.UpdateAccountPeers(ctx, accountID) + } + + return nil +} + +// ListRoutes returns a list of routes from account +func (am *DefaultAccountManager) ListRoutes(ctx context.Context, accountID, userID string) ([]*route.Route, error) { + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Read) + if err != nil { + return nil, status.NewPermissionValidationError(err) + } + if !allowed { + return nil, status.NewPermissionDeniedError() + } + + return am.Store.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID) +} + +func validateRoute(ctx context.Context, transaction store.Store, accountID string, routeToSave *route.Route) error { if routeToSave == nil { return status.Errorf(status.InvalidArgument, "route provided is nil") } @@ -238,19 +326,6 @@ func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userI return status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar) } - allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Update) - if err != nil { - return status.NewPermissionValidationError(err) - } - if !allowed { - return status.NewPermissionDeniedError() - } - - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return err - } - if len(routeToSave.Domains) > 0 && routeToSave.Network.IsValid() { return status.Errorf(status.InvalidArgument, "domains and network should not be provided at the same time") } @@ -267,96 +342,39 @@ func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userI return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time") } + groupsMap, err := validateRouteGroups(ctx, transaction, accountID, routeToSave) + if err != nil { + return err + } + + return checkRoutePrefixOrDomainsExistForPeers(ctx, transaction, accountID, routeToSave, groupsMap) +} + +// validateRouteGroups validates the route groups and returns the validated groups map. +func validateRouteGroups(ctx context.Context, transaction store.Store, accountID string, routeToSave *route.Route) (map[string]*types.Group, error) { + groupsToValidate := slices.Concat(routeToSave.Groups, routeToSave.PeerGroups, routeToSave.AccessControlGroups) + groupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, groupsToValidate) + if err != nil { + return nil, err + } + if len(routeToSave.PeerGroups) > 0 { - err = validateGroups(routeToSave.PeerGroups, account.Groups) - if err != nil { - return err + if err = validateGroups(routeToSave.PeerGroups, groupsMap); err != nil { + return nil, err } } if len(routeToSave.AccessControlGroups) > 0 { - err = validateGroups(routeToSave.AccessControlGroups, account.Groups) - if err != nil { - return err + if err = validateGroups(routeToSave.AccessControlGroups, groupsMap); err != nil { + return nil, err } } - err = am.checkRoutePrefixOrDomainsExistForPeers(account, routeToSave.Peer, routeToSave.ID, routeToSave.Copy().PeerGroups, routeToSave.Network, routeToSave.Domains) - if err != nil { - return err + if err = validateGroups(routeToSave.Groups, groupsMap); err != nil { + return nil, err } - err = validateGroups(routeToSave.Groups, account.Groups) - if err != nil { - return err - } - - oldRoute := account.Routes[routeToSave.ID] - account.Routes[routeToSave.ID] = routeToSave - - account.Network.IncSerial() - if err = am.Store.SaveAccount(ctx, account); err != nil { - return err - } - - if am.isRouteChangeAffectPeers(account, oldRoute) || am.isRouteChangeAffectPeers(account, routeToSave) { - am.UpdateAccountPeers(ctx, accountID) - } - - am.StoreEvent(ctx, userID, string(routeToSave.ID), accountID, activity.RouteUpdated, routeToSave.EventMeta()) - - return nil -} - -// DeleteRoute deletes route with routeID -func (am *DefaultAccountManager) DeleteRoute(ctx context.Context, accountID string, routeID route.ID, userID string) error { - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) - defer unlock() - - allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Delete) - if err != nil { - return status.NewPermissionValidationError(err) - } - if !allowed { - return status.NewPermissionDeniedError() - } - - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return err - } - - routy := account.Routes[routeID] - if routy == nil { - return status.Errorf(status.NotFound, "route with ID %s doesn't exist", routeID) - } - delete(account.Routes, routeID) - - account.Network.IncSerial() - if err = am.Store.SaveAccount(ctx, account); err != nil { - return err - } - - am.StoreEvent(ctx, userID, string(routy.ID), accountID, activity.RouteRemoved, routy.EventMeta()) - - if am.isRouteChangeAffectPeers(account, routy) { - am.UpdateAccountPeers(ctx, accountID) - } - - return nil -} - -// ListRoutes returns a list of routes from account -func (am *DefaultAccountManager) ListRoutes(ctx context.Context, accountID, userID string) ([]*route.Route, error) { - allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Routes, operations.Read) - if err != nil { - return nil, status.NewPermissionValidationError(err) - } - if !allowed { - return nil, status.NewPermissionDeniedError() - } - - return am.Store.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID) + return groupsMap, nil } func toProtocolRoute(route *route.Route) *proto.Route { @@ -455,8 +473,40 @@ func getProtoPortInfo(rule *types.RouteFirewallRule) *proto.PortInfo { return &portInfo } -// isRouteChangeAffectPeers checks if a given route affects peers by determining -// if it has a routing peer, distribution, or peer groups that include peers -func (am *DefaultAccountManager) isRouteChangeAffectPeers(account *types.Account, route *route.Route) bool { - return am.anyGroupHasPeers(account, route.Groups) || am.anyGroupHasPeers(account, route.PeerGroups) || route.Peer != "" +// areRouteChangesAffectPeers checks if a given route affects peers by determining +// if it has a routing peer, distribution, or peer groups that include peers. +func areRouteChangesAffectPeers(ctx context.Context, transaction store.Store, route *route.Route) (bool, error) { + if route.Peer != "" { + return true, nil + } + + hasPeers, err := anyGroupHasPeersOrResources(ctx, transaction, route.AccountID, route.Groups) + if err != nil { + return false, err + } + + if hasPeers { + return true, nil + } + + return anyGroupHasPeersOrResources(ctx, transaction, route.AccountID, route.PeerGroups) +} + +// GetRoutesByPrefixOrDomains return list of routes by account and route prefix +func getRoutesByPrefixOrDomains(ctx context.Context, transaction store.Store, accountID string, prefix netip.Prefix, domains domain.List) ([]*route.Route, error) { + accountRoutes, err := transaction.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + routes := make([]*route.Route, 0) + for _, r := range accountRoutes { + dynamic := r.IsDynamic() + if dynamic && r.Domains.PunycodeString() == domains.PunycodeString() || + !dynamic && r.Network.String() == prefix.String() { + routes = append(routes, r) + } + } + + return routes, nil } diff --git a/management/server/status/error.go b/management/server/status/error.go index 8fbe0bad9..5a6f6d1a7 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -227,3 +227,7 @@ func NewUserRoleNotFoundError(role string) error { func NewOperationNotFoundError(operation operations.Operation) error { return Errorf(NotFound, "operation: %s not found", operation) } + +func NewRouteNotFoundError(routeID string) error { + return Errorf(NotFound, "route: %s not found", routeID) +} diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index d81890775..a6c4d56bf 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -23,8 +23,6 @@ import ( "gorm.io/gorm/clause" "gorm.io/gorm/logger" - "github.com/netbirdio/netbird/management/server/util" - nbdns "github.com/netbirdio/netbird/dns" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" @@ -34,6 +32,7 @@ import ( "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" ) @@ -1968,12 +1967,58 @@ func (s *SqlStore) DeletePostureChecks(ctx context.Context, lockStrength Locking // GetAccountRoutes retrieves network routes for an account. func (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) { - return getRecords[*route.Route](s.db, lockStrength, accountID) + var routes []*route.Route + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Find(&routes, accountIDCondition, accountID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get routes from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get routes from store") + } + + return routes, nil } // GetRouteByID retrieves a route by its ID and account ID. -func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrength, routeID string, accountID string) (*route.Route, error) { - return getRecordByID[route.Route](s.db, lockStrength, routeID, accountID) +func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID string, routeID string) (*route.Route, error) { + var route *route.Route + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + First(&route, accountAndIDQueryCondition, accountID, routeID) + if err := result.Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.NewRouteNotFoundError(routeID) + } + log.WithContext(ctx).Errorf("failed to get route from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get route from store") + } + + return route, nil +} + +// SaveRoute saves a route to the database. +func (s *SqlStore) SaveRoute(ctx context.Context, lockStrength LockingStrength, route *route.Route) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(route) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to save route to the store: %s", err) + return status.Errorf(status.Internal, "failed to save route to store") + } + + return nil +} + +// DeleteRoute deletes a route from the database. +func (s *SqlStore) DeleteRoute(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&route.Route{}, accountAndIDQueryCondition, accountID, routeID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to delete route from the store: %s", err) + return status.Errorf(status.Internal, "failed to delete route from store") + } + + if result.RowsAffected == 0 { + return status.NewRouteNotFoundError(routeID) + } + + return nil } // GetAccountSetupKeys retrieves setup keys for an account. @@ -2104,49 +2149,6 @@ func (s *SqlStore) DeleteNameServerGroup(ctx context.Context, lockStrength Locki return nil } -// getRecords retrieves records from the database based on the account ID. -func getRecords[T any](db *gorm.DB, lockStrength LockingStrength, accountID string) ([]T, error) { - tx := db - if lockStrength != LockingStrengthNone { - tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) - } - - var record []T - - result := tx.Find(&record, accountIDCondition, accountID) - if err := result.Error; err != nil { - parts := strings.Split(fmt.Sprintf("%T", record), ".") - recordType := parts[len(parts)-1] - - return nil, status.Errorf(status.Internal, "failed to get account %ss from store: %v", recordType, err) - } - - return record, nil -} - -// getRecordByID retrieves a record by its ID and account ID from the database. -func getRecordByID[T any](db *gorm.DB, lockStrength LockingStrength, recordID, accountID string) (*T, error) { - tx := db - if lockStrength != LockingStrengthNone { - tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) - } - - var record T - - result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). - First(&record, accountAndIDQueryCondition, accountID, recordID) - if err := result.Error; err != nil { - parts := strings.Split(fmt.Sprintf("%T", record), ".") - recordType := parts[len(parts)-1] - - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, status.Errorf(status.NotFound, "%s not found", recordType) - } - return nil, status.Errorf(status.Internal, "failed to get %s from store: %v", recordType, err) - } - return &record, nil -} - // SaveDNSSettings saves the DNS settings to the store. func (s *SqlStore) SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error { result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 2c1f5f8e6..fab9048e5 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -19,21 +19,17 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/server/util" - nbdns "github.com/netbirdio/netbird/dns" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" - "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/types" - - route2 "github.com/netbirdio/netbird/route" - - "github.com/netbirdio/netbird/management/server/status" - nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/management/server/util" nbroute "github.com/netbirdio/netbird/route" + route2 "github.com/netbirdio/netbird/route" ) func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { @@ -3247,6 +3243,132 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { require.NoError(t, err) require.Equal(t, 8003, len(accountGroups)) } +func TestSqlStore_GetAccountRoutes(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectedCount int + }{ + { + name: "retrieve routes by existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectedCount: 1, + }, + { + name: "non-existing account ID", + accountID: "nonexistent", + expectedCount: 0, + }, + { + name: "empty account ID", + accountID: "", + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + routes, err := store.GetAccountRoutes(context.Background(), LockingStrengthShare, tt.accountID) + require.NoError(t, err) + require.Len(t, routes, tt.expectedCount) + }) + } +} + +func TestSqlStore_GetRouteByID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + tests := []struct { + name string + routeID string + expectError bool + }{ + { + name: "retrieve existing route", + routeID: "ct03t427qv97vmtmglog", + expectError: false, + }, + { + name: "retrieve non-existing route", + routeID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty route ID", + routeID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + route, err := store.GetRouteByID(context.Background(), LockingStrengthShare, accountID, tt.routeID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, route) + } else { + require.NoError(t, err) + require.NotNil(t, route) + require.Equal(t, tt.routeID, string(route.ID)) + } + }) + } +} + +func TestSqlStore_SaveRoute(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + route := &route2.Route{ + ID: "route-id", + AccountID: accountID, + Network: netip.MustParsePrefix("10.10.0.0/16"), + NetID: "netID", + PeerGroups: []string{"routeA"}, + NetworkType: route2.IPv4Network, + Masquerade: true, + Metric: 9999, + Enabled: true, + Groups: []string{"groupA"}, + AccessControlGroups: []string{}, + } + err = store.SaveRoute(context.Background(), LockingStrengthUpdate, route) + require.NoError(t, err) + + saveRoute, err := store.GetRouteByID(context.Background(), LockingStrengthShare, accountID, string(route.ID)) + require.NoError(t, err) + require.Equal(t, route, saveRoute) + +} + +func TestSqlStore_DeleteRoute(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + routeID := "ct03t427qv97vmtmglog" + + err = store.DeleteRoute(context.Background(), LockingStrengthUpdate, accountID, routeID) + require.NoError(t, err) + + route, err := store.GetRouteByID(context.Background(), LockingStrengthShare, accountID, routeID) + require.Error(t, err) + require.Nil(t, route) +} func TestSqlStore_GetAccountMeta(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) diff --git a/management/server/store/store.go b/management/server/store/store.go index c7b103454..d41379b1c 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -145,7 +145,9 @@ type Store interface { DeleteSetupKey(ctx context.Context, lockStrength LockingStrength, accountID, keyID string) error GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) - GetRouteByID(ctx context.Context, lockStrength LockingStrength, routeID string, accountID string) (*route.Route, error) + GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) (*route.Route, error) + SaveRoute(ctx context.Context, lockStrength LockingStrength, route *route.Route) error + DeleteRoute(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) error GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*dns.NameServerGroup, error) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, nameServerGroupID string, accountID string) (*dns.NameServerGroup, error) diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql index 324bf6293..0393d1ade 100644 --- a/management/server/testdata/extended-store.sql +++ b/management/server/testdata/extended-store.sql @@ -38,4 +38,5 @@ INSERT INTO "groups" VALUES('cfefqs706sqkneg59g2g','bf1c8084-ba50-4ce7-9439-3465 INSERT INTO posture_checks VALUES('csplshq7qv948l48f7t0','NetBird Version > 0.32.0','','bf1c8084-ba50-4ce7-9439-34653001fc3b','{"NBVersionCheck":{"MinVersion":"0.31.0"}}'); INSERT INTO posture_checks VALUES('cspnllq7qv95uq1r4k90','Allow Berlin and Deny local network 172.16.1.0/24','','bf1c8084-ba50-4ce7-9439-34653001fc3b','{"GeoLocationCheck":{"Locations":[{"CountryCode":"DE","CityName":"Berlin"}],"Action":"allow"},"PeerNetworkRangeCheck":{"Action":"deny","Ranges":["172.16.1.0/24"]}}'); INSERT INTO name_server_groups VALUES('csqdelq7qv97ncu7d9t0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Google DNS','Google DNS Servers','[{"IP":"8.8.8.8","NSType":1,"Port":53},{"IP":"8.8.4.4","NSType":1,"Port":53}]','["cfefqs706sqkneg59g2g"]',1,'[]',1,0); +INSERT INTO routes VALUES('ct03t427qv97vmtmglog','bf1c8084-ba50-4ce7-9439-34653001fc3b','"10.10.0.0/16"',NULL,0,'aws-eu-central-1-vpc','Production VPC in Frankfurt','ct03r5q7qv97vmtmglng',NULL,1,1,9999,1,'["cfefqs706sqkneg59g2g"]',NULL); INSERT INTO installations VALUES(1,''); From 91f7ee6a3c83bfa06eec47c577a6c1ad20cce05b Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 18 Jun 2025 16:49:03 +0200 Subject: [PATCH 099/213] Fix route notification On Android ignore the dynamic roots in the route notifications --- .../routemanager/notifier/notifier.go | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/client/internal/routemanager/notifier/notifier.go b/client/internal/routemanager/notifier/notifier.go index 25a3a71e0..3cc7c3308 100644 --- a/client/internal/routemanager/notifier/notifier.go +++ b/client/internal/routemanager/notifier/notifier.go @@ -32,7 +32,6 @@ func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { func (n *Notifier) SetInitialClientRoutes(clientRoutes []*route.Route) { nets := make([]string, 0) for _, r := range clientRoutes { - // filter out domain routes if r.IsDynamic() { continue } @@ -46,30 +45,27 @@ func (n *Notifier) OnNewRoutes(idMap route.HAMap) { if runtime.GOOS != "android" { return } - newNets := make([]string, 0) + + var newNets []string for _, routes := range idMap { for _, r := range routes { + if r.IsDynamic() { + continue + } newNets = append(newNets, r.Network.String()) } } sort.Strings(newNets) - switch runtime.GOOS { - case "android": - if !n.hasDiff(n.initialRouteRanges, newNets) { - return - } - default: - if !n.hasDiff(n.routeRanges, newNets) { - return - } + if !n.hasDiff(n.initialRouteRanges, newNets) { + return } n.routeRanges = newNets - n.notify() } +// OnNewPrefixes is called from iOS only func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) { newNets := make([]string, 0) for _, prefix := range prefixes { @@ -77,19 +73,11 @@ func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) { } sort.Strings(newNets) - switch runtime.GOOS { - case "android": - if !n.hasDiff(n.initialRouteRanges, newNets) { - return - } - default: - if !n.hasDiff(n.routeRanges, newNets) { - return - } + if !n.hasDiff(n.routeRanges, newNets) { + return } n.routeRanges = newNets - n.notify() } From 0e5dc9d41229ef3f368aa5feb336934815b69a04 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:23:23 +0200 Subject: [PATCH 100/213] [client] Add more Android advanced settings (#4001) --- client/android/preferences.go | 138 +++++++++++++++++++++++--- client/iface/device/device_android.go | 11 +- client/iface/iface.go | 1 + client/iface/iface_new_android.go | 2 +- client/internal/config.go | 14 ++- client/internal/engine.go | 1 + 6 files changed, 149 insertions(+), 18 deletions(-) diff --git a/client/android/preferences.go b/client/android/preferences.go index 2a8b197e7..2d5668d1c 100644 --- a/client/android/preferences.go +++ b/client/android/preferences.go @@ -4,12 +4,12 @@ import ( "github.com/netbirdio/netbird/client/internal" ) -// Preferences export a subset of the internal config for gomobile +// Preferences exports a subset of the internal config for gomobile type Preferences struct { configInput internal.ConfigInput } -// NewPreferences create new Preferences instance +// NewPreferences creates a new Preferences instance func NewPreferences(configPath string) *Preferences { ci := internal.ConfigInput{ ConfigPath: configPath, @@ -17,7 +17,7 @@ func NewPreferences(configPath string) *Preferences { return &Preferences{ci} } -// GetManagementURL read url from config file +// GetManagementURL reads URL from config file func (p *Preferences) GetManagementURL() (string, error) { if p.configInput.ManagementURL != "" { return p.configInput.ManagementURL, nil @@ -30,12 +30,12 @@ func (p *Preferences) GetManagementURL() (string, error) { return cfg.ManagementURL.String(), err } -// SetManagementURL store the given url and wait for commit +// SetManagementURL stores the given URL and waits for commit func (p *Preferences) SetManagementURL(url string) { p.configInput.ManagementURL = url } -// GetAdminURL read url from config file +// GetAdminURL reads URL from config file func (p *Preferences) GetAdminURL() (string, error) { if p.configInput.AdminURL != "" { return p.configInput.AdminURL, nil @@ -48,12 +48,12 @@ func (p *Preferences) GetAdminURL() (string, error) { return cfg.AdminURL.String(), err } -// SetAdminURL store the given url and wait for commit +// SetAdminURL stores the given URL and waits for commit func (p *Preferences) SetAdminURL(url string) { p.configInput.AdminURL = url } -// GetPreSharedKey read preshared key from config file +// GetPreSharedKey reads pre-shared key from config file func (p *Preferences) GetPreSharedKey() (string, error) { if p.configInput.PreSharedKey != nil { return *p.configInput.PreSharedKey, nil @@ -66,17 +66,17 @@ func (p *Preferences) GetPreSharedKey() (string, error) { return cfg.PreSharedKey, err } -// SetPreSharedKey store the given key and wait for commit +// SetPreSharedKey stores the given key and waits for commit func (p *Preferences) SetPreSharedKey(key string) { p.configInput.PreSharedKey = &key } -// SetRosenpassEnabled store if rosenpass is enabled +// SetRosenpassEnabled stores whether Rosenpass is enabled func (p *Preferences) SetRosenpassEnabled(enabled bool) { p.configInput.RosenpassEnabled = &enabled } -// GetRosenpassEnabled read rosenpass enabled from config file +// GetRosenpassEnabled reads Rosenpass enabled status from config file func (p *Preferences) GetRosenpassEnabled() (bool, error) { if p.configInput.RosenpassEnabled != nil { return *p.configInput.RosenpassEnabled, nil @@ -89,12 +89,12 @@ func (p *Preferences) GetRosenpassEnabled() (bool, error) { return cfg.RosenpassEnabled, err } -// SetRosenpassPermissive store the given permissive and wait for commit +// SetRosenpassPermissive stores the given permissive setting and waits for commit func (p *Preferences) SetRosenpassPermissive(permissive bool) { p.configInput.RosenpassPermissive = &permissive } -// GetRosenpassPermissive read rosenpass permissive from config file +// GetRosenpassPermissive reads Rosenpass permissive setting from config file func (p *Preferences) GetRosenpassPermissive() (bool, error) { if p.configInput.RosenpassPermissive != nil { return *p.configInput.RosenpassPermissive, nil @@ -107,7 +107,119 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) { return cfg.RosenpassPermissive, err } -// Commit write out the changes into config file +// GetDisableClientRoutes reads disable client routes setting from config file +func (p *Preferences) GetDisableClientRoutes() (bool, error) { + if p.configInput.DisableClientRoutes != nil { + return *p.configInput.DisableClientRoutes, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.DisableClientRoutes, err +} + +// SetDisableClientRoutes stores the given value and waits for commit +func (p *Preferences) SetDisableClientRoutes(disable bool) { + p.configInput.DisableClientRoutes = &disable +} + +// GetDisableServerRoutes reads disable server routes setting from config file +func (p *Preferences) GetDisableServerRoutes() (bool, error) { + if p.configInput.DisableServerRoutes != nil { + return *p.configInput.DisableServerRoutes, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.DisableServerRoutes, err +} + +// SetDisableServerRoutes stores the given value and waits for commit +func (p *Preferences) SetDisableServerRoutes(disable bool) { + p.configInput.DisableServerRoutes = &disable +} + +// GetDisableDNS reads disable DNS setting from config file +func (p *Preferences) GetDisableDNS() (bool, error) { + if p.configInput.DisableDNS != nil { + return *p.configInput.DisableDNS, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.DisableDNS, err +} + +// SetDisableDNS stores the given value and waits for commit +func (p *Preferences) SetDisableDNS(disable bool) { + p.configInput.DisableDNS = &disable +} + +// GetDisableFirewall reads disable firewall setting from config file +func (p *Preferences) GetDisableFirewall() (bool, error) { + if p.configInput.DisableFirewall != nil { + return *p.configInput.DisableFirewall, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.DisableFirewall, err +} + +// SetDisableFirewall stores the given value and waits for commit +func (p *Preferences) SetDisableFirewall(disable bool) { + p.configInput.DisableFirewall = &disable +} + +// GetServerSSHAllowed reads server SSH allowed setting from config file +func (p *Preferences) GetServerSSHAllowed() (bool, error) { + if p.configInput.ServerSSHAllowed != nil { + return *p.configInput.ServerSSHAllowed, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + if cfg.ServerSSHAllowed == nil { + // Default to false for security on Android + return false, nil + } + return *cfg.ServerSSHAllowed, err +} + +// SetServerSSHAllowed stores the given value and waits for commit +func (p *Preferences) SetServerSSHAllowed(allowed bool) { + p.configInput.ServerSSHAllowed = &allowed +} + +// GetBlockInbound reads block inbound setting from config file +func (p *Preferences) GetBlockInbound() (bool, error) { + if p.configInput.BlockInbound != nil { + return *p.configInput.BlockInbound, nil + } + + cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + if err != nil { + return false, err + } + return cfg.BlockInbound, err +} + +// SetBlockInbound stores the given value and waits for commit +func (p *Preferences) SetBlockInbound(block bool) { + p.configInput.BlockInbound = &block +} + +// Commit writes out the changes to the config file func (p *Preferences) Commit() error { _, err := internal.UpdateOrCreateConfig(p.configInput) return err diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go index ab3e611e1..ae9e29bd1 100644 --- a/client/iface/device/device_android.go +++ b/client/iface/device/device_android.go @@ -24,6 +24,7 @@ type WGTunDevice struct { mtu int iceBind *bind.ICEBind tunAdapter TunAdapter + disableDNS bool name string device *device.Device @@ -32,7 +33,7 @@ type WGTunDevice struct { configurer WGConfigurer } -func NewTunDevice(address wgaddr.Address, port int, key string, mtu int, iceBind *bind.ICEBind, tunAdapter TunAdapter) *WGTunDevice { +func NewTunDevice(address wgaddr.Address, port int, key string, mtu int, iceBind *bind.ICEBind, tunAdapter TunAdapter, disableDNS bool) *WGTunDevice { return &WGTunDevice{ address: address, port: port, @@ -40,6 +41,7 @@ func NewTunDevice(address wgaddr.Address, port int, key string, mtu int, iceBind mtu: mtu, iceBind: iceBind, tunAdapter: tunAdapter, + disableDNS: disableDNS, } } @@ -49,6 +51,13 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string routesString := routesToString(routes) searchDomainsToString := searchDomainsToString(searchDomains) + // Skip DNS configuration when DisableDNS is enabled + if t.disableDNS { + log.Info("DNS is disabled, skipping DNS and search domain configuration") + dns = "" + searchDomainsToString = "" + } + fd, err := t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, dns, searchDomainsToString, routesString) if err != nil { log.Errorf("failed to create Android interface: %s", err) diff --git a/client/iface/iface.go b/client/iface/iface.go index 7d609f4cd..006dfe4e7 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -43,6 +43,7 @@ type WGIFaceOpts struct { MobileArgs *device.MobileIFaceArguments TransportNet transport.Net FilterFn bind.FilterFn + DisableDNS bool } // WGIface represents an interface instance diff --git a/client/iface/iface_new_android.go b/client/iface/iface_new_android.go index 35046b887..c8babea32 100644 --- a/client/iface/iface_new_android.go +++ b/client/iface/iface_new_android.go @@ -18,7 +18,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) { wgIFace := &WGIface{ userspaceBind: true, - tun: device.NewTunDevice(wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunAdapter), + tun: device.NewTunDevice(wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunAdapter, opts.DisableDNS), wgProxyFactory: wgproxy.NewUSPFactory(iceBind), } return wgIFace, nil diff --git a/client/internal/config.go b/client/internal/config.go index 45a7620e1..37ee1e1bf 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -223,6 +223,8 @@ func createNewConfig(input ConfigInput) (*Config, error) { config := &Config{ // defaults to false only for new (post 0.26) configurations ServerSSHAllowed: util.False(), + // default to disabling server routes on Android for security + DisableServerRoutes: runtime.GOOS == "android", } if _, err := config.apply(input); err != nil { @@ -416,9 +418,15 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { config.ServerSSHAllowed = input.ServerSSHAllowed updated = true } else if config.ServerSSHAllowed == nil { - // enables SSH for configs from old versions to preserve backwards compatibility - log.Infof("falling back to enabled SSH server for pre-existing configuration") - config.ServerSSHAllowed = util.True() + if runtime.GOOS == "android" { + // default to disabled SSH on Android for security + log.Infof("setting SSH server to false by default on Android") + config.ServerSSHAllowed = util.False() + } else { + // enables SSH for configs from old versions to preserve backwards compatibility + log.Infof("falling back to enabled SSH server for pre-existing configuration") + config.ServerSSHAllowed = util.True() + } updated = true } diff --git a/client/internal/engine.go b/client/internal/engine.go index 253ecb2a6..4ea6fbd94 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1527,6 +1527,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) { MTU: iface.DefaultMTU, TransportNet: transportNet, FilterFn: e.addrViaRoutes, + DisableDNS: e.config.DisableDNS, } switch runtime.GOOS { From 23b5d45b68c4caeb8bb493f58aed08aad665aa4d Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:56:48 +0200 Subject: [PATCH 101/213] [client] Fix port range squashing (#4007) --- client/internal/acl/manager.go | 8 +- client/internal/acl/manager_test.go | 428 ++++++++++++++++++++++++++++ 2 files changed, 434 insertions(+), 2 deletions(-) diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index c8bc9123b..32dc7fbb8 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -398,11 +398,15 @@ func (d *DefaultManager) squashAcceptRules( // // We zeroed this to notify squash function that this protocol can't be squashed. addRuleToCalculationMap := func(i int, r *mgmProto.FirewallRule, protocols map[mgmProto.RuleProtocol]*protoMatch) { - drop := r.Action == mgmProto.RuleAction_DROP || r.Port != "" - if drop { + hasPortRestrictions := r.Action == mgmProto.RuleAction_DROP || + r.Port != "" || !portInfoEmpty(r.PortInfo) + + if hasPortRestrictions { + // Don't squash rules with port restrictions protocols[r.Protocol] = &protoMatch{ips: map[string]int{}} return } + if _, ok := protocols[r.Protocol]; !ok { protocols[r.Protocol] = &protoMatch{ ips: map[string]int{}, diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 16620033e..b378de8c8 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -330,6 +330,434 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) { assert.Equal(t, len(networkMap.FirewallRules), len(rules)) } +func TestDefaultManagerSquashRulesWithPortRestrictions(t *testing.T) { + tests := []struct { + name string + rules []*mgmProto.FirewallRule + expectedCount int + description string + }{ + { + name: "should not squash rules with port ranges", + rules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 8080, + End: 8090, + }, + }, + }, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 8080, + End: 8090, + }, + }, + }, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 8080, + End: 8090, + }, + }, + }, + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 8080, + End: 8090, + }, + }, + }, + }, + }, + expectedCount: 4, + description: "Rules with port ranges should not be squashed even if they cover all peers", + }, + { + name: "should not squash rules with specific ports", + rules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 80, + }, + }, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 80, + }, + }, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 80, + }, + }, + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 80, + }, + }, + }, + }, + expectedCount: 4, + description: "Rules with specific ports should not be squashed even if they cover all peers", + }, + { + name: "should not squash rules with legacy port field", + rules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + }, + expectedCount: 4, + description: "Rules with legacy port field should not be squashed", + }, + { + name: "should not squash rules with DROP action", + rules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_DROP, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_DROP, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_DROP, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_DROP, + Protocol: mgmProto.RuleProtocol_TCP, + }, + }, + expectedCount: 4, + description: "Rules with DROP action should not be squashed", + }, + { + name: "should squash rules without port restrictions", + rules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + }, + expectedCount: 1, + description: "Rules without port restrictions should be squashed into a single 0.0.0.0 rule", + }, + { + name: "mixed rules should not squash protocol with port restrictions", + rules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + PortInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 80, + }, + }, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + }, + }, + expectedCount: 4, + description: "TCP should not be squashed because one rule has port restrictions", + }, + { + name: "should squash UDP but not TCP when TCP has port restrictions", + rules: []*mgmProto.FirewallRule{ + // TCP rules with port restrictions - should NOT be squashed + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_TCP, + Port: "443", + }, + // UDP rules without port restrictions - SHOULD be squashed + { + PeerIP: "10.93.0.1", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_UDP, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_UDP, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_UDP, + }, + { + PeerIP: "10.93.0.4", + Direction: mgmProto.RuleDirection_IN, + Action: mgmProto.RuleAction_ACCEPT, + Protocol: mgmProto.RuleProtocol_UDP, + }, + }, + expectedCount: 5, // 4 TCP rules + 1 squashed UDP rule (0.0.0.0) + description: "UDP should be squashed to 0.0.0.0 rule, but TCP should remain as individual rules due to port restrictions", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + networkMap := &mgmProto.NetworkMap{ + RemotePeers: []*mgmProto.RemotePeerConfig{ + {AllowedIps: []string{"10.93.0.1"}}, + {AllowedIps: []string{"10.93.0.2"}}, + {AllowedIps: []string{"10.93.0.3"}}, + {AllowedIps: []string{"10.93.0.4"}}, + }, + FirewallRules: tt.rules, + } + + manager := &DefaultManager{} + rules, _ := manager.squashAcceptRules(networkMap) + + assert.Equal(t, tt.expectedCount, len(rules), tt.description) + + // For squashed rules, verify we get the expected 0.0.0.0 rule + if tt.expectedCount == 1 { + assert.Equal(t, "0.0.0.0", rules[0].PeerIP) + assert.Equal(t, mgmProto.RuleDirection_IN, rules[0].Direction) + assert.Equal(t, mgmProto.RuleAction_ACCEPT, rules[0].Action) + } + }) + } +} + +func TestPortInfoEmpty(t *testing.T) { + tests := []struct { + name string + portInfo *mgmProto.PortInfo + expected bool + }{ + { + name: "nil PortInfo should be empty", + portInfo: nil, + expected: true, + }, + { + name: "PortInfo with zero port should be empty", + portInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 0, + }, + }, + expected: true, + }, + { + name: "PortInfo with valid port should not be empty", + portInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Port{ + Port: 80, + }, + }, + expected: false, + }, + { + name: "PortInfo with nil range should be empty", + portInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: nil, + }, + }, + expected: true, + }, + { + name: "PortInfo with zero start range should be empty", + portInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 0, + End: 100, + }, + }, + }, + expected: true, + }, + { + name: "PortInfo with zero end range should be empty", + portInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 80, + End: 0, + }, + }, + }, + expected: true, + }, + { + name: "PortInfo with valid range should not be empty", + portInfo: &mgmProto.PortInfo{ + PortSelection: &mgmProto.PortInfo_Range_{ + Range: &mgmProto.PortInfo_Range{ + Start: 8080, + End: 8090, + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := portInfoEmpty(tt.portInfo) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestDefaultManagerEnableSSHRules(t *testing.T) { networkMap := &mgmProto.NetworkMap{ PeerConfig: &mgmProto.PeerConfig{ From e9016aeceaa85be61f317c9bc3adfec7ff4558d0 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 19 Jun 2025 13:07:06 +0300 Subject: [PATCH 102/213] [management] Add backward compatibility for older clients without firewall rules port range support (#4003) Adds backward compatibility for clients with versions prior to v0.48.0 that do not support port range firewall rules. - Skips generation of firewall rules with multi-port ranges for older clients - Preserves support for single-port ranges by treating them as individual port rules, ensuring compatibility with older clients --- management/server/peer.go | 4 +- management/server/policy_test.go | 139 ++++++++++++++----- management/server/posture/nb_version.go | 30 ++-- management/server/posture/nb_version_test.go | 65 +++++++++ management/server/types/account.go | 70 +++++++--- management/server/types/firewall_rule.go | 1 - 6 files changed, 246 insertions(+), 63 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index f2469e09b..1a1289721 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -92,7 +92,7 @@ func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, acc // fetch all the peers that have access to the user's peers for _, peer := range peers { - aclPeers, _ := account.GetPeerConnectionResources(ctx, peer.ID, approvedPeersMap) + aclPeers, _ := account.GetPeerConnectionResources(ctx, peer, approvedPeersMap) for _, p := range aclPeers { peersMap[p.ID] = p } @@ -1149,7 +1149,7 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun } for _, p := range userPeers { - aclPeers, _ := account.GetPeerConnectionResources(ctx, p.ID, approvedPeersMap) + aclPeers, _ := account.GetPeerConnectionResources(ctx, p, approvedPeersMap) for _, aclPeer := range aclPeers { if aclPeer.ID == peer.ID { return peer, nil diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 679ec3b86..4352f3cff 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -27,6 +27,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { ID: "peerB", IP: net.ParseIP("100.65.80.39"), Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{WtVersion: "0.48.0"}, }, "peerC": { ID: "peerC", @@ -63,6 +64,12 @@ func TestAccount_getPeersByPolicy(t *testing.T) { IP: net.ParseIP("100.65.31.2"), Status: &nbpeer.PeerStatus{}, }, + "peerK": { + ID: "peerK", + IP: net.ParseIP("100.32.80.1"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{WtVersion: "0.30.0"}, + }, }, Groups: map[string]*types.Group{ "GroupAll": { @@ -111,6 +118,13 @@ func TestAccount_getPeersByPolicy(t *testing.T) { "peerI", }, }, + "GroupWorkflow": { + ID: "GroupWorkflow", + Name: "workflow", + Peers: []string{ + "peerK", + }, + }, }, Policies: []*types.Policy{ { @@ -189,6 +203,39 @@ func TestAccount_getPeersByPolicy(t *testing.T) { }, }, }, + { + ID: "RuleWorkflow", + Name: "Workflow", + Description: "No description", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "RuleWorkflow", + Name: "Workflow", + Description: "No description", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolTCP, + Action: types.PolicyTrafficActionAccept, + PortRanges: []types.RulePortRange{ + { + Start: 8088, + End: 8088, + }, + { + Start: 9090, + End: 9095, + }, + }, + Sources: []string{ + "GroupWorkflow", + }, + Destinations: []string{ + "GroupDMZ", + }, + }, + }, + }, }, } @@ -199,14 +246,14 @@ func TestAccount_getPeersByPolicy(t *testing.T) { t.Run("check that all peers get map", func(t *testing.T) { for _, p := range account.Peers { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), p.ID, validatedPeers) - assert.GreaterOrEqual(t, len(peers), 2, "minimum number peers should present") - assert.GreaterOrEqual(t, len(firewallRules), 2, "minimum number of firewall rules should present") + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), p, validatedPeers) + assert.GreaterOrEqual(t, len(peers), 1, "minimum number peers should present") + assert.GreaterOrEqual(t, len(firewallRules), 1, "minimum number of firewall rules should present") } }) t.Run("check first peer map details", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", validatedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], validatedPeers) assert.Len(t, peers, 8) assert.Contains(t, peers, account.Peers["peerA"]) assert.Contains(t, peers, account.Peers["peerC"]) @@ -364,6 +411,32 @@ func TestAccount_getPeersByPolicy(t *testing.T) { assert.True(t, contains, "rule not found in expected rules %#v", rule) } }) + + t.Run("check port ranges support for older peers", func(t *testing.T) { + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerK"], validatedPeers) + assert.Len(t, peers, 1) + assert.Contains(t, peers, account.Peers["peerI"]) + + expectedFirewallRules := []*types.FirewallRule{ + { + PeerIP: "100.65.31.2", + Direction: types.FirewallRuleDirectionIN, + Action: "accept", + Protocol: "tcp", + Port: "8088", + PolicyID: "RuleWorkflow", + }, + { + PeerIP: "100.65.31.2", + Direction: types.FirewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "8088", + PolicyID: "RuleWorkflow", + }, + } + assert.ElementsMatch(t, firewallRules, expectedFirewallRules) + }) } func TestAccount_getPeersByPolicyDirect(t *testing.T) { @@ -466,10 +539,10 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { } t.Run("check first peer map", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Contains(t, peers, account.Peers["peerC"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.254.139", Direction: types.FirewallRuleDirectionIN, @@ -487,19 +560,19 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) t.Run("check second peer map", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerC", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Contains(t, peers, account.Peers["peerB"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.80.39", Direction: types.FirewallRuleDirectionIN, @@ -517,21 +590,21 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) account.Policies[1].Rules[0].Bidirectional = false t.Run("check first peer map directional only", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Contains(t, peers, account.Peers["peerC"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.254.139", Direction: types.FirewallRuleDirectionOUT, @@ -541,19 +614,19 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) t.Run("check second peer map directional only", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerC", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Contains(t, peers, account.Peers["peerB"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.80.39", Direction: types.FirewallRuleDirectionIN, @@ -563,11 +636,11 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) } @@ -748,7 +821,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { t.Run("verify peer's network map with default group peer list", func(t *testing.T) { // peerB doesn't fulfill the NB posture check but is included in the destination group Swarm, // will establish a connection with all source peers satisfying the NB posture check. - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -758,7 +831,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerC satisfy the NB posture check, should establish connection to all destination group peer's // We expect a single permissive firewall rule which all outgoing connections - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerC", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) assert.Len(t, firewallRules, 1) expectedFirewallRules := []*types.FirewallRule{ @@ -775,7 +848,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerE doesn't fulfill the NB posture check and exists in only destination group Swarm, // all source group peers satisfying the NB posture check should establish connection - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerE", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -785,7 +858,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerI doesn't fulfill the OS version posture check and exists in only destination group Swarm, // all source group peers satisfying the NB posture check should establish connection - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerI", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -800,19 +873,19 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerB doesn't satisfy the NB posture check, and doesn't exist in destination group peer's // no connection should be established to any peer of destination group - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Len(t, peers, 0) assert.Len(t, firewallRules, 0) // peerI doesn't satisfy the OS version posture check, and doesn't exist in destination group peer's // no connection should be established to any peer of destination group - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerI", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers) assert.Len(t, peers, 0) assert.Len(t, firewallRules, 0) // peerC satisfy the NB posture check, should establish connection to all destination group peer's // We expect a single permissive firewall rule which all outgoing connections - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerC", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers)) @@ -827,14 +900,14 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerE doesn't fulfill the NB posture check and exists in only destination group Swarm, // all source group peers satisfying the NB posture check should establish connection - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerE", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers) assert.Len(t, peers, 3) assert.Len(t, firewallRules, 3) assert.Contains(t, peers, account.Peers["peerA"]) assert.Contains(t, peers, account.Peers["peerC"]) assert.Contains(t, peers, account.Peers["peerD"]) - peers, firewallRules = account.GetPeerConnectionResources(context.Background(), "peerA", approvedPeers) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerA"], approvedPeers) assert.Len(t, peers, 5) // assert peers from Group Swarm assert.Contains(t, peers, account.Peers["peerD"]) diff --git a/management/server/posture/nb_version.go b/management/server/posture/nb_version.go index e98e8e795..33bf01ad1 100644 --- a/management/server/posture/nb_version.go +++ b/management/server/posture/nb_version.go @@ -24,20 +24,12 @@ func sanitizeVersion(version string) string { } func (n *NBVersionCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, error) { - peerVersion := sanitizeVersion(peer.Meta.WtVersion) - minVersion := sanitizeVersion(n.MinVersion) - - peerNBVersion, err := version.NewVersion(peerVersion) + meetsMin, err := MeetsMinVersion(n.MinVersion, peer.Meta.WtVersion) if err != nil { return false, err } - constraints, err := version.NewConstraint(">= " + minVersion) - if err != nil { - return false, err - } - - if constraints.Check(peerNBVersion) { + if meetsMin { return true, nil } @@ -60,3 +52,21 @@ func (n *NBVersionCheck) Validate() error { } return nil } + +// MeetsMinVersion checks if the peer's version meets or exceeds the minimum required version +func MeetsMinVersion(minVer, peerVer string) (bool, error) { + peerVer = sanitizeVersion(peerVer) + minVer = sanitizeVersion(minVer) + + peerNBVer, err := version.NewVersion(peerVer) + if err != nil { + return false, err + } + + constraints, err := version.NewConstraint(">= " + minVer) + if err != nil { + return false, err + } + + return constraints.Check(peerNBVer), nil +} diff --git a/management/server/posture/nb_version_test.go b/management/server/posture/nb_version_test.go index 1bf485453..d3478afc2 100644 --- a/management/server/posture/nb_version_test.go +++ b/management/server/posture/nb_version_test.go @@ -139,3 +139,68 @@ func TestNBVersionCheck_Validate(t *testing.T) { }) } } + +func TestMeetsMinVersion(t *testing.T) { + tests := []struct { + name string + minVer string + peerVer string + want bool + wantErr bool + }{ + { + name: "Peer version greater than min version", + minVer: "0.26.0", + peerVer: "0.60.1", + want: true, + wantErr: false, + }, + { + name: "Peer version equals min version", + minVer: "1.0.0", + peerVer: "1.0.0", + want: true, + wantErr: false, + }, + { + name: "Peer version less than min version", + minVer: "1.0.0", + peerVer: "0.9.9", + want: false, + wantErr: false, + }, + { + name: "Peer version with pre-release tag greater than min version", + minVer: "1.0.0", + peerVer: "1.0.1-alpha", + want: true, + wantErr: false, + }, + { + name: "Invalid peer version format", + minVer: "1.0.0", + peerVer: "dev", + want: false, + wantErr: true, + }, + { + name: "Invalid min version format", + minVer: "invalid.version", + peerVer: "1.0.0", + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MeetsMinVersion(tt.minVer, tt.peerVer) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/management/server/types/account.go b/management/server/types/account.go index da230f0b2..090ba76e4 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -36,6 +36,9 @@ const ( PublicCategory = "public" PrivateCategory = "private" UnknownCategory = "unknown" + + // firewallRuleMinPortRangesVer defines the minimum peer version that supports port range rules. + firewallRuleMinPortRangesVer = "0.48.0" ) type LookupMap map[string]struct{} @@ -248,7 +251,7 @@ func (a *Account) GetPeerNetworkMap( } } - aclPeers, firewallRules := a.GetPeerConnectionResources(ctx, peerID, validatedPeersMap) + aclPeers, firewallRules := a.GetPeerConnectionResources(ctx, peer, validatedPeersMap) // exclude expired peers var peersToConnect []*nbpeer.Peer var expiredPeers []*nbpeer.Peer @@ -961,8 +964,9 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) map // GetPeerConnectionResources for a given peer // // This function returns the list of peers and firewall rules that are applicable to a given peer. -func (a *Account) GetPeerConnectionResources(ctx context.Context, peerID string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, []*FirewallRule) { - generateResources, getAccumulatedResources := a.connResourcesGenerator(ctx) +func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.Peer, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, []*FirewallRule) { + generateResources, getAccumulatedResources := a.connResourcesGenerator(ctx, peer) + for _, policy := range a.Policies { if !policy.Enabled { continue @@ -973,8 +977,8 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peerID string, continue } - sourcePeers, peerInSources := a.getAllPeersFromGroups(ctx, rule.Sources, peerID, policy.SourcePostureChecks, validatedPeersMap) - destinationPeers, peerInDestinations := a.getAllPeersFromGroups(ctx, rule.Destinations, peerID, nil, validatedPeersMap) + sourcePeers, peerInSources := a.getAllPeersFromGroups(ctx, rule.Sources, peer.ID, policy.SourcePostureChecks, validatedPeersMap) + destinationPeers, peerInDestinations := a.getAllPeersFromGroups(ctx, rule.Destinations, peer.ID, nil, validatedPeersMap) if rule.Bidirectional { if peerInSources { @@ -1003,7 +1007,7 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peerID string, // The generator function is used to generate the list of peers and firewall rules that are applicable to a given peer. // It safe to call the generator function multiple times for same peer and different rules no duplicates will be // generated. The accumulator function returns the result of all the generator calls. -func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) { +func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer.Peer) (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) { rulesExists := make(map[string]struct{}) peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) @@ -1051,17 +1055,7 @@ func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, continue } - for _, port := range rule.Ports { - pr := fr // clone rule and add set new port - pr.Port = port - rules = append(rules, &pr) - } - - for _, portRange := range rule.PortRanges { - pr := fr - pr.PortRange = portRange - rules = append(rules, &pr) - } + rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...) } }, func() ([]*nbpeer.Peer, []*FirewallRule) { return peers, rules @@ -1590,3 +1584,45 @@ func (a *Account) AddAllGroup() error { } return nil } + +// expandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules +func expandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { + var expanded []*FirewallRule + + if len(rule.Ports) > 0 { + for _, port := range rule.Ports { + fr := base + fr.Port = port + expanded = append(expanded, &fr) + } + return expanded + } + + supportPortRanges := peerSupportsPortRanges(peer.Meta.WtVersion) + for _, portRange := range rule.PortRanges { + fr := base + + if supportPortRanges { + fr.PortRange = portRange + } else { + // Peer doesn't support port ranges, only allow single-port ranges + if portRange.Start != portRange.End { + continue + } + fr.Port = strconv.FormatUint(uint64(portRange.Start), 10) + } + expanded = append(expanded, &fr) + } + + return expanded +} + +// peerSupportsPortRanges checks if the peer version supports port ranges. +func peerSupportsPortRanges(peerVer string) bool { + if strings.Contains(peerVer, "dev") { + return true + } + + meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer) + return err == nil && meetMinVer +} diff --git a/management/server/types/firewall_rule.go b/management/server/types/firewall_rule.go index ef54abea2..19222a607 100644 --- a/management/server/types/firewall_rule.go +++ b/management/server/types/firewall_rule.go @@ -76,7 +76,6 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule rules = append(rules, generateRulesWithPortRanges(baseRule, rule, rulesExists)...) } else { rules = append(rules, generateRulesWithPorts(ctx, baseRule, rule, rulesExists)...) - } // TODO: generate IPv6 rules for dynamic routes From b45284f08623b57cf987e7a416a5c91f327ae3c1 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:46:56 +0200 Subject: [PATCH 103/213] [management] export ephemeral peer flag on api (#4004) --- management/server/http/api/openapi.yml | 5 +++++ management/server/http/api/types.gen.go | 6 ++++++ management/server/http/handlers/peers/peers_handler.go | 1 + 3 files changed, 12 insertions(+) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 58134d375..1c5ca9b04 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -426,6 +426,10 @@ components: items: type: string example: "stage-host-1" + ephemeral: + description: Indicates whether the peer is ephemeral or not + type: boolean + example: false required: - city_name - connected @@ -450,6 +454,7 @@ components: - approval_required - serial_number - extra_dns_labels + - ephemeral AccessiblePeer: allOf: - $ref: '#/components/schemas/PeerMinimum' diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 0a09d7ca2..d27fd2a57 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -1016,6 +1016,9 @@ type Peer struct { // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // Ephemeral Indicates whether the peer is ephemeral or not + Ephemeral bool `json:"ephemeral"` + // ExtraDnsLabels Extra DNS labels added to the peer ExtraDnsLabels []string `json:"extra_dns_labels"` @@ -1097,6 +1100,9 @@ type PeerBatch struct { // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // Ephemeral Indicates whether the peer is ephemeral or not + Ephemeral bool `json:"ephemeral"` + // ExtraDnsLabels Extra DNS labels added to the peer ExtraDnsLabels []string `json:"extra_dns_labels"` diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 58ea06ea3..8c20ed65f 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -365,6 +365,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD CityName: peer.Location.CityName, SerialNumber: peer.Meta.SystemSerialNumber, InactivityExpirationEnabled: peer.InactivityExpirationEnabled, + Ephemeral: peer.Ephemeral, } } From 83457f8b99a8d5382e069eed682173e213b2b218 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:13:24 +0200 Subject: [PATCH 104/213] [management] add transaction for integrated validator groups update and primary account update (#4014) --- management/server/account.go | 69 +++++++++++++---------- management/server/integrated_validator.go | 28 ++++----- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index daeaf6e55..b376f6f5e 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1853,40 +1853,49 @@ func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.C } func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) { - account, err := am.Store.GetAccount(ctx, accountId) + var account *types.Account + err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + var err error + account, err = transaction.GetAccount(ctx, accountId) + if err != nil { + return err + } + + if account.IsDomainPrimaryAccount { + return nil + } + + existingPrimaryAccountID, err := transaction.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, account.Domain) + + // error is not a not found error + if handleNotFound(err) != nil { + return err + } + + // a primary account already exists for this private domain + if err == nil { + log.WithContext(ctx).WithFields(log.Fields{ + "accountId": accountId, + "existingAccountId": existingPrimaryAccountID, + }).Errorf("cannot update account to primary, another account already exists as primary for the same domain") + return status.Errorf(status.Internal, "cannot update account to primary") + } + + account.IsDomainPrimaryAccount = true + + if err := transaction.SaveAccount(ctx, account); err != nil { + log.WithContext(ctx).WithFields(log.Fields{ + "accountId": accountId, + }).Errorf("failed to update account to primary: %v", err) + return status.Errorf(status.Internal, "failed to update account to primary") + } + + return nil + }) if err != nil { return nil, err } - if account.IsDomainPrimaryAccount { - return account, nil - } - - existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, account.Domain) - - // error is not a not found error - if handleNotFound(err) != nil { - return nil, err - } - - // a primary account already exists for this private domain - if err == nil { - log.WithContext(ctx).WithFields(log.Fields{ - "accountId": accountId, - "existingAccountId": existingPrimaryAccountID, - }).Errorf("cannot update account to primary, another account already exists as primary for the same domain") - return nil, status.Errorf(status.Internal, "cannot update account to primary") - } - - account.IsDomainPrimaryAccount = true - - if err := am.Store.SaveAccount(ctx, account); err != nil { - log.WithContext(ctx).WithFields(log.Fields{ - "accountId": accountId, - }).Errorf("failed to update account to primary: %v", err) - return nil, status.Errorf(status.Internal, "failed to update account to primary") - } - return account, nil } diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index ef77bf10c..edb89466c 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -37,21 +37,23 @@ func (am *DefaultAccountManager) UpdateIntegratedValidatorGroups(ctx context.Con unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - a, err := am.Store.GetAccountByUser(ctx, userID) - if err != nil { - return err - } + return am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + a, err := transaction.GetAccountByUser(ctx, userID) + if err != nil { + return err + } - var extra *types.ExtraSettings + var extra *types.ExtraSettings - if a.Settings.Extra != nil { - extra = a.Settings.Extra - } else { - extra = &types.ExtraSettings{} - a.Settings.Extra = extra - } - extra.IntegratedValidatorGroups = groups - return am.Store.SaveAccount(ctx, a) + if a.Settings.Extra != nil { + extra = a.Settings.Extra + } else { + extra = &types.ExtraSettings{} + a.Settings.Extra = extra + } + extra.IntegratedValidatorGroups = groups + return transaction.SaveAccount(ctx, a) + }) } func (am *DefaultAccountManager) GroupValidation(ctx context.Context, accountID string, groupIDs []string) (bool, error) { From 2a51609436f78544dd24fd416072325ce665bf15 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:07:19 +0200 Subject: [PATCH 105/213] [client] Handle lazy routing peers that are part of HA groups (#3943) * Activate new lazy routing peers if the HA group is active * Prevent lazy peers going to idle if HA group members are active (#3948) --- client/internal/conn_mgr.go | 2 +- .../lazyconn/inactivity/inactivity.go | 5 + client/internal/lazyconn/manager/manager.go | 149 +++++++++++++++--- client/internal/peer/conn.go | 6 +- 4 files changed, 137 insertions(+), 25 deletions(-) diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index aac312dc3..c630d3052 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -175,7 +175,7 @@ func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Co PeerConnID: conn.ConnID(), Log: conn.Log, } - excluded, err := e.lazyConnMgr.AddPeer(lazyPeerCfg) + excluded, err := e.lazyConnMgr.AddPeer(e.lazyCtx, lazyPeerCfg) if err != nil { conn.Log.Errorf("failed to add peer to lazyconn manager: %v", err) if err := conn.Open(ctx); err != nil { diff --git a/client/internal/lazyconn/inactivity/inactivity.go b/client/internal/lazyconn/inactivity/inactivity.go index a30c1846d..9b7c8511b 100644 --- a/client/internal/lazyconn/inactivity/inactivity.go +++ b/client/internal/lazyconn/inactivity/inactivity.go @@ -68,3 +68,8 @@ func (i *Monitor) PauseTimer() { func (i *Monitor) ResetTimer() { i.timer.Reset(i.inactivityThreshold) } + +func (i *Monitor) ResetMonitor(ctx context.Context, timeoutChan chan peer.ConnID) { + i.Stop() + go i.Start(ctx, timeoutChan) +} diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go index 718bdbddf..74ede50a7 100644 --- a/client/internal/lazyconn/manager/manager.go +++ b/client/internal/lazyconn/manager/manager.go @@ -58,7 +58,7 @@ type Manager struct { // Route HA group management peerToHAGroups map[string][]route.HAUniqueID // peer ID -> HA groups they belong to haGroupToPeers map[route.HAUniqueID][]string // HA group -> peer IDs in the group - routesMu sync.RWMutex // protects route mappings + routesMu sync.RWMutex onInactive chan peerid.ConnID } @@ -146,7 +146,7 @@ func (m *Manager) Start(ctx context.Context) { case peerConnID := <-m.activityManager.OnActivityChan: m.onPeerActivity(ctx, peerConnID) case peerConnID := <-m.onInactive: - m.onPeerInactivityTimedOut(peerConnID) + m.onPeerInactivityTimedOut(ctx, peerConnID) } } } @@ -197,7 +197,7 @@ func (m *Manager) ExcludePeer(ctx context.Context, peerConfigs []lazyconn.PeerCo return added } -func (m *Manager) AddPeer(peerCfg lazyconn.PeerConfig) (bool, error) { +func (m *Manager) AddPeer(ctx context.Context, peerCfg lazyconn.PeerConfig) (bool, error) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -225,6 +225,13 @@ func (m *Manager) AddPeer(peerCfg lazyconn.PeerConfig) (bool, error) { peerCfg: &peerCfg, expectedWatcher: watcherActivity, } + + // Check if this peer should be activated because its HA group peers are active + if group, ok := m.shouldActivateNewPeer(peerCfg.PublicKey); ok { + peerCfg.Log.Debugf("peer belongs to active HA group %s, will activate immediately", group) + m.activateNewPeerInActiveGroup(ctx, peerCfg) + } + return false, nil } @@ -315,36 +322,38 @@ func (m *Manager) activateSinglePeer(ctx context.Context, cfg *lazyconn.PeerConf // activateHAGroupPeers activates all peers in HA groups that the given peer belongs to func (m *Manager) activateHAGroupPeers(ctx context.Context, triggerPeerID string) { + var peersToActivate []string + m.routesMu.RLock() haGroups := m.peerToHAGroups[triggerPeerID] - m.routesMu.RUnlock() if len(haGroups) == 0 { + m.routesMu.RUnlock() log.Debugf("peer %s is not part of any HA groups", triggerPeerID) return } - activatedCount := 0 for _, haGroup := range haGroups { - m.routesMu.RLock() peers := m.haGroupToPeers[haGroup] - m.routesMu.RUnlock() - for _, peerID := range peers { - if peerID == triggerPeerID { - continue + if peerID != triggerPeerID { + peersToActivate = append(peersToActivate, peerID) } + } + } + m.routesMu.RUnlock() - cfg, mp := m.getPeerForActivation(peerID) - if cfg == nil { - continue - } + activatedCount := 0 + for _, peerID := range peersToActivate { + cfg, mp := m.getPeerForActivation(peerID) + if cfg == nil { + continue + } - if m.activateSinglePeer(ctx, cfg, mp) { - activatedCount++ - cfg.Log.Infof("activated peer as part of HA group %s (triggered by %s)", haGroup, triggerPeerID) - m.peerStore.PeerConnOpen(m.engineCtx, cfg.PublicKey) - } + if m.activateSinglePeer(ctx, cfg, mp) { + activatedCount++ + cfg.Log.Infof("activated peer as part of HA group (triggered by %s)", triggerPeerID) + m.peerStore.PeerConnOpen(m.engineCtx, cfg.PublicKey) } } @@ -354,6 +363,51 @@ func (m *Manager) activateHAGroupPeers(ctx context.Context, triggerPeerID string } } +// shouldActivateNewPeer checks if a newly added peer should be activated +// because other peers in its HA groups are already active +func (m *Manager) shouldActivateNewPeer(peerID string) (route.HAUniqueID, bool) { + m.routesMu.RLock() + defer m.routesMu.RUnlock() + + haGroups := m.peerToHAGroups[peerID] + if len(haGroups) == 0 { + return "", false + } + + for _, haGroup := range haGroups { + peers := m.haGroupToPeers[haGroup] + for _, groupPeerID := range peers { + if groupPeerID == peerID { + continue + } + + cfg, ok := m.managedPeers[groupPeerID] + if !ok { + continue + } + if mp, ok := m.managedPeersByConnID[cfg.PeerConnID]; ok && mp.expectedWatcher == watcherInactivity { + return haGroup, true + } + } + } + return "", false +} + +// activateNewPeerInActiveGroup activates a newly added peer that should be active due to HA group +func (m *Manager) activateNewPeerInActiveGroup(ctx context.Context, peerCfg lazyconn.PeerConfig) { + mp, ok := m.managedPeersByConnID[peerCfg.PeerConnID] + if !ok { + return + } + + if !m.activateSinglePeer(ctx, &peerCfg, mp) { + return + } + + peerCfg.Log.Infof("activated newly added peer due to active HA group peers") + m.peerStore.PeerConnOpen(m.engineCtx, peerCfg.PublicKey) +} + func (m *Manager) addActivePeer(ctx context.Context, peerCfg lazyconn.PeerConfig) error { if _, ok := m.managedPeers[peerCfg.PublicKey]; ok { peerCfg.Log.Warnf("peer already managed") @@ -415,6 +469,48 @@ func (m *Manager) close() { log.Infof("lazy connection manager closed") } +// shouldDeferIdleForHA checks if peer should stay connected due to HA group requirements +func (m *Manager) shouldDeferIdleForHA(peerID string) bool { + m.routesMu.RLock() + defer m.routesMu.RUnlock() + + haGroups := m.peerToHAGroups[peerID] + if len(haGroups) == 0 { + return false + } + + for _, haGroup := range haGroups { + groupPeers := m.haGroupToPeers[haGroup] + + for _, groupPeerID := range groupPeers { + if groupPeerID == peerID { + continue + } + + cfg, ok := m.managedPeers[groupPeerID] + if !ok { + continue + } + + groupMp, ok := m.managedPeersByConnID[cfg.PeerConnID] + if !ok { + continue + } + + if groupMp.expectedWatcher != watcherInactivity { + continue + } + + // Other member is still connected, defer idle + if peer, ok := m.peerStore.PeerConn(groupPeerID); ok && peer.IsConnected() { + return true + } + } + } + + return false +} + func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -441,7 +537,7 @@ func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) m.peerStore.PeerConnOpen(m.engineCtx, mp.peerCfg.PublicKey) } -func (m *Manager) onPeerInactivityTimedOut(peerConnID peerid.ConnID) { +func (m *Manager) onPeerInactivityTimedOut(ctx context.Context, peerConnID peerid.ConnID) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -456,6 +552,17 @@ func (m *Manager) onPeerInactivityTimedOut(peerConnID peerid.ConnID) { return } + if m.shouldDeferIdleForHA(mp.peerCfg.PublicKey) { + iw, ok := m.inactivityMonitors[peerConnID] + if ok { + mp.peerCfg.Log.Debugf("resetting inactivity timer due to HA group requirements") + iw.ResetMonitor(ctx, m.onInactive) + } else { + mp.peerCfg.Log.Errorf("inactivity monitor not found for HA defer reset") + } + return + } + mp.peerCfg.Log.Infof("connection timed out") // this is blocking operation, potentially can be optimized @@ -489,7 +596,7 @@ func (m *Manager) onPeerConnected(peerConnID peerid.ConnID) { iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID] if !ok { - mp.peerCfg.Log.Errorf("inactivity monitor not found for peer") + mp.peerCfg.Log.Warnf("inactivity monitor not found for peer") return } diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index b33023873..c3f44cc7f 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -317,12 +317,12 @@ func (conn *Conn) WgConfig() WgConfig { return conn.config.WgConfig } -// IsConnected unit tests only -// refactor unit test to use status recorder use refactor status recorded to manage connection status in peer.Conn +// IsConnected returns true if the peer is connected func (conn *Conn) IsConnected() bool { conn.mu.Lock() defer conn.mu.Unlock() - return conn.currentConnPriority != conntype.None + + return conn.evalStatus() == StatusConnected } func (conn *Conn) GetKey() string { From 3581648071814f5812250efde6fd6794db932b26 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Sun, 22 Jun 2025 11:03:58 +0300 Subject: [PATCH 106/213] [client] Refactor showLoginURL to improve error handling and connection status checks (#4026) This PR refactors showLoginURL to improve error handling and connection status checks by delaying the login fetch until user interaction and closing the pop-up if already connected. - Moved s.login(false) call into the click handler to defer network I/O. - Added a conn.Status check after opening the URL to skip reconnection if already connected. - Enhanced error logs for missing verification URLs and service status failures. --- client/ui/client_ui.go | 44 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 00a535dd6..db53bc4eb 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -992,21 +992,6 @@ func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { // showLoginURL creates a borderless window styled like a pop-up in the top-right corner using s.wLoginURL. func (s *serviceClient) showLoginURL() { - resp, err := s.login(false) - if err != nil { - log.Errorf("failed to fetch login URL: %v", err) - return - } - verificationURL := resp.VerificationURIComplete - if verificationURL == "" { - verificationURL = resp.VerificationURI - } - - if verificationURL == "" { - log.Error("no verification URL provided in the login response") - return - } - resIcon := fyne.NewStaticResource("netbird.png", iconAbout) if s.wLoginURL == nil { @@ -1025,6 +1010,21 @@ func (s *serviceClient) showLoginURL() { return } + resp, err := s.login(false) + if err != nil { + log.Errorf("failed to fetch login URL: %v", err) + return + } + verificationURL := resp.VerificationURIComplete + if verificationURL == "" { + verificationURL = resp.VerificationURI + } + + if verificationURL == "" { + log.Error("no verification URL provided in the login response") + return + } + if err := openURL(verificationURL); err != nil { log.Errorf("failed to open login URL: %v", err) return @@ -1038,7 +1038,19 @@ func (s *serviceClient) showLoginURL() { } label.SetText("Re-authentication successful.\nReconnecting") - time.Sleep(300 * time.Millisecond) + status, err := conn.Status(s.ctx, &proto.StatusRequest{}) + if err != nil { + log.Errorf("get service status: %v", err) + return + } + + if status.Status == string(internal.StatusConnected) { + label.SetText("Already connected.\nClosing this window.") + time.Sleep(2 * time.Second) + s.wLoginURL.Close() + return + } + _, err = conn.Up(s.ctx, &proto.UpRequest{}) if err != nil { label.SetText("Reconnecting failed, please create \na debug bundle in the settings and contact support.") From 08e9b05d5199f913e5ffd105e3f99a2565983a96 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sun, 22 Jun 2025 10:33:04 +0200 Subject: [PATCH 107/213] [client] close windows when process needs to exit (#4027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes a bug by ensuring that the advanced settings and re-authentication windows are closed appropriately when the main GUI process exits. - Updated runSelfCommand calls throughout the UI to pass a context parameter. - Modified runSelfCommand’s signature and its internal command invocation to use exec.CommandContext for proper cancellation handling. --- client/ui/client_ui.go | 2 +- client/ui/event_handler.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index db53bc4eb..2b74c31d1 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -879,7 +879,7 @@ func (s *serviceClient) onUpdateAvailable() { func (s *serviceClient) onSessionExpire() { s.sendNotification = true if s.sendNotification { - s.eventHandler.runSelfCommand("login-url", "true") + s.eventHandler.runSelfCommand(s.ctx, "login-url", "true") s.sendNotification = false } } diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go index 5441f3481..39ea3867c 100644 --- a/client/ui/event_handler.go +++ b/client/ui/event_handler.go @@ -122,7 +122,7 @@ func (h *eventHandler) handleAdvancedSettingsClick() { go func() { defer h.client.mAdvancedSettings.Enable() defer h.client.getSrvConfig() - h.runSelfCommand("settings", "true") + h.runSelfCommand(h.client.ctx, "settings", "true") }() } @@ -130,7 +130,7 @@ func (h *eventHandler) handleCreateDebugBundleClick() { h.client.mCreateDebugBundle.Disable() go func() { defer h.client.mCreateDebugBundle.Enable() - h.runSelfCommand("debug", "true") + h.runSelfCommand(h.client.ctx, "debug", "true") }() } @@ -154,7 +154,7 @@ func (h *eventHandler) handleNetworksClick() { h.client.mNetworks.Disable() go func() { defer h.client.mNetworks.Enable() - h.runSelfCommand("networks", "true") + h.runSelfCommand(h.client.ctx, "networks", "true") }() } @@ -172,14 +172,14 @@ func (h *eventHandler) updateConfigWithErr() { } } -func (h *eventHandler) runSelfCommand(command, arg string) { +func (h *eventHandler) runSelfCommand(ctx context.Context, command, arg string) { proc, err := os.Executable() if err != nil { log.Errorf("error getting executable path: %v", err) return } - cmd := exec.Command(proc, + cmd := exec.CommandContext(ctx, proc, fmt.Sprintf("--%s=%s", command, arg), fmt.Sprintf("--daemon-addr=%s", h.client.addr), ) From 870e29db63e118ca8273cabc39107365611eef55 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sun, 22 Jun 2025 13:44:25 +0200 Subject: [PATCH 108/213] [misc] add additional metrics (#4028) * add additional metrics we are collecting active rosenpass, ssh from the client side we are also collecting active user peers and active users * remove duplicated --- management/server/metrics/selfhosted.go | 17 ++++++++++++++++- management/server/metrics/selfhosted_test.go | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index 9a3b22e51..4ce57b1da 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -184,7 +184,9 @@ func (w *Worker) generateProperties(ctx context.Context) properties { ephemeralPeersSKs int ephemeralPeersSKUsage int activePeersLastDay int + activeUserPeersLastDay int osPeers map[string]int + activeUsersLastDay map[string]struct{} userPeers int rules int rulesProtocol map[string]int @@ -203,6 +205,7 @@ func (w *Worker) generateProperties(ctx context.Context) properties { version string peerActiveVersions []string osUIClients map[string]int + rosenpassEnabled int ) start := time.Now() metricsProperties := make(properties) @@ -210,6 +213,7 @@ func (w *Worker) generateProperties(ctx context.Context) properties { osUIClients = make(map[string]int) rulesProtocol = make(map[string]int) rulesDirection = make(map[string]int) + activeUsersLastDay = make(map[string]struct{}) uptime = time.Since(w.startupTime).Seconds() connections := w.connManager.GetAllConnectedPeers() version = nbversion.NetbirdVersion() @@ -277,10 +281,14 @@ func (w *Worker) generateProperties(ctx context.Context) properties { for _, peer := range account.Peers { peers++ - if peer.SSHEnabled { + if peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed { peersSSHEnabled++ } + if peer.Meta.Flags.RosenpassEnabled { + rosenpassEnabled++ + } + if peer.UserID != "" { userPeers++ } @@ -299,6 +307,10 @@ func (w *Worker) generateProperties(ctx context.Context) properties { _, connected := connections[peer.ID] if connected || peer.Status.LastSeen.After(w.lastRun) { activePeersLastDay++ + if peer.UserID != "" { + activeUserPeersLastDay++ + activeUsersLastDay[peer.UserID] = struct{}{} + } osActiveKey := osKey + "_active" osActiveCount := osPeers[osActiveKey] osPeers[osActiveKey] = osActiveCount + 1 @@ -320,6 +332,8 @@ func (w *Worker) generateProperties(ctx context.Context) properties { metricsProperties["ephemeral_peers_setup_keys"] = ephemeralPeersSKs metricsProperties["ephemeral_peers_setup_keys_usage"] = ephemeralPeersSKUsage metricsProperties["active_peers_last_day"] = activePeersLastDay + metricsProperties["active_user_peers_last_day"] = activeUserPeersLastDay + metricsProperties["active_users_last_day"] = len(activeUsersLastDay) metricsProperties["user_peers"] = userPeers metricsProperties["rules"] = rules metricsProperties["rules_with_src_posture_checks"] = rulesWithSrcPostureChecks @@ -338,6 +352,7 @@ func (w *Worker) generateProperties(ctx context.Context) properties { metricsProperties["ui_clients"] = uiClient metricsProperties["idp_manager"] = w.idpManager metricsProperties["store_engine"] = w.dataSource.GetStoreEngine() + metricsProperties["rosenpass_enabled"] = rosenpassEnabled for protocol, count := range rulesProtocol { metricsProperties["rules_protocol_"+protocol] = count diff --git a/management/server/metrics/selfhosted_test.go b/management/server/metrics/selfhosted_test.go index de6686400..db0d90e64 100644 --- a/management/server/metrics/selfhosted_test.go +++ b/management/server/metrics/selfhosted_test.go @@ -47,8 +47,8 @@ func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account { "1": { ID: "1", UserID: "test", - SSHEnabled: true, - Meta: nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"}, + SSHEnabled: false, + Meta: nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}}, }, }, Policies: []*types.Policy{ @@ -312,7 +312,19 @@ func TestGenerateProperties(t *testing.T) { } if properties["posture_checks"] != 2 { - t.Errorf("expected 1 posture_checks, got %d", properties["posture_checks"]) + t.Errorf("expected 2 posture_checks, got %d", properties["posture_checks"]) + } + + if properties["rosenpass_enabled"] != 1 { + t.Errorf("expected 1 rosenpass_enabled, got %d", properties["rosenpass_enabled"]) + } + + if properties["active_user_peers_last_day"] != 2 { + t.Errorf("expected 2 active_user_peers_last_day, got %d", properties["active_user_peers_last_day"]) + } + + if properties["active_users_last_day"] != 1 { + t.Errorf("expected 1 active_users_last_day, got %d", properties["active_users_last_day"]) } } From 5343bee7b2c0e0d0390a2a0d6a5ebf6c618ec977 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sun, 22 Jun 2025 16:44:33 +0200 Subject: [PATCH 109/213] [management] check and log on new management version (#4029) This PR enhances the version checker to send a custom User-Agent header when polling for updates, and configures both the management CLI and client UI to use distinct agents. - NewUpdate now takes an `httpAgent` string to set the User-Agent header. - `fetchVersion` builds a custom HTTP request (instead of `http.Get`) and sets the User-Agent. - Management CLI and client UI now pass `"nb/management"` and `"nb/client-ui"` respectively to NewUpdate. - Tests updated to supply an `httpAgent` constant. - Logs if there is a new version available for management --- client/ui/client_ui.go | 2 +- management/cmd/management.go | 7 +++++++ version/update.go | 14 ++++++++++++-- version/update_test.go | 8 +++++--- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 2b74c31d1..b7f98951e 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -280,7 +280,7 @@ func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool showAdvancedSettings: showSettings, showNetworks: showNetworks, - update: version.NewUpdate(), + update: version.NewUpdate("nb/client-ui"), } s.eventHandler = newEventHandler(s) diff --git a/management/cmd/management.go b/management/cmd/management.go index bce09efdd..27376e068 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -357,6 +357,13 @@ var ( log.WithContext(ctx).Infof("running HTTP server and gRPC server on the same port: %s", listener.Addr().String()) serveGRPCWithHTTP(ctx, listener, rootHandler, tlsEnabled) + update := version.NewUpdate("nb/management") + update.SetDaemonVersion(version.NetbirdVersion()) + update.SetOnUpdateListener(func() { + log.WithContext(ctx).Infof("your management version, \"%s\", is outdated, a new management version is available. Learn more here: https://github.com/netbirdio/netbird/releases", version.NetbirdVersion()) + }) + defer update.StopWatch() + SetupCloseHandler() <-stopCh diff --git a/version/update.go b/version/update.go index 3f548ea85..272eef4c6 100644 --- a/version/update.go +++ b/version/update.go @@ -21,6 +21,7 @@ var ( // Update fetch the version info periodically and notify the onUpdateListener in case the UI version or the // daemon version are deprecated type Update struct { + httpAgent string uiVersion *goversion.Version daemonVersion *goversion.Version latestAvailable *goversion.Version @@ -34,7 +35,7 @@ type Update struct { } // NewUpdate instantiate Update and start to fetch the new version information -func NewUpdate() *Update { +func NewUpdate(httpAgent string) *Update { currentVersion, err := goversion.NewVersion(version) if err != nil { currentVersion, _ = goversion.NewVersion("0.0.0") @@ -43,6 +44,7 @@ func NewUpdate() *Update { latestAvailable, _ := goversion.NewVersion("0.0.0") u := &Update{ + httpAgent: httpAgent, latestAvailable: latestAvailable, uiVersion: currentVersion, fetchTicker: time.NewTicker(fetchPeriod), @@ -112,7 +114,15 @@ func (u *Update) startFetcher() { func (u *Update) fetchVersion() bool { log.Debugf("fetching version info from %s", versionURL) - resp, err := http.Get(versionURL) + req, err := http.NewRequest("GET", versionURL, nil) + if err != nil { + log.Errorf("failed to create request for version info: %s", err) + return false + } + + req.Header.Set("User-Agent", u.httpAgent) + + resp, err := http.DefaultClient.Do(req) if err != nil { log.Errorf("failed to fetch version info: %s", err) return false diff --git a/version/update_test.go b/version/update_test.go index 4537ce220..a733714cf 100644 --- a/version/update_test.go +++ b/version/update_test.go @@ -9,6 +9,8 @@ import ( "time" ) +const httpAgent = "pkg/test" + func TestNewUpdate(t *testing.T) { version = "1.0.0" svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -21,7 +23,7 @@ func TestNewUpdate(t *testing.T) { wg.Add(1) onUpdate := false - u := NewUpdate() + u := NewUpdate(httpAgent) defer u.StopWatch() u.SetOnUpdateListener(func() { onUpdate = true @@ -46,7 +48,7 @@ func TestDoNotUpdate(t *testing.T) { wg.Add(1) onUpdate := false - u := NewUpdate() + u := NewUpdate(httpAgent) defer u.StopWatch() u.SetOnUpdateListener(func() { onUpdate = true @@ -71,7 +73,7 @@ func TestDaemonUpdate(t *testing.T) { wg.Add(1) onUpdate := false - u := NewUpdate() + u := NewUpdate(httpAgent) defer u.StopWatch() u.SetOnUpdateListener(func() { onUpdate = true From f37aa2cc9d027fe0d7ce1518d260d9423b5a95e2 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:09:02 +0200 Subject: [PATCH 110/213] [misc] Specify netbird binary location in Dockerfiles (#4024) --- client/Dockerfile | 5 ++++- client/Dockerfile-rootless | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/Dockerfile b/client/Dockerfile index 16b2916c7..5f1f70040 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,6 +1,9 @@ FROM alpine:3.21.3 # iproute2: busybox doesn't display ip rules properly RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables + +ARG NETBIRD_BINARY=netbird +COPY ${NETBIRD_BINARY} /usr/local/bin/netbird + ENV NB_FOREGROUND_MODE=true ENTRYPOINT [ "/usr/local/bin/netbird","up"] -COPY netbird /usr/local/bin/netbird diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless index 78314ba12..5055cb20d 100644 --- a/client/Dockerfile-rootless +++ b/client/Dockerfile-rootless @@ -1,6 +1,7 @@ FROM alpine:3.21.0 -COPY netbird /usr/local/bin/netbird +ARG NETBIRD_BINARY=netbird +COPY ${NETBIRD_BINARY} /usr/local/bin/netbird RUN apk add --no-cache ca-certificates \ && adduser -D -h /var/lib/netbird netbird From d897365abcda8527b7ff34d20d43cb5920b4428c Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:32:37 +0200 Subject: [PATCH 111/213] [client] Don't open cmd.exe during MSI actions (#4041) --- .github/workflows/release.yml | 2 +- client/netbird.wxs | 27 +++++---------------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4fc7f3154..2782ec3e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.0.18" + SIGN_PIPE_VER: "v0.0.19" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" COPYRIGHT: "NetBird GmbH" diff --git a/client/netbird.wxs b/client/netbird.wxs index 5e03a014d..91595539a 100644 --- a/client/netbird.wxs +++ b/client/netbird.wxs @@ -1,8 +1,10 @@ + xmlns="http://wixtoolset.org/schemas/v4/wxs" + xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"> + @@ -46,29 +48,10 @@ - + + - - - - - - - - From 1b73fae46e446cbd695899cafc53a4878694a6af Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:46:35 +0200 Subject: [PATCH 112/213] [management] add breakdown of network map calculation metrics (#4020) --- management/server/peer.go | 16 +++++- .../server/telemetry/updatechannel_metrics.go | 56 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index 1a1289721..254048a96 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1169,7 +1169,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account return } - start := time.Now() + globalStart := time.Now() approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { @@ -1204,18 +1204,27 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account defer wg.Done() defer func() { <-semaphore }() + start := time.Now() + postureChecks, err := am.getPeerPostureChecks(account, p.ID) if err != nil { log.WithContext(ctx).Debugf("failed to get posture checks for peer %s: %v", peer.ID, err) return } + am.metrics.UpdateChannelMetrics().CountCalcPostureChecksDuration(time.Since(start)) + start = time.Now() + remotePeerNetworkMap := account.GetPeerNetworkMap(ctx, p.ID, customZone, approvedPeersMap, resourcePolicies, routers, am.metrics.AccountManagerMetrics()) + am.metrics.UpdateChannelMetrics().CountCalcPeerNetworkMapDuration(time.Since(start)) + start = time.Now() + proxyNetworkMap, ok := proxyNetworkMaps[p.ID] if ok { remotePeerNetworkMap.Merge(proxyNetworkMap) } + am.metrics.UpdateChannelMetrics().CountMergeNetworkMapDuration(time.Since(start)) extraSetting, err := am.settingsManager.GetExtraSettings(ctx, accountID) if err != nil { @@ -1223,7 +1232,10 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account return } + start = time.Now() update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting) + am.metrics.UpdateChannelMetrics().CountToSyncResponseDuration(time.Since(start)) + am.peersUpdateManager.SendUpdate(ctx, p.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap}) }(peer) } @@ -1232,7 +1244,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account wg.Wait() if am.metrics != nil { - am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start)) + am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(globalStart)) } } diff --git a/management/server/telemetry/updatechannel_metrics.go b/management/server/telemetry/updatechannel_metrics.go index 584b9ec20..21b4a6085 100644 --- a/management/server/telemetry/updatechannel_metrics.go +++ b/management/server/telemetry/updatechannel_metrics.go @@ -18,6 +18,10 @@ type UpdateChannelMetrics struct { getAllConnectedPeersDurationMicro metric.Int64Histogram getAllConnectedPeers metric.Int64Histogram hasChannelDurationMicro metric.Int64Histogram + calcPostureChecksDurationMicro metric.Int64Histogram + calcPeerNetworkMapDurationMicro metric.Int64Histogram + mergeNetworkMapDurationMicro metric.Int64Histogram + toSyncResponseDurationMicro metric.Int64Histogram ctx context.Context } @@ -89,6 +93,38 @@ func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateCh return nil, err } + calcPostureChecksDurationMicro, err := meter.Int64Histogram("management.updatechannel.calc.posturechecks.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to get the posture checks for a peer"), + ) + if err != nil { + return nil, err + } + + calcPeerNetworkMapDurationMicro, err := meter.Int64Histogram("management.updatechannel.calc.networkmap.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to calculate the network map for a peer"), + ) + if err != nil { + return nil, err + } + + mergeNetworkMapDurationMicro, err := meter.Int64Histogram("management.updatechannel.merge.networkmap.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to merge the network maps for a peer"), + ) + if err != nil { + return nil, err + } + + toSyncResponseDurationMicro, err := meter.Int64Histogram("management.updatechannel.tosyncresponse.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to convert the network map to sync response"), + ) + if err != nil { + return nil, err + } + return &UpdateChannelMetrics{ createChannelDurationMicro: createChannelDurationMicro, closeChannelDurationMicro: closeChannelDurationMicro, @@ -98,6 +134,10 @@ func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateCh getAllConnectedPeersDurationMicro: getAllConnectedPeersDurationMicro, getAllConnectedPeers: getAllConnectedPeers, hasChannelDurationMicro: hasChannelDurationMicro, + calcPostureChecksDurationMicro: calcPostureChecksDurationMicro, + calcPeerNetworkMapDurationMicro: calcPeerNetworkMapDurationMicro, + mergeNetworkMapDurationMicro: mergeNetworkMapDurationMicro, + toSyncResponseDurationMicro: toSyncResponseDurationMicro, ctx: ctx, }, nil } @@ -137,3 +177,19 @@ func (metrics *UpdateChannelMetrics) CountGetAllConnectedPeersDuration(duration func (metrics *UpdateChannelMetrics) CountHasChannelDuration(duration time.Duration) { metrics.hasChannelDurationMicro.Record(metrics.ctx, duration.Microseconds()) } + +func (metrics *UpdateChannelMetrics) CountCalcPostureChecksDuration(duration time.Duration) { + metrics.calcPostureChecksDurationMicro.Record(metrics.ctx, duration.Microseconds()) +} + +func (metrics *UpdateChannelMetrics) CountCalcPeerNetworkMapDuration(duration time.Duration) { + metrics.calcPeerNetworkMapDurationMicro.Record(metrics.ctx, duration.Microseconds()) +} + +func (metrics *UpdateChannelMetrics) CountMergeNetworkMapDuration(duration time.Duration) { + metrics.mergeNetworkMapDurationMicro.Record(metrics.ctx, duration.Microseconds()) +} + +func (metrics *UpdateChannelMetrics) CountToSyncResponseDuration(duration time.Duration) { + metrics.toSyncResponseDurationMicro.Record(metrics.ctx, duration.Microseconds()) +} From 52ff9d960213181a49cfa5f6bdfae6fb9bf3f513 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:34:22 +0200 Subject: [PATCH 113/213] [management] remove unused transaction (#4053) --- management/server/integrated_validator.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index edb89466c..cfde7c614 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -83,15 +83,12 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI var peers []*nbpeer.Peer var settings *types.Settings - err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } + groups, err = am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } - peers, err = transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") - return err - }) + peers, err = am.Store.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") if err != nil { return nil, err } From 34ac4e4b5a8ec6d6868623efb3ac602822f9f76d Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Thu, 26 Jun 2025 10:45:00 +0200 Subject: [PATCH 114/213] [misc] fix: self-hosting: the wrong default for NETBIRD_AUTH_PKCE_LOGIN_FLAG (#4055) * fix: self-hosting: the wrong default for NETBIRD_AUTH_PKCE_LOGIN_FLAG fixes https://github.com/netbirdio/netbird/issues/4054 * un-quote the number Co-authored-by: Maycon Santos --------- Co-authored-by: Maycon Santos --- infrastructure_files/base.setup.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index fdba1f215..4c0dc946e 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -60,7 +60,7 @@ NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken} NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"} NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false} NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN=${NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN:-false} -NETBIRD_AUTH_PKCE_LOGIN_FLAG=${NETBIRD_AUTH_PKCE_LOGIN_FLAG:-1} +NETBIRD_AUTH_PKCE_LOGIN_FLAG=${NETBIRD_AUTH_PKCE_LOGIN_FLAG:-0} NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE # Dashboard From 0480507a10bcb67a2fa21ae9376fe14802e1edcc Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Sat, 28 Jun 2025 11:38:15 +0200 Subject: [PATCH 115/213] [management] report networkmap duration in ms (#4064) --- management/server/telemetry/updatechannel_metrics.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/management/server/telemetry/updatechannel_metrics.go b/management/server/telemetry/updatechannel_metrics.go index 21b4a6085..2b280b352 100644 --- a/management/server/telemetry/updatechannel_metrics.go +++ b/management/server/telemetry/updatechannel_metrics.go @@ -19,7 +19,7 @@ type UpdateChannelMetrics struct { getAllConnectedPeers metric.Int64Histogram hasChannelDurationMicro metric.Int64Histogram calcPostureChecksDurationMicro metric.Int64Histogram - calcPeerNetworkMapDurationMicro metric.Int64Histogram + calcPeerNetworkMapDurationMs metric.Int64Histogram mergeNetworkMapDurationMicro metric.Int64Histogram toSyncResponseDurationMicro metric.Int64Histogram ctx context.Context @@ -101,8 +101,8 @@ func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateCh return nil, err } - calcPeerNetworkMapDurationMicro, err := meter.Int64Histogram("management.updatechannel.calc.networkmap.duration.micro", - metric.WithUnit("microseconds"), + calcPeerNetworkMapDurationMs, err := meter.Int64Histogram("management.updatechannel.calc.networkmap.duration.ms", + metric.WithUnit("milliseconds"), metric.WithDescription("Duration of how long it takes to calculate the network map for a peer"), ) if err != nil { @@ -135,7 +135,7 @@ func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateCh getAllConnectedPeers: getAllConnectedPeers, hasChannelDurationMicro: hasChannelDurationMicro, calcPostureChecksDurationMicro: calcPostureChecksDurationMicro, - calcPeerNetworkMapDurationMicro: calcPeerNetworkMapDurationMicro, + calcPeerNetworkMapDurationMs: calcPeerNetworkMapDurationMs, mergeNetworkMapDurationMicro: mergeNetworkMapDurationMicro, toSyncResponseDurationMicro: toSyncResponseDurationMicro, ctx: ctx, @@ -183,7 +183,7 @@ func (metrics *UpdateChannelMetrics) CountCalcPostureChecksDuration(duration tim } func (metrics *UpdateChannelMetrics) CountCalcPeerNetworkMapDuration(duration time.Duration) { - metrics.calcPeerNetworkMapDurationMicro.Record(metrics.ctx, duration.Microseconds()) + metrics.calcPeerNetworkMapDurationMs.Record(metrics.ctx, duration.Milliseconds()) } func (metrics *UpdateChannelMetrics) CountMergeNetworkMapDuration(duration time.Duration) { From c1c71b6d396b4f34af83195bd04780af0c133d40 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Mon, 30 Jun 2025 11:57:42 +0200 Subject: [PATCH 116/213] [client] improve adding route log message (#4034) from: Adding route to 1.2.3.4/32 via invalid IP @ 10 (wt0) to: Adding route to 1.2.3.4/32 via no-ip @ 10 (wt0) --- client/internal/routemanager/systemops/systemops.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index 261567dc3..8caf22f81 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -28,7 +28,10 @@ func (n Nexthop) String() string { if n.Intf == nil { return n.IP.String() } - return fmt.Sprintf("%s @ %d (%s)", n.IP.String(), n.Intf.Index, n.Intf.Name) + if n.IP.IsValid() { + return fmt.Sprintf("%s @ %d (%s)", n.IP.String(), n.Intf.Index, n.Intf.Name) + } + return fmt.Sprintf("no-ip @ %d (%s)", n.Intf.Index, n.Intf.Name) } type wgIface interface { From ef6cd36f1a3621c9e57a12161e24c3bfebbaba12 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Mon, 30 Jun 2025 11:59:35 +0200 Subject: [PATCH 117/213] [misc] fix arch install.sh error with empty temporary dependencies handle empty var before calling removal command --- release_files/install.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/release_files/install.sh b/release_files/install.sh index 0f63529ea..696cb03f1 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -154,8 +154,10 @@ add_aur_repo() { cd netbird-ui && makepkg -sri --noconfirm fi - # Clean up the installed packages - ${SUDO} pacman -Rs "$REMOVE_PKGS" --noconfirm + if [ -n "$REMOVE_PKGS" ]; then + # Clean up the installed packages + ${SUDO} pacman -Rs "$REMOVE_PKGS" --noconfirm + fi } prepare_tun_module() { @@ -494,4 +496,4 @@ case "$UPDATE_FLAG" in ;; *) install_netbird -esac \ No newline at end of file +esac From 07d1ad35fc91729c43383950fc82365a6f057c76 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Mon, 30 Jun 2025 12:02:03 +0200 Subject: [PATCH 118/213] [misc] start the service after installation on arch linux (#4071) --- release_files/install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/release_files/install.sh b/release_files/install.sh index 696cb03f1..856d332cb 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -130,7 +130,7 @@ repo_gpgcheck=1 EOF } -add_aur_repo() { +install_aur_package() { INSTALL_PKGS="git base-devel go" REMOVE_PKGS="" @@ -279,7 +279,9 @@ install_netbird() { ;; pacman) ${SUDO} pacman -Syy - add_aur_repo + install_aur_package + # in-line with the docs at https://wiki.archlinux.org/title/Netbird + ${SUDO} systemctl enable --now netbird@main.service ;; pkg) # Check if the package is already installed From 6ce864336845a5b6a6910397df91411ac306ab2a Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 1 Jul 2025 13:45:55 +0200 Subject: [PATCH 119/213] [client] Run login popup on goroutine (#4080) --- client/ui/client_ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index b7f98951e..ace5b71e4 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -879,7 +879,7 @@ func (s *serviceClient) onUpdateAvailable() { func (s *serviceClient) onSessionExpire() { s.sendNotification = true if s.sendNotification { - s.eventHandler.runSelfCommand(s.ctx, "login-url", "true") + go s.eventHandler.runSelfCommand(s.ctx, "login-url", "true") s.sendNotification = false } } From 29cbfe8467b285aede6f4844dcf1ef4e0fcf969f Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 1 Jul 2025 16:23:31 +0200 Subject: [PATCH 120/213] [misc] update sign pipeline version to v0.0.20 (#4082) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2782ec3e9..00898ab29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.0.19" + SIGN_PIPE_VER: "v0.0.20" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" COPYRIGHT: "NetBird GmbH" From dbdef04b9ed882dc72e52f46747b4a5bcee46379 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Wed, 2 Jul 2025 02:35:13 +0200 Subject: [PATCH 121/213] [misc] getting-started-with-zitadel.sh: drop unnecessary port 8080 (#4075) --- infrastructure_files/getting-started-with-zitadel.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/infrastructure_files/getting-started-with-zitadel.sh b/infrastructure_files/getting-started-with-zitadel.sh index 1e67bd177..2118ef480 100644 --- a/infrastructure_files/getting-started-with-zitadel.sh +++ b/infrastructure_files/getting-started-with-zitadel.sh @@ -791,7 +791,6 @@ services: - '443:443' - '443:443/udp' - '80:80' - - '8080:8080' volumes: - netbird_caddy_data:/data - ./Caddyfile:/etc/caddy/Caddyfile From d9402168adf0600e53fa912d7c96ec306228ba68 Mon Sep 17 00:00:00 2001 From: Ali Amer <76897266+aliamerj@users.noreply.github.com> Date: Wed, 2 Jul 2025 03:41:59 +0300 Subject: [PATCH 122/213] [management] Add option to disable default all-to-all policy (#3970) This PR introduces a new configuration option `DisableDefaultPolicy` that prevents the creation of the default all-to-all policy when new accounts are created. This is useful for automation scenarios where explicit policies are preferred. ### Key Changes: - Added DisableDefaultPolicy flag to the management server config - Modified account creation logic to respect this flag - Updated all test cases to explicitly pass the flag (defaulting to false to maintain backward compatibility) - Propagated the flag through the account manager initialization chain ### Testing: - Verified default behavior remains unchanged when flag is false - Confirmed no default policy is created when flag is true - All existing tests pass with the new parameter --- .../workflows/test-infrastructure-files.yml | 2 + client/cmd/testutil_test.go | 2 +- client/internal/engine_test.go | 2 +- client/server/server_test.go | 2 +- infrastructure_files/base.setup.env | 2 + infrastructure_files/management.json.tmpl | 1 + infrastructure_files/setup.env.example | 3 +- infrastructure_files/tests/setup.env | 1 + management/client/client_test.go | 2 +- management/cmd/management.go | 2 +- management/server/account.go | 12 +++-- management/server/account_test.go | 10 ++-- management/server/dns_test.go | 4 +- management/server/ephemeral_test.go | 2 +- management/server/group_test.go | 2 +- .../http/testing/testing_tools/tools.go | 3 +- management/server/management_proto_test.go | 2 +- management/server/management_test.go | 2 +- management/server/nameserver_test.go | 4 +- management/server/peer_test.go | 16 +++--- management/server/posture_checks_test.go | 2 +- management/server/route_test.go | 4 +- management/server/store/sql_store.go | 2 +- management/server/store/sql_store_test.go | 2 +- management/server/store/store.go | 2 +- management/server/types/account.go | 6 ++- management/server/types/config.go | 3 ++ management/server/user_test.go | 50 +++++++++---------- 28 files changed, 82 insertions(+), 65 deletions(-) diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index e2f9e40c8..bd37f65c4 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -134,6 +134,7 @@ jobs: NETBIRD_STORE_ENGINE_MYSQL_DSN: '${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$' CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4" + CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false run: | set -x @@ -180,6 +181,7 @@ jobs: grep -A 7 Relay management.json | egrep '"Secret": ".+"' grep DisablePromptLogin management.json | grep 'true' grep LoginFlag management.json | grep 0 + grep DisableDefaultPolicy management.json | grep "$CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY" - name: Install modules run: go mod tidy diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index 258a8daff..b5a80d63a 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -103,7 +103,7 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc Return(&types.Settings{}, nil). AnyTimes() - accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock) + accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false) if err != nil { t.Fatal(err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 8c084e366..d9c9881da 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1476,7 +1476,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri permissionsManager := permissions.NewManager(store) - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) if err != nil { return nil, "", err } diff --git a/client/server/server_test.go b/client/server/server_test.go index f2dff76fd..376b7e8bd 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -206,7 +206,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve settingsMockManager := settings.NewMockManager(ctrl) permissionsManagerMock := permissions.NewMockManager(ctrl) - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock) + accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false) if err != nil { return nil, "", err } diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index 4c0dc946e..e59939191 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -15,6 +15,7 @@ NETBIRD_MGMT_API_CERT_KEY_FILE="/etc/letsencrypt/live/$NETBIRD_LETSENCRYPT_DOMAI NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN=$NETBIRD_DOMAIN NETBIRD_MGMT_DNS_DOMAIN=${NETBIRD_MGMT_DNS_DOMAIN:-netbird.selfhosted} NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=${NETBIRD_MGMT_IDP_SIGNKEY_REFRESH:-false} +NETBIRD_MGMT_DISABLE_DEFAULT_POLICY=${NETBIRD_MGMT_DISABLE_DEFAULT_POLICY:-false} # Signal NETBIRD_SIGNAL_PROTOCOL="http" @@ -139,3 +140,4 @@ export NETBIRD_RELAY_PORT export NETBIRD_RELAY_ENDPOINT export NETBIRD_RELAY_AUTH_SECRET export NETBIRD_RELAY_TAG +export NETBIRD_MGMT_DISABLE_DEFAULT_POLICY diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index 4d09816ef..32f083917 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -38,6 +38,7 @@ "0.0.0.0/0" ] }, + "DisableDefaultPolicy": $NETBIRD_MGMT_DISABLE_DEFAULT_POLICY, "Datadir": "", "DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY", "StoreConfig": { diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example index b6a209953..382e1e18c 100644 --- a/infrastructure_files/setup.env.example +++ b/infrastructure_files/setup.env.example @@ -92,7 +92,8 @@ NETBIRD_LETSENCRYPT_EMAIL="" NETBIRD_DISABLE_ANONYMOUS_METRICS=false # DNS DOMAIN configures the domain name used for peer resolution. By default it is netbird.selfhosted NETBIRD_MGMT_DNS_DOMAIN=netbird.selfhosted - +# Disable default all-to-all policy for new accounts +NETBIRD_MGMT_DISABLE_DEFAULT_POLICY=false # ------------------------------------------- # Relay settings # ------------------------------------------- diff --git a/infrastructure_files/tests/setup.env b/infrastructure_files/tests/setup.env index dc1d6e014..5287e6c55 100644 --- a/infrastructure_files/tests/setup.env +++ b/infrastructure_files/tests/setup.env @@ -29,3 +29,4 @@ NETBIRD_TURN_EXTERNAL_IP=1.2.3.4 NETBIRD_RELAY_PORT=33445 NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN=true NETBIRD_AUTH_PKCE_LOGIN_FLAG=0 +NETBIRD_MGMT_DISABLE_DEFAULT_POLICY=$CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY diff --git a/management/client/client_test.go b/management/client/client_test.go index b22a79930..c163d1833 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -100,7 +100,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { Return(true, nil). AnyTimes() - accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock) + accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false) if err != nil { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index 27376e068..878e4c39e 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -215,7 +215,7 @@ var ( peersManager := peers.NewManager(store, permissionsManager) proxyController := integrations.NewController(store) accountManager, err := server.BuildManager(ctx, store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain, - dnsDomain, eventStore, geo, userDeleteFromIDPEnabled, integratedPeerValidator, appMetrics, proxyController, settingsManager, permissionsManager) + dnsDomain, eventStore, geo, userDeleteFromIDPEnabled, integratedPeerValidator, appMetrics, proxyController, settingsManager, permissionsManager, config.DisableDefaultPolicy) if err != nil { return fmt.Errorf("failed to build default manager: %v", err) } diff --git a/management/server/account.go b/management/server/account.go index b376f6f5e..3b7359502 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -102,6 +102,8 @@ type DefaultAccountManager struct { accountUpdateLocks sync.Map updateAccountPeersBufferInterval atomic.Int64 + + disableDefaultPolicy bool } // getJWTGroupsChanges calculates the changes needed to sync a user's JWT groups. @@ -170,6 +172,7 @@ func BuildManager( proxyController port_forwarding.Controller, settingsManager settings.Manager, permissionsManager permissions.Manager, + disableDefaultPolicy bool, ) (*DefaultAccountManager, error) { start := time.Now() defer func() { @@ -195,6 +198,7 @@ func BuildManager( proxyController: proxyController, settingsManager: settingsManager, permissionsManager: permissionsManager, + disableDefaultPolicy: disableDefaultPolicy, } am.startWarmup(ctx) @@ -543,7 +547,7 @@ func (am *DefaultAccountManager) newAccount(ctx context.Context, userID, domain log.WithContext(ctx).Warnf("an account with ID already exists, retrying...") continue case statusErr.Type() == status.NotFound: - newAccount := newAccountWithId(ctx, accountId, userID, domain) + newAccount := newAccountWithId(ctx, accountId, userID, domain, am.disableDefaultPolicy) am.StoreEvent(ctx, userID, newAccount.Id, accountId, activity.AccountCreated, nil) return newAccount, nil default: @@ -1688,7 +1692,7 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account } // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id -func newAccountWithId(ctx context.Context, accountID, userID, domain string) *types.Account { +func newAccountWithId(ctx context.Context, accountID, userID, domain string, disableDefaultPolicy bool) *types.Account { log.WithContext(ctx).Debugf("creating new account") network := types.NewNetwork() @@ -1731,7 +1735,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := acc.AddAllGroup(); err != nil { + if err := acc.AddAllGroup(disableDefaultPolicy); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc @@ -1833,7 +1837,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.C }, } - if err := newAccount.AddAllGroup(); err != nil { + if err := newAccount.AddAllGroup(am.disableDefaultPolicy); err != nil { return nil, false, status.Errorf(status.Internal, "failed to add all group to new account by private domain") } diff --git a/management/server/account_test.go b/management/server/account_test.go index c3b1f31a6..49fcedd56 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -373,7 +373,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { } for _, testCase := range tt { - account := newAccountWithId(context.Background(), "account-1", userID, "netbird.io") + account := newAccountWithId(context.Background(), "account-1", userID, "netbird.io", false) account.UpdateSettings(&testCase.accountSettings) account.Network = network account.Peers = testCase.peers @@ -398,7 +398,7 @@ func TestNewAccount(t *testing.T) { domain := "netbird.io" userId := "account_creator" accountID := "account_id" - account := newAccountWithId(context.Background(), accountID, userId, domain) + account := newAccountWithId(context.Background(), accountID, userId, domain, false) verifyNewAccountHasDefaultFields(t, account, userId, domain, []string{userId}) } @@ -640,7 +640,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) { func TestDefaultAccountManager_SyncUserJWTGroups(t *testing.T) { userId := "user-id" domain := "test.domain" - _ = newAccountWithId(context.Background(), "", userId, domain) + _ = newAccountWithId(context.Background(), "", userId, domain, false) manager, err := createManager(t) require.NoError(t, err, "unable to create account manager") accountID, err := manager.GetAccountIDByUserID(context.Background(), userId, domain) @@ -793,7 +793,7 @@ func TestAccountManager_GetAccountByUserID(t *testing.T) { } func createAccount(am *DefaultAccountManager, accountID, userID, domain string) (*types.Account, error) { - account := newAccountWithId(context.Background(), accountID, userID, domain) + account := newAccountWithId(context.Background(), accountID, userID, domain, false) err := am.Store.SaveAccount(context.Background(), account) if err != nil { return nil, err @@ -2879,7 +2879,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, error) { permissionsManager := permissions.NewManager(store) - manager, err := BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + manager, err := BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) if err != nil { return nil, err } diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 36476b14c..02bb042d7 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -217,7 +217,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { settingsMockManager := settings.NewMockManager(ctrl) permissionsManager := permissions.NewManager(store) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } func createDNSStore(t *testing.T) (store.Store, error) { @@ -267,7 +267,7 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*types.Account domain := "example.com" - account := newAccountWithId(context.Background(), dnsAccountID, dnsAdminUserID, domain) + account := newAccountWithId(context.Background(), dnsAccountID, dnsAdminUserID, domain, false) account.Users[dnsRegularUserID] = &types.User{ Id: dnsRegularUserID, diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go index 38477f7a8..3cf6ae7f3 100644 --- a/management/server/ephemeral_test.go +++ b/management/server/ephemeral_test.go @@ -127,7 +127,7 @@ func TestNewManagerPeerDisconnected(t *testing.T) { } func seedPeers(store *MockStore, numberOfPeers int, numberOfEphemeralPeers int) { - store.account = newAccountWithId(context.Background(), "my account", "", "") + store.account = newAccountWithId(context.Background(), "my account", "", "", false) for i := 0; i < numberOfPeers; i++ { peerId := fmt.Sprintf("peer_%d", i) diff --git a/management/server/group_test.go b/management/server/group_test.go index 4966f2b33..631fe3a71 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -369,7 +369,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t Id: "example user", AutoGroups: []string{groupForUsers.ID}, } - account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain) + account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, false) account.Routes[routeResource.ID] = routeResource account.Routes[routePeerGroupResource.ID] = routePeerGroupResource account.NameServerGroups[nameServerGroup.ID] = nameServerGroup diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go index 8c5d2e386..829bff455 100644 --- a/management/server/http/testing/testing_tools/tools.go +++ b/management/server/http/testing/testing_tools/tools.go @@ -1,5 +1,4 @@ package testing_tools - import ( "bytes" "context" @@ -138,7 +137,7 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve userManager := users.NewManager(store) permissionsManager := permissions.NewManager(store) settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager) - am, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager) + am, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false) if err != nil { t.Fatalf("Failed to create manager: %v", err) } diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index b85a43da4..337890ef9 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -444,7 +444,7 @@ func startManagementForTest(t *testing.T, testFile string, config *types.Config) permissionsManager := permissions.NewManager(store) accountManager, err := BuildManager(ctx, store, peersUpdateManager, nil, "", "netbird.selfhosted", - eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) if err != nil { cleanup() diff --git a/management/server/management_test.go b/management/server/management_test.go index a4f9a5e38..0a6b3f751 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -211,7 +211,7 @@ func startServer( port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, - ) + false) if err != nil { t.Fatalf("failed creating an account manager: %v", err) } diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index dc4be594f..75d1e7972 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -779,7 +779,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { t.Cleanup(ctrl.Finish) settingsMockManager := settings.NewMockManager(ctrl) permissionsManager := permissions.NewManager(store) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } func createNSStore(t *testing.T) (store.Store, error) { @@ -848,7 +848,7 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*types.Account, userID := testUserID domain := "example.com" - account := newAccountWithId(context.Background(), accountID, userID, domain) + account := newAccountWithId(context.Background(), accountID, userID, domain, false) account.NameServerGroups[existingNSGroup.ID] = &existingNSGroup diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 3d782f04c..775385a29 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -480,7 +480,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { accountID := "test_account" adminUser := "account_creator" someUser := "some_user" - account := newAccountWithId(context.Background(), accountID, adminUser, "") + account := newAccountWithId(context.Background(), accountID, adminUser, "", false) account.Users[someUser] = &types.User{ Id: someUser, Role: types.UserRoleUser, @@ -667,7 +667,7 @@ func TestDefaultAccountManager_GetPeers(t *testing.T) { accountID := "test_account" adminUser := "account_creator" someUser := "some_user" - account := newAccountWithId(context.Background(), accountID, adminUser, "") + account := newAccountWithId(context.Background(), accountID, adminUser, "", false) account.Users[someUser] = &types.User{ Id: someUser, Role: testCase.role, @@ -737,7 +737,7 @@ func setupTestAccountManager(b testing.TB, peers int, groups int) (*DefaultAccou adminUser := "account_creator" regularUser := "regular_user" - account := newAccountWithId(context.Background(), accountID, adminUser, "") + account := newAccountWithId(context.Background(), accountID, adminUser, "", false) account.Users[regularUser] = &types.User{ Id: regularUser, Role: types.UserRoleUser, @@ -1267,7 +1267,7 @@ func Test_RegisterPeerByUser(t *testing.T) { settingsMockManager := settings.NewMockManager(ctrl) permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1342,7 +1342,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { settingsMockManager := settings.NewMockManager(ctrl) permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1477,7 +1477,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) { permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1546,7 +1546,7 @@ func Test_LoginPeer(t *testing.T) { settingsMockManager := settings.NewMockManager(ctrl) permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -2052,7 +2052,7 @@ func Test_DeletePeer(t *testing.T) { // account with an admin and a regular user accountID := "test_account" adminUser := "account_creator" - account := newAccountWithId(context.Background(), accountID, adminUser, "") + account := newAccountWithId(context.Background(), accountID, adminUser, "", false) account.Peers = map[string]*nbpeer.Peer{ "peer1": { ID: "peer1", diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go index 8bd2fab66..f93467375 100644 --- a/management/server/posture_checks_test.go +++ b/management/server/posture_checks_test.go @@ -106,7 +106,7 @@ func initTestPostureChecksAccount(am *DefaultAccountManager) (*types.Account, er Role: types.UserRoleUser, } - account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain) + account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, false) account.Users[admin.Id] = admin account.Users[user.Id] = user diff --git a/management/server/route_test.go b/management/server/route_test.go index 833477b55..77cbc75b9 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1284,7 +1284,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { permissionsManager := permissions.NewManager(store) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager) + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } func createRouterStore(t *testing.T) (store.Store, error) { @@ -1305,7 +1305,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou accountID := "testingAcc" domain := "example.com" - account := newAccountWithId(context.Background(), accountID, userID, domain) + account := newAccountWithId(context.Background(), accountID, userID, domain, false) err := am.Store.SaveAccount(context.Background(), account) if err != nil { return nil, err diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index a6c4d56bf..72a73a57a 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -1184,7 +1184,7 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data for _, account := range fileStore.GetAllAccounts(ctx) { _, err = account.GetGroupAll() if err != nil { - if err := account.AddAllGroup(); err != nil { + if err := account.AddAllGroup(false); err != nil { return nil, err } } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index fab9048e5..f187be8c7 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -2044,7 +2044,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := acc.AddAllGroup(); err != nil { + if err := acc.AddAllGroup(false); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc diff --git a/management/server/store/store.go b/management/server/store/store.go index d41379b1c..f66130ad3 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -391,7 +391,7 @@ func addAllGroupToAccount(ctx context.Context, store Store) error { _, err := account.GetGroupAll() if err != nil { - if err := account.AddAllGroup(); err != nil { + if err := account.AddAllGroup(false); err != nil { return err } shouldSave = true diff --git a/management/server/types/account.go b/management/server/types/account.go index 090ba76e4..5a62ee4c6 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -1546,7 +1546,7 @@ func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[st } // AddAllGroup to account object if it doesn't exist -func (a *Account) AddAllGroup() error { +func (a *Account) AddAllGroup(disableDefaultPolicy bool) error { if len(a.Groups) == 0 { allGroup := &Group{ ID: xid.New().String(), @@ -1558,6 +1558,10 @@ func (a *Account) AddAllGroup() error { } a.Groups = map[string]*Group{allGroup.ID: allGroup} + if disableDefaultPolicy { + return nil + } + id := xid.New().String() defaultPolicy := &Policy{ diff --git a/management/server/types/config.go b/management/server/types/config.go index 13e0f62b5..5c64d6c6d 100644 --- a/management/server/types/config.go +++ b/management/server/types/config.go @@ -53,6 +53,9 @@ type Config struct { StoreConfig StoreConfig ReverseProxy ReverseProxy + + // disable default all-to-all policy + DisableDefaultPolicy bool } // GetAuthAudiences returns the audience from the http config and device authorization flow config diff --git a/management/server/user_test.go b/management/server/user_test.go index 66bdc1683..7508e0609 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -56,7 +56,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = s.SaveAccount(context.Background(), account) if err != nil { @@ -103,7 +103,7 @@ func TestUser_CreatePAT_ForDifferentUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockTargetUserId] = &types.User{ Id: mockTargetUserId, IsServiceUser: false, @@ -131,7 +131,7 @@ func TestUser_CreatePAT_ForServiceUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockTargetUserId] = &types.User{ Id: mockTargetUserId, IsServiceUser: true, @@ -163,7 +163,7 @@ func TestUser_CreatePAT_WithWrongExpiration(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -188,7 +188,7 @@ func TestUser_CreatePAT_WithEmptyName(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -213,7 +213,7 @@ func TestUser_DeletePAT(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockUserID] = &types.User{ Id: mockUserID, PATs: map[string]*types.PersonalAccessToken{ @@ -256,7 +256,7 @@ func TestUser_GetPAT(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockUserID] = &types.User{ Id: mockUserID, AccountID: mockAccountID, @@ -296,7 +296,7 @@ func TestUser_GetAllPATs(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockUserID] = &types.User{ Id: mockUserID, AccountID: mockAccountID, @@ -406,7 +406,7 @@ func TestUser_CreateServiceUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -453,7 +453,7 @@ func TestUser_CreateUser_ServiceUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -501,7 +501,7 @@ func TestUser_CreateUser_RegularUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -532,7 +532,7 @@ func TestUser_InviteNewUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -639,7 +639,7 @@ func TestUser_DeleteUser_ServiceUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockServiceUserID] = tt.serviceUser err = store.SaveAccount(context.Background(), account) @@ -678,7 +678,7 @@ func TestUser_DeleteUser_SelfDelete(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -705,7 +705,7 @@ func TestUser_DeleteUser_regularUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) targetId := "user2" account.Users[targetId] = &types.User{ @@ -792,7 +792,7 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) targetId := "user2" account.Users[targetId] = &types.User{ @@ -952,7 +952,7 @@ func TestDefaultAccountManager_GetUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) err = store.SaveAccount(context.Background(), account) if err != nil { @@ -988,7 +988,7 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users["normal_user1"] = types.NewRegularUser("normal_user1") account.Users["normal_user2"] = types.NewRegularUser("normal_user2") @@ -1030,7 +1030,7 @@ func TestDefaultAccountManager_ExternalCache(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) externalUser := &types.User{ Id: "externalUser", Role: types.UserRoleUser, @@ -1098,7 +1098,7 @@ func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockServiceUserID] = &types.User{ Id: mockServiceUserID, Role: "user", @@ -1132,7 +1132,7 @@ func TestUser_GetUsersFromAccount_ForUser(t *testing.T) { } t.Cleanup(cleanup) - account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") + account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false) account.Users[mockServiceUserID] = &types.User{ Id: mockServiceUserID, Role: "user", @@ -1499,7 +1499,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) { } t.Cleanup(cleanup) - account1 := newAccountWithId(context.Background(), "account1", "ownerAccount1", "") + account1 := newAccountWithId(context.Background(), "account1", "ownerAccount1", "", false) targetId := "user2" account1.Users[targetId] = &types.User{ Id: targetId, @@ -1508,7 +1508,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) { } require.NoError(t, s.SaveAccount(context.Background(), account1)) - account2 := newAccountWithId(context.Background(), "account2", "ownerAccount2", "") + account2 := newAccountWithId(context.Background(), "account2", "ownerAccount2", "", false) require.NoError(t, s.SaveAccount(context.Background(), account2)) permissionsManager := permissions.NewManager(s) @@ -1535,7 +1535,7 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) { } t.Cleanup(cleanup) - account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "") + account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "", false) account1.Settings.RegularUsersViewBlocked = false account1.Users["blocked-user"] = &types.User{ Id: "blocked-user", @@ -1557,7 +1557,7 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) { } require.NoError(t, store.SaveAccount(context.Background(), account1)) - account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "") + account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "", false) account2.Users["settings-blocked-user"] = &types.User{ Id: "settings-blocked-user", Role: types.UserRoleUser, From 56a1a75e3faf63a89e025dcd0799d6335021fb0a Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 2 Jul 2025 09:01:02 +0200 Subject: [PATCH 123/213] [client] Support random wireguard port on client (#4085) Adds support for using a random available WireGuard port when the user specifies port `0`. - Updates `freePort` logic to bind to the requested port (including `0`) without falling back to the default. - Removes default port assignment in the configuration path, allowing `0` to propagate. - Adjusts tests to handle dynamically assigned ports when using `0`. --- client/internal/config.go | 4 ---- client/internal/connect.go | 11 +++-------- client/internal/connect_test.go | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/client/internal/config.go b/client/internal/config.go index 37ee1e1bf..add702cdb 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -319,10 +319,6 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { *input.WireguardPort, config.WgPort) config.WgPort = *input.WireguardPort updated = true - } else if config.WgPort == 0 { - config.WgPort = iface.DefaultWgPort - log.Infof("using default Wireguard port %d", config.WgPort) - updated = true } if input.InterfaceName != nil && *input.InterfaceName != config.WgIface { diff --git a/client/internal/connect.go b/client/internal/connect.go index f59fbe34a..7b49fa3ad 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -17,7 +17,6 @@ import ( "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" - "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" @@ -526,17 +525,13 @@ func statusRecorderToSignalConnStateNotifier(statusRecorder *peer.Status) signal // freePort attempts to determine if the provided port is available, if not it will ask the system for a free port. func freePort(initPort int) (int, error) { - addr := net.UDPAddr{} - if initPort == 0 { - initPort = iface.DefaultWgPort - } - - addr.Port = initPort + addr := net.UDPAddr{Port: initPort} conn, err := net.ListenUDP("udp", &addr) if err == nil { + returnPort := conn.LocalAddr().(*net.UDPAddr).Port closeConnWithLog(conn) - return initPort, nil + return returnPort, nil } // if the port is already in use, ask the system for a free port diff --git a/client/internal/connect_test.go b/client/internal/connect_test.go index 78b4b06e8..c317c88d8 100644 --- a/client/internal/connect_test.go +++ b/client/internal/connect_test.go @@ -13,10 +13,10 @@ func Test_freePort(t *testing.T) { shouldMatch bool }{ { - name: "not provided, fallback to default", + name: "when port is 0 use random port", port: 0, - want: 51820, - shouldMatch: true, + want: 0, + shouldMatch: false, }, { name: "provided and available", @@ -31,7 +31,7 @@ func Test_freePort(t *testing.T) { shouldMatch: false, }, } - c1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 51830}) + c1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 0}) if err != nil { t.Errorf("freePort error = %v", err) } @@ -39,6 +39,14 @@ func Test_freePort(t *testing.T) { _ = c1.Close() }(c1) + if tests[1].port == c1.LocalAddr().(*net.UDPAddr).Port { + tests[1].port++ + tests[1].want++ + } + + tests[2].port = c1.LocalAddr().(*net.UDPAddr).Port + tests[2].want = c1.LocalAddr().(*net.UDPAddr).Port + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 692282691987d33b9ee8d5e01b54dcdb70d38ffe Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Wed, 2 Jul 2025 02:42:47 -0600 Subject: [PATCH 124/213] [client] Support fullstatus without probes (#4052) --- client/cmd/status.go | 2 +- client/proto/daemon.pb.go | 2656 ++++++++++---------------------- client/proto/daemon.proto | 1 + client/proto/daemon_grpc.pb.go | 192 ++- client/server/server.go | 4 +- 5 files changed, 912 insertions(+), 1943 deletions(-) diff --git a/client/cmd/status.go b/client/cmd/status.go index a85ee925e..b108ca57a 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -120,7 +120,7 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) { } defer conn.Close() - resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true}) + resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true, ShouldRunProbes: true}) if err != nil { return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message()) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 6c4c2dc93..202dc6f89 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 -// protoc v3.21.12 +// protoc-gen-go v1.36.6 +// protoc v5.29.3 // source: daemon.proto package proto @@ -14,6 +14,7 @@ import ( timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -195,18 +196,16 @@ func (SystemEvent_Category) EnumDescriptor() ([]byte, []int) { } type EmptyRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EmptyRequest) Reset() { *x = EmptyRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *EmptyRequest) String() string { @@ -217,7 +216,7 @@ func (*EmptyRequest) ProtoMessage() {} func (x *EmptyRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -233,16 +232,13 @@ func (*EmptyRequest) Descriptor() ([]byte, []int) { } type LoginRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // setupKey netbird setup key. SetupKey string `protobuf:"bytes,1,opt,name=setupKey,proto3" json:"setupKey,omitempty"` // This is the old PreSharedKey field which will be deprecated in favor of optionalPreSharedKey field that is defined as optional // to allow clearing of preshared key while being able to persist in the config file. // - // Deprecated: Do not use. + // Deprecated: Marked as deprecated in daemon.proto. PreSharedKey string `protobuf:"bytes,2,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"` // managementUrl to authenticate. ManagementUrl string `protobuf:"bytes,3,opt,name=managementUrl,proto3" json:"managementUrl,omitempty"` @@ -280,15 +276,15 @@ type LoginRequest struct { CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` LazyConnectionEnabled *bool `protobuf:"varint,28,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"` BlockInbound *bool `protobuf:"varint,29,opt,name=block_inbound,json=blockInbound,proto3,oneof" json:"block_inbound,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LoginRequest) Reset() { *x = LoginRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LoginRequest) String() string { @@ -299,7 +295,7 @@ func (*LoginRequest) ProtoMessage() {} func (x *LoginRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -321,7 +317,7 @@ func (x *LoginRequest) GetSetupKey() string { return "" } -// Deprecated: Do not use. +// Deprecated: Marked as deprecated in daemon.proto. func (x *LoginRequest) GetPreSharedKey() string { if x != nil { return x.PreSharedKey @@ -519,23 +515,20 @@ func (x *LoginRequest) GetBlockInbound() bool { } type LoginResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - NeedsSSOLogin bool `protobuf:"varint,1,opt,name=needsSSOLogin,proto3" json:"needsSSOLogin,omitempty"` - UserCode string `protobuf:"bytes,2,opt,name=userCode,proto3" json:"userCode,omitempty"` - VerificationURI string `protobuf:"bytes,3,opt,name=verificationURI,proto3" json:"verificationURI,omitempty"` - VerificationURIComplete string `protobuf:"bytes,4,opt,name=verificationURIComplete,proto3" json:"verificationURIComplete,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + NeedsSSOLogin bool `protobuf:"varint,1,opt,name=needsSSOLogin,proto3" json:"needsSSOLogin,omitempty"` + UserCode string `protobuf:"bytes,2,opt,name=userCode,proto3" json:"userCode,omitempty"` + VerificationURI string `protobuf:"bytes,3,opt,name=verificationURI,proto3" json:"verificationURI,omitempty"` + VerificationURIComplete string `protobuf:"bytes,4,opt,name=verificationURIComplete,proto3" json:"verificationURIComplete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LoginResponse) Reset() { *x = LoginResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LoginResponse) String() string { @@ -546,7 +539,7 @@ func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -590,21 +583,18 @@ func (x *LoginResponse) GetVerificationURIComplete() string { } type WaitSSOLoginRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + UserCode string `protobuf:"bytes,1,opt,name=userCode,proto3" json:"userCode,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` unknownFields protoimpl.UnknownFields - - UserCode string `protobuf:"bytes,1,opt,name=userCode,proto3" json:"userCode,omitempty"` - Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + sizeCache protoimpl.SizeCache } func (x *WaitSSOLoginRequest) Reset() { *x = WaitSSOLoginRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *WaitSSOLoginRequest) String() string { @@ -615,7 +605,7 @@ func (*WaitSSOLoginRequest) ProtoMessage() {} func (x *WaitSSOLoginRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -645,18 +635,16 @@ func (x *WaitSSOLoginRequest) GetHostname() string { } type WaitSSOLoginResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WaitSSOLoginResponse) Reset() { *x = WaitSSOLoginResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *WaitSSOLoginResponse) String() string { @@ -667,7 +655,7 @@ func (*WaitSSOLoginResponse) ProtoMessage() {} func (x *WaitSSOLoginResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -683,18 +671,16 @@ func (*WaitSSOLoginResponse) Descriptor() ([]byte, []int) { } type UpRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpRequest) Reset() { *x = UpRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpRequest) String() string { @@ -705,7 +691,7 @@ func (*UpRequest) ProtoMessage() {} func (x *UpRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -721,18 +707,16 @@ func (*UpRequest) Descriptor() ([]byte, []int) { } type UpResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpResponse) Reset() { *x = UpResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpResponse) String() string { @@ -743,7 +727,7 @@ func (*UpResponse) ProtoMessage() {} func (x *UpResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -759,20 +743,18 @@ func (*UpResponse) Descriptor() ([]byte, []int) { } type StatusRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - GetFullPeerStatus bool `protobuf:"varint,1,opt,name=getFullPeerStatus,proto3" json:"getFullPeerStatus,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + GetFullPeerStatus bool `protobuf:"varint,1,opt,name=getFullPeerStatus,proto3" json:"getFullPeerStatus,omitempty"` + ShouldRunProbes bool `protobuf:"varint,2,opt,name=shouldRunProbes,proto3" json:"shouldRunProbes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StatusRequest) Reset() { *x = StatusRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StatusRequest) String() string { @@ -783,7 +765,7 @@ func (*StatusRequest) ProtoMessage() {} func (x *StatusRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -805,25 +787,29 @@ func (x *StatusRequest) GetGetFullPeerStatus() bool { return false } -type StatusResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *StatusRequest) GetShouldRunProbes() bool { + if x != nil { + return x.ShouldRunProbes + } + return false +} +type StatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` // status of the server. Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` FullStatus *FullStatus `protobuf:"bytes,2,opt,name=fullStatus,proto3" json:"fullStatus,omitempty"` // NetBird daemon version DaemonVersion string `protobuf:"bytes,3,opt,name=daemonVersion,proto3" json:"daemonVersion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StatusResponse) Reset() { *x = StatusResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StatusResponse) String() string { @@ -834,7 +820,7 @@ func (*StatusResponse) ProtoMessage() {} func (x *StatusResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -871,18 +857,16 @@ func (x *StatusResponse) GetDaemonVersion() string { } type DownRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DownRequest) Reset() { *x = DownRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DownRequest) String() string { @@ -893,7 +877,7 @@ func (*DownRequest) ProtoMessage() {} func (x *DownRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -909,18 +893,16 @@ func (*DownRequest) Descriptor() ([]byte, []int) { } type DownResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DownResponse) Reset() { *x = DownResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DownResponse) String() string { @@ -931,7 +913,7 @@ func (*DownResponse) ProtoMessage() {} func (x *DownResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -947,18 +929,16 @@ func (*DownResponse) Descriptor() ([]byte, []int) { } type GetConfigRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetConfigRequest) Reset() { *x = GetConfigRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetConfigRequest) String() string { @@ -969,7 +949,7 @@ func (*GetConfigRequest) ProtoMessage() {} func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -985,10 +965,7 @@ func (*GetConfigRequest) Descriptor() ([]byte, []int) { } type GetConfigResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // managementUrl settings value. ManagementUrl string `protobuf:"bytes,1,opt,name=managementUrl,proto3" json:"managementUrl,omitempty"` // configFile settings value. @@ -1013,15 +990,15 @@ type GetConfigResponse struct { DisableClientRoutes bool `protobuf:"varint,18,opt,name=disable_client_routes,json=disableClientRoutes,proto3" json:"disable_client_routes,omitempty"` DisableServerRoutes bool `protobuf:"varint,19,opt,name=disable_server_routes,json=disableServerRoutes,proto3" json:"disable_server_routes,omitempty"` BlockLanAccess bool `protobuf:"varint,20,opt,name=block_lan_access,json=blockLanAccess,proto3" json:"block_lan_access,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetConfigResponse) Reset() { *x = GetConfigResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetConfigResponse) String() string { @@ -1032,7 +1009,7 @@ func (*GetConfigResponse) ProtoMessage() {} func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1182,10 +1159,7 @@ func (x *GetConfigResponse) GetBlockLanAccess() bool { // PeerState contains the latest state of a peer type PeerState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,omitempty"` @@ -1203,15 +1177,15 @@ type PeerState struct { Networks []string `protobuf:"bytes,16,rep,name=networks,proto3" json:"networks,omitempty"` Latency *durationpb.Duration `protobuf:"bytes,17,opt,name=latency,proto3" json:"latency,omitempty"` RelayAddress string `protobuf:"bytes,18,opt,name=relayAddress,proto3" json:"relayAddress,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PeerState) Reset() { *x = PeerState{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PeerState) String() string { @@ -1222,7 +1196,7 @@ func (*PeerState) ProtoMessage() {} func (x *PeerState) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1358,26 +1332,23 @@ func (x *PeerState) GetRelayAddress() string { // LocalPeerState contains the latest state of the local peer type LocalPeerState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` - PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` - KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"` - Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - RosenpassEnabled bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - RosenpassPermissive bool `protobuf:"varint,6,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` - Networks []string `protobuf:"bytes,7,rep,name=networks,proto3" json:"networks,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` + PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` + KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"` + Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + RosenpassEnabled bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,6,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + Networks []string `protobuf:"bytes,7,rep,name=networks,proto3" json:"networks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LocalPeerState) Reset() { *x = LocalPeerState{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LocalPeerState) String() string { @@ -1388,7 +1359,7 @@ func (*LocalPeerState) ProtoMessage() {} func (x *LocalPeerState) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1454,22 +1425,19 @@ func (x *LocalPeerState) GetNetworks() []string { // SignalState contains the latest state of a signal connection type SignalState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` + Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` unknownFields protoimpl.UnknownFields - - URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` - Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` - Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SignalState) Reset() { *x = SignalState{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SignalState) String() string { @@ -1480,7 +1448,7 @@ func (*SignalState) ProtoMessage() {} func (x *SignalState) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1518,22 +1486,19 @@ func (x *SignalState) GetError() string { // ManagementState contains the latest state of a management connection type ManagementState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` + Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` unknownFields protoimpl.UnknownFields - - URL string `protobuf:"bytes,1,opt,name=URL,proto3" json:"URL,omitempty"` - Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` - Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ManagementState) Reset() { *x = ManagementState{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ManagementState) String() string { @@ -1544,7 +1509,7 @@ func (*ManagementState) ProtoMessage() {} func (x *ManagementState) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1582,22 +1547,19 @@ func (x *ManagementState) GetError() string { // RelayState contains the latest state of the relay type RelayState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + URI string `protobuf:"bytes,1,opt,name=URI,proto3" json:"URI,omitempty"` + Available bool `protobuf:"varint,2,opt,name=available,proto3" json:"available,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` unknownFields protoimpl.UnknownFields - - URI string `protobuf:"bytes,1,opt,name=URI,proto3" json:"URI,omitempty"` - Available bool `protobuf:"varint,2,opt,name=available,proto3" json:"available,omitempty"` - Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RelayState) Reset() { *x = RelayState{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RelayState) String() string { @@ -1608,7 +1570,7 @@ func (*RelayState) ProtoMessage() {} func (x *RelayState) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1645,23 +1607,20 @@ func (x *RelayState) GetError() string { } type NSGroupState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Servers []string `protobuf:"bytes,1,rep,name=servers,proto3" json:"servers,omitempty"` + Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` + Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` unknownFields protoimpl.UnknownFields - - Servers []string `protobuf:"bytes,1,rep,name=servers,proto3" json:"servers,omitempty"` - Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` - Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` - Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *NSGroupState) Reset() { *x = NSGroupState{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NSGroupState) String() string { @@ -1672,7 +1631,7 @@ func (*NSGroupState) ProtoMessage() {} func (x *NSGroupState) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1717,28 +1676,25 @@ func (x *NSGroupState) GetError() string { // FullStatus contains the full state held by the Status instance type FullStatus struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ManagementState *ManagementState `protobuf:"bytes,1,opt,name=managementState,proto3" json:"managementState,omitempty"` - SignalState *SignalState `protobuf:"bytes,2,opt,name=signalState,proto3" json:"signalState,omitempty"` - LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"` - Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` - Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"` - DnsServers []*NSGroupState `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"` - NumberOfForwardingRules int32 `protobuf:"varint,8,opt,name=NumberOfForwardingRules,proto3" json:"NumberOfForwardingRules,omitempty"` - Events []*SystemEvent `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"` - LazyConnectionEnabled bool `protobuf:"varint,9,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ManagementState *ManagementState `protobuf:"bytes,1,opt,name=managementState,proto3" json:"managementState,omitempty"` + SignalState *SignalState `protobuf:"bytes,2,opt,name=signalState,proto3" json:"signalState,omitempty"` + LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"` + Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` + Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"` + DnsServers []*NSGroupState `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"` + NumberOfForwardingRules int32 `protobuf:"varint,8,opt,name=NumberOfForwardingRules,proto3" json:"NumberOfForwardingRules,omitempty"` + Events []*SystemEvent `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"` + LazyConnectionEnabled bool `protobuf:"varint,9,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FullStatus) Reset() { *x = FullStatus{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FullStatus) String() string { @@ -1749,7 +1705,7 @@ func (*FullStatus) ProtoMessage() {} func (x *FullStatus) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1829,18 +1785,16 @@ func (x *FullStatus) GetLazyConnectionEnabled() bool { // Networks type ListNetworksRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListNetworksRequest) Reset() { *x = ListNetworksRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListNetworksRequest) String() string { @@ -1851,7 +1805,7 @@ func (*ListNetworksRequest) ProtoMessage() {} func (x *ListNetworksRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1867,20 +1821,17 @@ func (*ListNetworksRequest) Descriptor() ([]byte, []int) { } type ListNetworksResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*Network `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` unknownFields protoimpl.UnknownFields - - Routes []*Network `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListNetworksResponse) Reset() { *x = ListNetworksResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListNetworksResponse) String() string { @@ -1891,7 +1842,7 @@ func (*ListNetworksResponse) ProtoMessage() {} func (x *ListNetworksResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1914,22 +1865,19 @@ func (x *ListNetworksResponse) GetRoutes() []*Network { } type SelectNetworksRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NetworkIDs []string `protobuf:"bytes,1,rep,name=networkIDs,proto3" json:"networkIDs,omitempty"` + Append bool `protobuf:"varint,2,opt,name=append,proto3" json:"append,omitempty"` + All bool `protobuf:"varint,3,opt,name=all,proto3" json:"all,omitempty"` unknownFields protoimpl.UnknownFields - - NetworkIDs []string `protobuf:"bytes,1,rep,name=networkIDs,proto3" json:"networkIDs,omitempty"` - Append bool `protobuf:"varint,2,opt,name=append,proto3" json:"append,omitempty"` - All bool `protobuf:"varint,3,opt,name=all,proto3" json:"all,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SelectNetworksRequest) Reset() { *x = SelectNetworksRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SelectNetworksRequest) String() string { @@ -1940,7 +1888,7 @@ func (*SelectNetworksRequest) ProtoMessage() {} func (x *SelectNetworksRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1977,18 +1925,16 @@ func (x *SelectNetworksRequest) GetAll() bool { } type SelectNetworksResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SelectNetworksResponse) Reset() { *x = SelectNetworksResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SelectNetworksResponse) String() string { @@ -1999,7 +1945,7 @@ func (*SelectNetworksResponse) ProtoMessage() {} func (x *SelectNetworksResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2015,20 +1961,17 @@ func (*SelectNetworksResponse) Descriptor() ([]byte, []int) { } type IPList struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ips []string `protobuf:"bytes,1,rep,name=ips,proto3" json:"ips,omitempty"` unknownFields protoimpl.UnknownFields - - Ips []string `protobuf:"bytes,1,rep,name=ips,proto3" json:"ips,omitempty"` + sizeCache protoimpl.SizeCache } func (x *IPList) Reset() { *x = IPList{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *IPList) String() string { @@ -2039,7 +1982,7 @@ func (*IPList) ProtoMessage() {} func (x *IPList) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2062,24 +2005,21 @@ func (x *IPList) GetIps() []string { } type Network struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"` + Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"` + Domains []string `protobuf:"bytes,4,rep,name=domains,proto3" json:"domains,omitempty"` + ResolvedIPs map[string]*IPList `protobuf:"bytes,5,rep,name=resolvedIPs,proto3" json:"resolvedIPs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` - Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"` - Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"` - Domains []string `protobuf:"bytes,4,rep,name=domains,proto3" json:"domains,omitempty"` - ResolvedIPs map[string]*IPList `protobuf:"bytes,5,rep,name=resolvedIPs,proto3" json:"resolvedIPs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *Network) Reset() { *x = Network{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Network) String() string { @@ -2090,7 +2030,7 @@ func (*Network) ProtoMessage() {} func (x *Network) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2142,24 +2082,21 @@ func (x *Network) GetResolvedIPs() map[string]*IPList { // ForwardingRules type PortInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to PortSelection: + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to PortSelection: // // *PortInfo_Port // *PortInfo_Range_ PortSelection isPortInfo_PortSelection `protobuf_oneof:"portSelection"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PortInfo) Reset() { *x = PortInfo{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PortInfo) String() string { @@ -2170,7 +2107,7 @@ func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[26] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2185,23 +2122,27 @@ func (*PortInfo) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{26} } -func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection { - if m != nil { - return m.PortSelection +func (x *PortInfo) GetPortSelection() isPortInfo_PortSelection { + if x != nil { + return x.PortSelection } return nil } func (x *PortInfo) GetPort() uint32 { - if x, ok := x.GetPortSelection().(*PortInfo_Port); ok { - return x.Port + if x != nil { + if x, ok := x.PortSelection.(*PortInfo_Port); ok { + return x.Port + } } return 0 } func (x *PortInfo) GetRange() *PortInfo_Range { - if x, ok := x.GetPortSelection().(*PortInfo_Range_); ok { - return x.Range + if x != nil { + if x, ok := x.PortSelection.(*PortInfo_Range_); ok { + return x.Range + } } return nil } @@ -2223,24 +2164,21 @@ func (*PortInfo_Port) isPortInfo_PortSelection() {} func (*PortInfo_Range_) isPortInfo_PortSelection() {} type ForwardingRule struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Protocol string `protobuf:"bytes,1,opt,name=protocol,proto3" json:"protocol,omitempty"` - DestinationPort *PortInfo `protobuf:"bytes,2,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"` - TranslatedAddress string `protobuf:"bytes,3,opt,name=translatedAddress,proto3" json:"translatedAddress,omitempty"` - TranslatedHostname string `protobuf:"bytes,4,opt,name=translatedHostname,proto3" json:"translatedHostname,omitempty"` - TranslatedPort *PortInfo `protobuf:"bytes,5,opt,name=translatedPort,proto3" json:"translatedPort,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Protocol string `protobuf:"bytes,1,opt,name=protocol,proto3" json:"protocol,omitempty"` + DestinationPort *PortInfo `protobuf:"bytes,2,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"` + TranslatedAddress string `protobuf:"bytes,3,opt,name=translatedAddress,proto3" json:"translatedAddress,omitempty"` + TranslatedHostname string `protobuf:"bytes,4,opt,name=translatedHostname,proto3" json:"translatedHostname,omitempty"` + TranslatedPort *PortInfo `protobuf:"bytes,5,opt,name=translatedPort,proto3" json:"translatedPort,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ForwardingRule) Reset() { *x = ForwardingRule{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ForwardingRule) String() string { @@ -2251,7 +2189,7 @@ func (*ForwardingRule) ProtoMessage() {} func (x *ForwardingRule) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[27] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2302,20 +2240,17 @@ func (x *ForwardingRule) GetTranslatedPort() *PortInfo { } type ForwardingRulesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*ForwardingRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` unknownFields protoimpl.UnknownFields - - Rules []*ForwardingRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ForwardingRulesResponse) Reset() { *x = ForwardingRulesResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ForwardingRulesResponse) String() string { @@ -2326,7 +2261,7 @@ func (*ForwardingRulesResponse) ProtoMessage() {} func (x *ForwardingRulesResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[28] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2350,23 +2285,20 @@ func (x *ForwardingRulesResponse) GetRules() []*ForwardingRule { // DebugBundler type DebugBundleRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Anonymize bool `protobuf:"varint,1,opt,name=anonymize,proto3" json:"anonymize,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + SystemInfo bool `protobuf:"varint,3,opt,name=systemInfo,proto3" json:"systemInfo,omitempty"` + UploadURL string `protobuf:"bytes,4,opt,name=uploadURL,proto3" json:"uploadURL,omitempty"` unknownFields protoimpl.UnknownFields - - Anonymize bool `protobuf:"varint,1,opt,name=anonymize,proto3" json:"anonymize,omitempty"` - Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` - SystemInfo bool `protobuf:"varint,3,opt,name=systemInfo,proto3" json:"systemInfo,omitempty"` - UploadURL string `protobuf:"bytes,4,opt,name=uploadURL,proto3" json:"uploadURL,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DebugBundleRequest) Reset() { *x = DebugBundleRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DebugBundleRequest) String() string { @@ -2377,7 +2309,7 @@ func (*DebugBundleRequest) ProtoMessage() {} func (x *DebugBundleRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[29] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2421,22 +2353,19 @@ func (x *DebugBundleRequest) GetUploadURL() string { } type DebugBundleResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - UploadedKey string `protobuf:"bytes,2,opt,name=uploadedKey,proto3" json:"uploadedKey,omitempty"` - UploadFailureReason string `protobuf:"bytes,3,opt,name=uploadFailureReason,proto3" json:"uploadFailureReason,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + UploadedKey string `protobuf:"bytes,2,opt,name=uploadedKey,proto3" json:"uploadedKey,omitempty"` + UploadFailureReason string `protobuf:"bytes,3,opt,name=uploadFailureReason,proto3" json:"uploadFailureReason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DebugBundleResponse) Reset() { *x = DebugBundleResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DebugBundleResponse) String() string { @@ -2447,7 +2376,7 @@ func (*DebugBundleResponse) ProtoMessage() {} func (x *DebugBundleResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[30] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2484,18 +2413,16 @@ func (x *DebugBundleResponse) GetUploadFailureReason() string { } type GetLogLevelRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetLogLevelRequest) Reset() { *x = GetLogLevelRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetLogLevelRequest) String() string { @@ -2506,7 +2433,7 @@ func (*GetLogLevelRequest) ProtoMessage() {} func (x *GetLogLevelRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[31] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2522,20 +2449,17 @@ func (*GetLogLevelRequest) Descriptor() ([]byte, []int) { } type GetLogLevelResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` unknownFields protoimpl.UnknownFields - - Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetLogLevelResponse) Reset() { *x = GetLogLevelResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetLogLevelResponse) String() string { @@ -2546,7 +2470,7 @@ func (*GetLogLevelResponse) ProtoMessage() {} func (x *GetLogLevelResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[32] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2569,20 +2493,17 @@ func (x *GetLogLevelResponse) GetLevel() LogLevel { } type SetLogLevelRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` unknownFields protoimpl.UnknownFields - - Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SetLogLevelRequest) Reset() { *x = SetLogLevelRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetLogLevelRequest) String() string { @@ -2593,7 +2514,7 @@ func (*SetLogLevelRequest) ProtoMessage() {} func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[33] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2616,18 +2537,16 @@ func (x *SetLogLevelRequest) GetLevel() LogLevel { } type SetLogLevelResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SetLogLevelResponse) Reset() { *x = SetLogLevelResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetLogLevelResponse) String() string { @@ -2638,7 +2557,7 @@ func (*SetLogLevelResponse) ProtoMessage() {} func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[34] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2655,20 +2574,17 @@ func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { // State represents a daemon state entry type State struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + sizeCache protoimpl.SizeCache } func (x *State) Reset() { *x = State{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *State) String() string { @@ -2679,7 +2595,7 @@ func (*State) ProtoMessage() {} func (x *State) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[35] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2703,18 +2619,16 @@ func (x *State) GetName() string { // ListStatesRequest is empty as it requires no parameters type ListStatesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListStatesRequest) Reset() { *x = ListStatesRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListStatesRequest) String() string { @@ -2725,7 +2639,7 @@ func (*ListStatesRequest) ProtoMessage() {} func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[36] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2742,20 +2656,17 @@ func (*ListStatesRequest) Descriptor() ([]byte, []int) { // ListStatesResponse contains a list of states type ListStatesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + States []*State `protobuf:"bytes,1,rep,name=states,proto3" json:"states,omitempty"` unknownFields protoimpl.UnknownFields - - States []*State `protobuf:"bytes,1,rep,name=states,proto3" json:"states,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListStatesResponse) Reset() { *x = ListStatesResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListStatesResponse) String() string { @@ -2766,7 +2677,7 @@ func (*ListStatesResponse) ProtoMessage() {} func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[37] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2790,21 +2701,18 @@ func (x *ListStatesResponse) GetStates() []*State { // CleanStateRequest for cleaning states type CleanStateRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StateName string `protobuf:"bytes,1,opt,name=state_name,json=stateName,proto3" json:"state_name,omitempty"` + All bool `protobuf:"varint,2,opt,name=all,proto3" json:"all,omitempty"` unknownFields protoimpl.UnknownFields - - StateName string `protobuf:"bytes,1,opt,name=state_name,json=stateName,proto3" json:"state_name,omitempty"` - All bool `protobuf:"varint,2,opt,name=all,proto3" json:"all,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CleanStateRequest) Reset() { *x = CleanStateRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CleanStateRequest) String() string { @@ -2815,7 +2723,7 @@ func (*CleanStateRequest) ProtoMessage() {} func (x *CleanStateRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[38] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2846,20 +2754,17 @@ func (x *CleanStateRequest) GetAll() bool { // CleanStateResponse contains the result of the clean operation type CleanStateResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CleanedStates int32 `protobuf:"varint,1,opt,name=cleaned_states,json=cleanedStates,proto3" json:"cleaned_states,omitempty"` unknownFields protoimpl.UnknownFields - - CleanedStates int32 `protobuf:"varint,1,opt,name=cleaned_states,json=cleanedStates,proto3" json:"cleaned_states,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CleanStateResponse) Reset() { *x = CleanStateResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CleanStateResponse) String() string { @@ -2870,7 +2775,7 @@ func (*CleanStateResponse) ProtoMessage() {} func (x *CleanStateResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[39] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2894,21 +2799,18 @@ func (x *CleanStateResponse) GetCleanedStates() int32 { // DeleteStateRequest for deleting states type DeleteStateRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StateName string `protobuf:"bytes,1,opt,name=state_name,json=stateName,proto3" json:"state_name,omitempty"` + All bool `protobuf:"varint,2,opt,name=all,proto3" json:"all,omitempty"` unknownFields protoimpl.UnknownFields - - StateName string `protobuf:"bytes,1,opt,name=state_name,json=stateName,proto3" json:"state_name,omitempty"` - All bool `protobuf:"varint,2,opt,name=all,proto3" json:"all,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteStateRequest) Reset() { *x = DeleteStateRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteStateRequest) String() string { @@ -2919,7 +2821,7 @@ func (*DeleteStateRequest) ProtoMessage() {} func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[40] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2950,20 +2852,17 @@ func (x *DeleteStateRequest) GetAll() bool { // DeleteStateResponse contains the result of the delete operation type DeleteStateResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + DeletedStates int32 `protobuf:"varint,1,opt,name=deleted_states,json=deletedStates,proto3" json:"deleted_states,omitempty"` unknownFields protoimpl.UnknownFields - - DeletedStates int32 `protobuf:"varint,1,opt,name=deleted_states,json=deletedStates,proto3" json:"deleted_states,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteStateResponse) Reset() { *x = DeleteStateResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteStateResponse) String() string { @@ -2974,7 +2873,7 @@ func (*DeleteStateResponse) ProtoMessage() {} func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[41] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2997,20 +2896,17 @@ func (x *DeleteStateResponse) GetDeletedStates() int32 { } type SetNetworkMapPersistenceRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` unknownFields protoimpl.UnknownFields - - Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SetNetworkMapPersistenceRequest) Reset() { *x = SetNetworkMapPersistenceRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetNetworkMapPersistenceRequest) String() string { @@ -3021,7 +2917,7 @@ func (*SetNetworkMapPersistenceRequest) ProtoMessage() {} func (x *SetNetworkMapPersistenceRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[42] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3044,18 +2940,16 @@ func (x *SetNetworkMapPersistenceRequest) GetEnabled() bool { } type SetNetworkMapPersistenceResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SetNetworkMapPersistenceResponse) Reset() { *x = SetNetworkMapPersistenceResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetNetworkMapPersistenceResponse) String() string { @@ -3066,7 +2960,7 @@ func (*SetNetworkMapPersistenceResponse) ProtoMessage() {} func (x *SetNetworkMapPersistenceResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[43] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3082,25 +2976,22 @@ func (*SetNetworkMapPersistenceResponse) Descriptor() ([]byte, []int) { } type TCPFlags struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Syn bool `protobuf:"varint,1,opt,name=syn,proto3" json:"syn,omitempty"` + Ack bool `protobuf:"varint,2,opt,name=ack,proto3" json:"ack,omitempty"` + Fin bool `protobuf:"varint,3,opt,name=fin,proto3" json:"fin,omitempty"` + Rst bool `protobuf:"varint,4,opt,name=rst,proto3" json:"rst,omitempty"` + Psh bool `protobuf:"varint,5,opt,name=psh,proto3" json:"psh,omitempty"` + Urg bool `protobuf:"varint,6,opt,name=urg,proto3" json:"urg,omitempty"` unknownFields protoimpl.UnknownFields - - Syn bool `protobuf:"varint,1,opt,name=syn,proto3" json:"syn,omitempty"` - Ack bool `protobuf:"varint,2,opt,name=ack,proto3" json:"ack,omitempty"` - Fin bool `protobuf:"varint,3,opt,name=fin,proto3" json:"fin,omitempty"` - Rst bool `protobuf:"varint,4,opt,name=rst,proto3" json:"rst,omitempty"` - Psh bool `protobuf:"varint,5,opt,name=psh,proto3" json:"psh,omitempty"` - Urg bool `protobuf:"varint,6,opt,name=urg,proto3" json:"urg,omitempty"` + sizeCache protoimpl.SizeCache } func (x *TCPFlags) Reset() { *x = TCPFlags{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *TCPFlags) String() string { @@ -3111,7 +3002,7 @@ func (*TCPFlags) ProtoMessage() {} func (x *TCPFlags) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[44] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3169,28 +3060,25 @@ func (x *TCPFlags) GetUrg() bool { } type TracePacketRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - SourceIp string `protobuf:"bytes,1,opt,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"` - DestinationIp string `protobuf:"bytes,2,opt,name=destination_ip,json=destinationIp,proto3" json:"destination_ip,omitempty"` - Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` - SourcePort uint32 `protobuf:"varint,4,opt,name=source_port,json=sourcePort,proto3" json:"source_port,omitempty"` - DestinationPort uint32 `protobuf:"varint,5,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"` - Direction string `protobuf:"bytes,6,opt,name=direction,proto3" json:"direction,omitempty"` - TcpFlags *TCPFlags `protobuf:"bytes,7,opt,name=tcp_flags,json=tcpFlags,proto3,oneof" json:"tcp_flags,omitempty"` - IcmpType *uint32 `protobuf:"varint,8,opt,name=icmp_type,json=icmpType,proto3,oneof" json:"icmp_type,omitempty"` - IcmpCode *uint32 `protobuf:"varint,9,opt,name=icmp_code,json=icmpCode,proto3,oneof" json:"icmp_code,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + SourceIp string `protobuf:"bytes,1,opt,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"` + DestinationIp string `protobuf:"bytes,2,opt,name=destination_ip,json=destinationIp,proto3" json:"destination_ip,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` + SourcePort uint32 `protobuf:"varint,4,opt,name=source_port,json=sourcePort,proto3" json:"source_port,omitempty"` + DestinationPort uint32 `protobuf:"varint,5,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"` + Direction string `protobuf:"bytes,6,opt,name=direction,proto3" json:"direction,omitempty"` + TcpFlags *TCPFlags `protobuf:"bytes,7,opt,name=tcp_flags,json=tcpFlags,proto3,oneof" json:"tcp_flags,omitempty"` + IcmpType *uint32 `protobuf:"varint,8,opt,name=icmp_type,json=icmpType,proto3,oneof" json:"icmp_type,omitempty"` + IcmpCode *uint32 `protobuf:"varint,9,opt,name=icmp_code,json=icmpCode,proto3,oneof" json:"icmp_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TracePacketRequest) Reset() { *x = TracePacketRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *TracePacketRequest) String() string { @@ -3201,7 +3089,7 @@ func (*TracePacketRequest) ProtoMessage() {} func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[45] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3280,23 +3168,20 @@ func (x *TracePacketRequest) GetIcmpCode() uint32 { } type TraceStage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - Allowed bool `protobuf:"varint,3,opt,name=allowed,proto3" json:"allowed,omitempty"` - ForwardingDetails *string `protobuf:"bytes,4,opt,name=forwarding_details,json=forwardingDetails,proto3,oneof" json:"forwarding_details,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Allowed bool `protobuf:"varint,3,opt,name=allowed,proto3" json:"allowed,omitempty"` + ForwardingDetails *string `protobuf:"bytes,4,opt,name=forwarding_details,json=forwardingDetails,proto3,oneof" json:"forwarding_details,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TraceStage) Reset() { *x = TraceStage{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *TraceStage) String() string { @@ -3307,7 +3192,7 @@ func (*TraceStage) ProtoMessage() {} func (x *TraceStage) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[46] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3351,21 +3236,18 @@ func (x *TraceStage) GetForwardingDetails() string { } type TracePacketResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Stages []*TraceStage `protobuf:"bytes,1,rep,name=stages,proto3" json:"stages,omitempty"` - FinalDisposition bool `protobuf:"varint,2,opt,name=final_disposition,json=finalDisposition,proto3" json:"final_disposition,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Stages []*TraceStage `protobuf:"bytes,1,rep,name=stages,proto3" json:"stages,omitempty"` + FinalDisposition bool `protobuf:"varint,2,opt,name=final_disposition,json=finalDisposition,proto3" json:"final_disposition,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TracePacketResponse) Reset() { *x = TracePacketResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *TracePacketResponse) String() string { @@ -3376,7 +3258,7 @@ func (*TracePacketResponse) ProtoMessage() {} func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[47] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3406,18 +3288,16 @@ func (x *TracePacketResponse) GetFinalDisposition() bool { } type SubscribeRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SubscribeRequest) Reset() { *x = SubscribeRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SubscribeRequest) String() string { @@ -3428,7 +3308,7 @@ func (*SubscribeRequest) ProtoMessage() {} func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[48] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3444,26 +3324,23 @@ func (*SubscribeRequest) Descriptor() ([]byte, []int) { } type SystemEvent struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Severity SystemEvent_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"` + Category SystemEvent_Category `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + UserMessage string `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Severity SystemEvent_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"` - Category SystemEvent_Category `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"` - Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` - UserMessage string `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"` - Timestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *SystemEvent) Reset() { *x = SystemEvent{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SystemEvent) String() string { @@ -3474,7 +3351,7 @@ func (*SystemEvent) ProtoMessage() {} func (x *SystemEvent) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[49] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3539,18 +3416,16 @@ func (x *SystemEvent) GetMetadata() map[string]string { } type GetEventsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetEventsRequest) Reset() { *x = GetEventsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetEventsRequest) String() string { @@ -3561,7 +3436,7 @@ func (*GetEventsRequest) ProtoMessage() {} func (x *GetEventsRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[50] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3577,20 +3452,17 @@ func (*GetEventsRequest) Descriptor() ([]byte, []int) { } type GetEventsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Events []*SystemEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` unknownFields protoimpl.UnknownFields - - Events []*SystemEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetEventsResponse) Reset() { *x = GetEventsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetEventsResponse) String() string { @@ -3601,7 +3473,7 @@ func (*GetEventsResponse) ProtoMessage() {} func (x *GetEventsResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[51] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3624,21 +3496,18 @@ func (x *GetEventsResponse) GetEvents() []*SystemEvent { } type PortInfo_Range struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` + End uint32 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` unknownFields protoimpl.UnknownFields - - Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` - End uint32 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[53] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_daemon_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PortInfo_Range) String() string { @@ -3649,7 +3518,7 @@ func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[53] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3680,648 +3549,350 @@ func (x *PortInfo_Range) GetEnd() uint32 { var File_daemon_proto protoreflect.FileDescriptor -var file_daemon_proto_rawDesc = []byte{ - 0x0a, 0x0c, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0e, 0x0a, 0x0c, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x0d, 0x0a, 0x0c, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, - 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, - 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, - 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, - 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, - 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, - 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, - 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x74, 0x45, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x63, 0x6c, 0x65, 0x61, - 0x6e, 0x4e, 0x41, 0x54, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x4e, 0x41, 0x54, 0x45, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x78, - 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, - 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, - 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0d, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, - 0x12, 0x29, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, - 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, - 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x14, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, - 0x4b, 0x65, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x14, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, - 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x04, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0f, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, - 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x13, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, - 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, - 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, - 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, - 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, - 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x48, 0x07, 0x52, 0x0e, 0x6e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, - 0x12, 0x4a, 0x0a, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x08, 0x52, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x48, 0x09, 0x52, 0x13, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x15, - 0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x24, - 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18, 0x16, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6e, - 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, - 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0c, - 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, - 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0d, - 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, - 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26, 0x0a, 0x0e, - 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1b, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x1c, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x0f, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, - 0x28, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x18, 0x1d, 0x20, 0x01, 0x28, 0x08, 0x48, 0x10, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, - 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, - 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, - 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, - 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, - 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, - 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, - 0x18, 0x0a, 0x16, 0x5f, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xb5, 0x01, 0x0a, 0x0d, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, - 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, - 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, - 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, - 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, - 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, - 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, - 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, - 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x06, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, - 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, - 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, - 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, - 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, - 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, - 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, - 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, - 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, - 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, - 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, - 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, - 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, - 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, - 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, - 0x1f, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18, 0x11, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6e, 0x73, - 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x13, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, - 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, - 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, - 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, - 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, - 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, - 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, - 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, - 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, - 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, - 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, - 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, - 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, - 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, - 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, - 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, - 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, - 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, - 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xef, 0x03, 0x0a, - 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, - 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, - 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, - 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, - 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, - 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, - 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x15, - 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, - 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, - 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, - 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, - 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, - 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, - 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, - 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, 0x01, 0x0a, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, - 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, - 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, - 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, - 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, - 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, - 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, - 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, - 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0e, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, - 0x6f, 0x72, 0x74, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, - 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x88, 0x01, 0x0a, - 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, - 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x22, 0x7d, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, - 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, - 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x13, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, - 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, - 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, - 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, - 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, - 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, - 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, - 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, - 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, - 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, - 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, - 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, - 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, - 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, - 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, - 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, - 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, - 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, - 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, - 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, - 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, - 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, - 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, - 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, - 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, - 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, - 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, - 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, - 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, - 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, - 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, - 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, - 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, - 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, - 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, - 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, - 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, - 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xb3, 0x0b, 0x0a, 0x0d, 0x44, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, - 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, - 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, - 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, - 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, - 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, - 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, - 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, - 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, - 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, - 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, - 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, -} +const file_daemon_proto_rawDesc = "" + + "\n" + + "\fdaemon.proto\x12\x06daemon\x1a google/protobuf/descriptor.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\x0e\n" + + "\fEmptyRequest\"\xbf\r\n" + + "\fLoginRequest\x12\x1a\n" + + "\bsetupKey\x18\x01 \x01(\tR\bsetupKey\x12&\n" + + "\fpreSharedKey\x18\x02 \x01(\tB\x02\x18\x01R\fpreSharedKey\x12$\n" + + "\rmanagementUrl\x18\x03 \x01(\tR\rmanagementUrl\x12\x1a\n" + + "\badminURL\x18\x04 \x01(\tR\badminURL\x12&\n" + + "\x0enatExternalIPs\x18\x05 \x03(\tR\x0enatExternalIPs\x120\n" + + "\x13cleanNATExternalIPs\x18\x06 \x01(\bR\x13cleanNATExternalIPs\x12*\n" + + "\x10customDNSAddress\x18\a \x01(\fR\x10customDNSAddress\x120\n" + + "\x13isUnixDesktopClient\x18\b \x01(\bR\x13isUnixDesktopClient\x12\x1a\n" + + "\bhostname\x18\t \x01(\tR\bhostname\x12/\n" + + "\x10rosenpassEnabled\x18\n" + + " \x01(\bH\x00R\x10rosenpassEnabled\x88\x01\x01\x12)\n" + + "\rinterfaceName\x18\v \x01(\tH\x01R\rinterfaceName\x88\x01\x01\x12)\n" + + "\rwireguardPort\x18\f \x01(\x03H\x02R\rwireguardPort\x88\x01\x01\x127\n" + + "\x14optionalPreSharedKey\x18\r \x01(\tH\x03R\x14optionalPreSharedKey\x88\x01\x01\x123\n" + + "\x12disableAutoConnect\x18\x0e \x01(\bH\x04R\x12disableAutoConnect\x88\x01\x01\x12/\n" + + "\x10serverSSHAllowed\x18\x0f \x01(\bH\x05R\x10serverSSHAllowed\x88\x01\x01\x125\n" + + "\x13rosenpassPermissive\x18\x10 \x01(\bH\x06R\x13rosenpassPermissive\x88\x01\x01\x120\n" + + "\x13extraIFaceBlacklist\x18\x11 \x03(\tR\x13extraIFaceBlacklist\x12+\n" + + "\x0enetworkMonitor\x18\x12 \x01(\bH\aR\x0enetworkMonitor\x88\x01\x01\x12J\n" + + "\x10dnsRouteInterval\x18\x13 \x01(\v2\x19.google.protobuf.DurationH\bR\x10dnsRouteInterval\x88\x01\x01\x127\n" + + "\x15disable_client_routes\x18\x14 \x01(\bH\tR\x13disableClientRoutes\x88\x01\x01\x127\n" + + "\x15disable_server_routes\x18\x15 \x01(\bH\n" + + "R\x13disableServerRoutes\x88\x01\x01\x12$\n" + + "\vdisable_dns\x18\x16 \x01(\bH\vR\n" + + "disableDns\x88\x01\x01\x12.\n" + + "\x10disable_firewall\x18\x17 \x01(\bH\fR\x0fdisableFirewall\x88\x01\x01\x12-\n" + + "\x10block_lan_access\x18\x18 \x01(\bH\rR\x0eblockLanAccess\x88\x01\x01\x128\n" + + "\x15disable_notifications\x18\x19 \x01(\bH\x0eR\x14disableNotifications\x88\x01\x01\x12\x1d\n" + + "\n" + + "dns_labels\x18\x1a \x03(\tR\tdnsLabels\x12&\n" + + "\x0ecleanDNSLabels\x18\x1b \x01(\bR\x0ecleanDNSLabels\x129\n" + + "\x15lazyConnectionEnabled\x18\x1c \x01(\bH\x0fR\x15lazyConnectionEnabled\x88\x01\x01\x12(\n" + + "\rblock_inbound\x18\x1d \x01(\bH\x10R\fblockInbound\x88\x01\x01B\x13\n" + + "\x11_rosenpassEnabledB\x10\n" + + "\x0e_interfaceNameB\x10\n" + + "\x0e_wireguardPortB\x17\n" + + "\x15_optionalPreSharedKeyB\x15\n" + + "\x13_disableAutoConnectB\x13\n" + + "\x11_serverSSHAllowedB\x16\n" + + "\x14_rosenpassPermissiveB\x11\n" + + "\x0f_networkMonitorB\x13\n" + + "\x11_dnsRouteIntervalB\x18\n" + + "\x16_disable_client_routesB\x18\n" + + "\x16_disable_server_routesB\x0e\n" + + "\f_disable_dnsB\x13\n" + + "\x11_disable_firewallB\x13\n" + + "\x11_block_lan_accessB\x18\n" + + "\x16_disable_notificationsB\x18\n" + + "\x16_lazyConnectionEnabledB\x10\n" + + "\x0e_block_inbound\"\xb5\x01\n" + + "\rLoginResponse\x12$\n" + + "\rneedsSSOLogin\x18\x01 \x01(\bR\rneedsSSOLogin\x12\x1a\n" + + "\buserCode\x18\x02 \x01(\tR\buserCode\x12(\n" + + "\x0fverificationURI\x18\x03 \x01(\tR\x0fverificationURI\x128\n" + + "\x17verificationURIComplete\x18\x04 \x01(\tR\x17verificationURIComplete\"M\n" + + "\x13WaitSSOLoginRequest\x12\x1a\n" + + "\buserCode\x18\x01 \x01(\tR\buserCode\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\"\x16\n" + + "\x14WaitSSOLoginResponse\"\v\n" + + "\tUpRequest\"\f\n" + + "\n" + + "UpResponse\"g\n" + + "\rStatusRequest\x12,\n" + + "\x11getFullPeerStatus\x18\x01 \x01(\bR\x11getFullPeerStatus\x12(\n" + + "\x0fshouldRunProbes\x18\x02 \x01(\bR\x0fshouldRunProbes\"\x82\x01\n" + + "\x0eStatusResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x122\n" + + "\n" + + "fullStatus\x18\x02 \x01(\v2\x12.daemon.FullStatusR\n" + + "fullStatus\x12$\n" + + "\rdaemonVersion\x18\x03 \x01(\tR\rdaemonVersion\"\r\n" + + "\vDownRequest\"\x0e\n" + + "\fDownResponse\"\x12\n" + + "\x10GetConfigRequest\"\xa3\x06\n" + + "\x11GetConfigResponse\x12$\n" + + "\rmanagementUrl\x18\x01 \x01(\tR\rmanagementUrl\x12\x1e\n" + + "\n" + + "configFile\x18\x02 \x01(\tR\n" + + "configFile\x12\x18\n" + + "\alogFile\x18\x03 \x01(\tR\alogFile\x12\"\n" + + "\fpreSharedKey\x18\x04 \x01(\tR\fpreSharedKey\x12\x1a\n" + + "\badminURL\x18\x05 \x01(\tR\badminURL\x12$\n" + + "\rinterfaceName\x18\x06 \x01(\tR\rinterfaceName\x12$\n" + + "\rwireguardPort\x18\a \x01(\x03R\rwireguardPort\x12.\n" + + "\x12disableAutoConnect\x18\t \x01(\bR\x12disableAutoConnect\x12*\n" + + "\x10serverSSHAllowed\x18\n" + + " \x01(\bR\x10serverSSHAllowed\x12*\n" + + "\x10rosenpassEnabled\x18\v \x01(\bR\x10rosenpassEnabled\x120\n" + + "\x13rosenpassPermissive\x18\f \x01(\bR\x13rosenpassPermissive\x123\n" + + "\x15disable_notifications\x18\r \x01(\bR\x14disableNotifications\x124\n" + + "\x15lazyConnectionEnabled\x18\x0e \x01(\bR\x15lazyConnectionEnabled\x12\"\n" + + "\fblockInbound\x18\x0f \x01(\bR\fblockInbound\x12&\n" + + "\x0enetworkMonitor\x18\x10 \x01(\bR\x0enetworkMonitor\x12\x1f\n" + + "\vdisable_dns\x18\x11 \x01(\bR\n" + + "disableDns\x122\n" + + "\x15disable_client_routes\x18\x12 \x01(\bR\x13disableClientRoutes\x122\n" + + "\x15disable_server_routes\x18\x13 \x01(\bR\x13disableServerRoutes\x12(\n" + + "\x10block_lan_access\x18\x14 \x01(\bR\x0eblockLanAccess\"\xde\x05\n" + + "\tPeerState\x12\x0e\n" + + "\x02IP\x18\x01 \x01(\tR\x02IP\x12\x16\n" + + "\x06pubKey\x18\x02 \x01(\tR\x06pubKey\x12\x1e\n" + + "\n" + + "connStatus\x18\x03 \x01(\tR\n" + + "connStatus\x12F\n" + + "\x10connStatusUpdate\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x10connStatusUpdate\x12\x18\n" + + "\arelayed\x18\x05 \x01(\bR\arelayed\x124\n" + + "\x15localIceCandidateType\x18\a \x01(\tR\x15localIceCandidateType\x126\n" + + "\x16remoteIceCandidateType\x18\b \x01(\tR\x16remoteIceCandidateType\x12\x12\n" + + "\x04fqdn\x18\t \x01(\tR\x04fqdn\x12<\n" + + "\x19localIceCandidateEndpoint\x18\n" + + " \x01(\tR\x19localIceCandidateEndpoint\x12>\n" + + "\x1aremoteIceCandidateEndpoint\x18\v \x01(\tR\x1aremoteIceCandidateEndpoint\x12R\n" + + "\x16lastWireguardHandshake\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\x16lastWireguardHandshake\x12\x18\n" + + "\abytesRx\x18\r \x01(\x03R\abytesRx\x12\x18\n" + + "\abytesTx\x18\x0e \x01(\x03R\abytesTx\x12*\n" + + "\x10rosenpassEnabled\x18\x0f \x01(\bR\x10rosenpassEnabled\x12\x1a\n" + + "\bnetworks\x18\x10 \x03(\tR\bnetworks\x123\n" + + "\alatency\x18\x11 \x01(\v2\x19.google.protobuf.DurationR\alatency\x12\"\n" + + "\frelayAddress\x18\x12 \x01(\tR\frelayAddress\"\xf0\x01\n" + + "\x0eLocalPeerState\x12\x0e\n" + + "\x02IP\x18\x01 \x01(\tR\x02IP\x12\x16\n" + + "\x06pubKey\x18\x02 \x01(\tR\x06pubKey\x12(\n" + + "\x0fkernelInterface\x18\x03 \x01(\bR\x0fkernelInterface\x12\x12\n" + + "\x04fqdn\x18\x04 \x01(\tR\x04fqdn\x12*\n" + + "\x10rosenpassEnabled\x18\x05 \x01(\bR\x10rosenpassEnabled\x120\n" + + "\x13rosenpassPermissive\x18\x06 \x01(\bR\x13rosenpassPermissive\x12\x1a\n" + + "\bnetworks\x18\a \x03(\tR\bnetworks\"S\n" + + "\vSignalState\x12\x10\n" + + "\x03URL\x18\x01 \x01(\tR\x03URL\x12\x1c\n" + + "\tconnected\x18\x02 \x01(\bR\tconnected\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"W\n" + + "\x0fManagementState\x12\x10\n" + + "\x03URL\x18\x01 \x01(\tR\x03URL\x12\x1c\n" + + "\tconnected\x18\x02 \x01(\bR\tconnected\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"R\n" + + "\n" + + "RelayState\x12\x10\n" + + "\x03URI\x18\x01 \x01(\tR\x03URI\x12\x1c\n" + + "\tavailable\x18\x02 \x01(\bR\tavailable\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"r\n" + + "\fNSGroupState\x12\x18\n" + + "\aservers\x18\x01 \x03(\tR\aservers\x12\x18\n" + + "\adomains\x18\x02 \x03(\tR\adomains\x12\x18\n" + + "\aenabled\x18\x03 \x01(\bR\aenabled\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\"\xef\x03\n" + + "\n" + + "FullStatus\x12A\n" + + "\x0fmanagementState\x18\x01 \x01(\v2\x17.daemon.ManagementStateR\x0fmanagementState\x125\n" + + "\vsignalState\x18\x02 \x01(\v2\x13.daemon.SignalStateR\vsignalState\x12>\n" + + "\x0elocalPeerState\x18\x03 \x01(\v2\x16.daemon.LocalPeerStateR\x0elocalPeerState\x12'\n" + + "\x05peers\x18\x04 \x03(\v2\x11.daemon.PeerStateR\x05peers\x12*\n" + + "\x06relays\x18\x05 \x03(\v2\x12.daemon.RelayStateR\x06relays\x125\n" + + "\vdns_servers\x18\x06 \x03(\v2\x14.daemon.NSGroupStateR\n" + + "dnsServers\x128\n" + + "\x17NumberOfForwardingRules\x18\b \x01(\x05R\x17NumberOfForwardingRules\x12+\n" + + "\x06events\x18\a \x03(\v2\x13.daemon.SystemEventR\x06events\x124\n" + + "\x15lazyConnectionEnabled\x18\t \x01(\bR\x15lazyConnectionEnabled\"\x15\n" + + "\x13ListNetworksRequest\"?\n" + + "\x14ListNetworksResponse\x12'\n" + + "\x06routes\x18\x01 \x03(\v2\x0f.daemon.NetworkR\x06routes\"a\n" + + "\x15SelectNetworksRequest\x12\x1e\n" + + "\n" + + "networkIDs\x18\x01 \x03(\tR\n" + + "networkIDs\x12\x16\n" + + "\x06append\x18\x02 \x01(\bR\x06append\x12\x10\n" + + "\x03all\x18\x03 \x01(\bR\x03all\"\x18\n" + + "\x16SelectNetworksResponse\"\x1a\n" + + "\x06IPList\x12\x10\n" + + "\x03ips\x18\x01 \x03(\tR\x03ips\"\xf9\x01\n" + + "\aNetwork\x12\x0e\n" + + "\x02ID\x18\x01 \x01(\tR\x02ID\x12\x14\n" + + "\x05range\x18\x02 \x01(\tR\x05range\x12\x1a\n" + + "\bselected\x18\x03 \x01(\bR\bselected\x12\x18\n" + + "\adomains\x18\x04 \x03(\tR\adomains\x12B\n" + + "\vresolvedIPs\x18\x05 \x03(\v2 .daemon.Network.ResolvedIPsEntryR\vresolvedIPs\x1aN\n" + + "\x10ResolvedIPsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12$\n" + + "\x05value\x18\x02 \x01(\v2\x0e.daemon.IPListR\x05value:\x028\x01\"\x92\x01\n" + + "\bPortInfo\x12\x14\n" + + "\x04port\x18\x01 \x01(\rH\x00R\x04port\x12.\n" + + "\x05range\x18\x02 \x01(\v2\x16.daemon.PortInfo.RangeH\x00R\x05range\x1a/\n" + + "\x05Range\x12\x14\n" + + "\x05start\x18\x01 \x01(\rR\x05start\x12\x10\n" + + "\x03end\x18\x02 \x01(\rR\x03endB\x0f\n" + + "\rportSelection\"\x80\x02\n" + + "\x0eForwardingRule\x12\x1a\n" + + "\bprotocol\x18\x01 \x01(\tR\bprotocol\x12:\n" + + "\x0fdestinationPort\x18\x02 \x01(\v2\x10.daemon.PortInfoR\x0fdestinationPort\x12,\n" + + "\x11translatedAddress\x18\x03 \x01(\tR\x11translatedAddress\x12.\n" + + "\x12translatedHostname\x18\x04 \x01(\tR\x12translatedHostname\x128\n" + + "\x0etranslatedPort\x18\x05 \x01(\v2\x10.daemon.PortInfoR\x0etranslatedPort\"G\n" + + "\x17ForwardingRulesResponse\x12,\n" + + "\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\x88\x01\n" + + "\x12DebugBundleRequest\x12\x1c\n" + + "\tanonymize\x18\x01 \x01(\bR\tanonymize\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1e\n" + + "\n" + + "systemInfo\x18\x03 \x01(\bR\n" + + "systemInfo\x12\x1c\n" + + "\tuploadURL\x18\x04 \x01(\tR\tuploadURL\"}\n" + + "\x13DebugBundleResponse\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12 \n" + + "\vuploadedKey\x18\x02 \x01(\tR\vuploadedKey\x120\n" + + "\x13uploadFailureReason\x18\x03 \x01(\tR\x13uploadFailureReason\"\x14\n" + + "\x12GetLogLevelRequest\"=\n" + + "\x13GetLogLevelResponse\x12&\n" + + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"<\n" + + "\x12SetLogLevelRequest\x12&\n" + + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"\x15\n" + + "\x13SetLogLevelResponse\"\x1b\n" + + "\x05State\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x13\n" + + "\x11ListStatesRequest\";\n" + + "\x12ListStatesResponse\x12%\n" + + "\x06states\x18\x01 \x03(\v2\r.daemon.StateR\x06states\"D\n" + + "\x11CleanStateRequest\x12\x1d\n" + + "\n" + + "state_name\x18\x01 \x01(\tR\tstateName\x12\x10\n" + + "\x03all\x18\x02 \x01(\bR\x03all\";\n" + + "\x12CleanStateResponse\x12%\n" + + "\x0ecleaned_states\x18\x01 \x01(\x05R\rcleanedStates\"E\n" + + "\x12DeleteStateRequest\x12\x1d\n" + + "\n" + + "state_name\x18\x01 \x01(\tR\tstateName\x12\x10\n" + + "\x03all\x18\x02 \x01(\bR\x03all\"<\n" + + "\x13DeleteStateResponse\x12%\n" + + "\x0edeleted_states\x18\x01 \x01(\x05R\rdeletedStates\";\n" + + "\x1fSetNetworkMapPersistenceRequest\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\"\"\n" + + " SetNetworkMapPersistenceResponse\"v\n" + + "\bTCPFlags\x12\x10\n" + + "\x03syn\x18\x01 \x01(\bR\x03syn\x12\x10\n" + + "\x03ack\x18\x02 \x01(\bR\x03ack\x12\x10\n" + + "\x03fin\x18\x03 \x01(\bR\x03fin\x12\x10\n" + + "\x03rst\x18\x04 \x01(\bR\x03rst\x12\x10\n" + + "\x03psh\x18\x05 \x01(\bR\x03psh\x12\x10\n" + + "\x03urg\x18\x06 \x01(\bR\x03urg\"\x80\x03\n" + + "\x12TracePacketRequest\x12\x1b\n" + + "\tsource_ip\x18\x01 \x01(\tR\bsourceIp\x12%\n" + + "\x0edestination_ip\x18\x02 \x01(\tR\rdestinationIp\x12\x1a\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\x12\x1f\n" + + "\vsource_port\x18\x04 \x01(\rR\n" + + "sourcePort\x12)\n" + + "\x10destination_port\x18\x05 \x01(\rR\x0fdestinationPort\x12\x1c\n" + + "\tdirection\x18\x06 \x01(\tR\tdirection\x122\n" + + "\ttcp_flags\x18\a \x01(\v2\x10.daemon.TCPFlagsH\x00R\btcpFlags\x88\x01\x01\x12 \n" + + "\ticmp_type\x18\b \x01(\rH\x01R\bicmpType\x88\x01\x01\x12 \n" + + "\ticmp_code\x18\t \x01(\rH\x02R\bicmpCode\x88\x01\x01B\f\n" + + "\n" + + "_tcp_flagsB\f\n" + + "\n" + + "_icmp_typeB\f\n" + + "\n" + + "_icmp_code\"\x9f\x01\n" + + "\n" + + "TraceStage\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x18\n" + + "\aallowed\x18\x03 \x01(\bR\aallowed\x122\n" + + "\x12forwarding_details\x18\x04 \x01(\tH\x00R\x11forwardingDetails\x88\x01\x01B\x15\n" + + "\x13_forwarding_details\"n\n" + + "\x13TracePacketResponse\x12*\n" + + "\x06stages\x18\x01 \x03(\v2\x12.daemon.TraceStageR\x06stages\x12+\n" + + "\x11final_disposition\x18\x02 \x01(\bR\x10finalDisposition\"\x12\n" + + "\x10SubscribeRequest\"\x93\x04\n" + + "\vSystemEvent\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x128\n" + + "\bseverity\x18\x02 \x01(\x0e2\x1c.daemon.SystemEvent.SeverityR\bseverity\x128\n" + + "\bcategory\x18\x03 \x01(\x0e2\x1c.daemon.SystemEvent.CategoryR\bcategory\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12 \n" + + "\vuserMessage\x18\x05 \x01(\tR\vuserMessage\x128\n" + + "\ttimestamp\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12=\n" + + "\bmetadata\x18\a \x03(\v2!.daemon.SystemEvent.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\":\n" + + "\bSeverity\x12\b\n" + + "\x04INFO\x10\x00\x12\v\n" + + "\aWARNING\x10\x01\x12\t\n" + + "\x05ERROR\x10\x02\x12\f\n" + + "\bCRITICAL\x10\x03\"R\n" + + "\bCategory\x12\v\n" + + "\aNETWORK\x10\x00\x12\a\n" + + "\x03DNS\x10\x01\x12\x12\n" + + "\x0eAUTHENTICATION\x10\x02\x12\x10\n" + + "\fCONNECTIVITY\x10\x03\x12\n" + + "\n" + + "\x06SYSTEM\x10\x04\"\x12\n" + + "\x10GetEventsRequest\"@\n" + + "\x11GetEventsResponse\x12+\n" + + "\x06events\x18\x01 \x03(\v2\x13.daemon.SystemEventR\x06events*b\n" + + "\bLogLevel\x12\v\n" + + "\aUNKNOWN\x10\x00\x12\t\n" + + "\x05PANIC\x10\x01\x12\t\n" + + "\x05FATAL\x10\x02\x12\t\n" + + "\x05ERROR\x10\x03\x12\b\n" + + "\x04WARN\x10\x04\x12\b\n" + + "\x04INFO\x10\x05\x12\t\n" + + "\x05DEBUG\x10\x06\x12\t\n" + + "\x05TRACE\x10\a2\xb3\v\n" + + "\rDaemonService\x126\n" + + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + + "\x02Up\x12\x11.daemon.UpRequest\x1a\x12.daemon.UpResponse\"\x00\x129\n" + + "\x06Status\x12\x15.daemon.StatusRequest\x1a\x16.daemon.StatusResponse\"\x00\x123\n" + + "\x04Down\x12\x13.daemon.DownRequest\x1a\x14.daemon.DownResponse\"\x00\x12B\n" + + "\tGetConfig\x12\x18.daemon.GetConfigRequest\x1a\x19.daemon.GetConfigResponse\"\x00\x12K\n" + + "\fListNetworks\x12\x1b.daemon.ListNetworksRequest\x1a\x1c.daemon.ListNetworksResponse\"\x00\x12Q\n" + + "\x0eSelectNetworks\x12\x1d.daemon.SelectNetworksRequest\x1a\x1e.daemon.SelectNetworksResponse\"\x00\x12S\n" + + "\x10DeselectNetworks\x12\x1d.daemon.SelectNetworksRequest\x1a\x1e.daemon.SelectNetworksResponse\"\x00\x12J\n" + + "\x0fForwardingRules\x12\x14.daemon.EmptyRequest\x1a\x1f.daemon.ForwardingRulesResponse\"\x00\x12H\n" + + "\vDebugBundle\x12\x1a.daemon.DebugBundleRequest\x1a\x1b.daemon.DebugBundleResponse\"\x00\x12H\n" + + "\vGetLogLevel\x12\x1a.daemon.GetLogLevelRequest\x1a\x1b.daemon.GetLogLevelResponse\"\x00\x12H\n" + + "\vSetLogLevel\x12\x1a.daemon.SetLogLevelRequest\x1a\x1b.daemon.SetLogLevelResponse\"\x00\x12E\n" + + "\n" + + "ListStates\x12\x19.daemon.ListStatesRequest\x1a\x1a.daemon.ListStatesResponse\"\x00\x12E\n" + + "\n" + + "CleanState\x12\x19.daemon.CleanStateRequest\x1a\x1a.daemon.CleanStateResponse\"\x00\x12H\n" + + "\vDeleteState\x12\x1a.daemon.DeleteStateRequest\x1a\x1b.daemon.DeleteStateResponse\"\x00\x12o\n" + + "\x18SetNetworkMapPersistence\x12'.daemon.SetNetworkMapPersistenceRequest\x1a(.daemon.SetNetworkMapPersistenceResponse\"\x00\x12H\n" + + "\vTracePacket\x12\x1a.daemon.TracePacketRequest\x1a\x1b.daemon.TracePacketResponse\"\x00\x12D\n" + + "\x0fSubscribeEvents\x12\x18.daemon.SubscribeRequest\x1a\x13.daemon.SystemEvent\"\x000\x01\x12B\n" + + "\tGetEvents\x12\x18.daemon.GetEventsRequest\x1a\x19.daemon.GetEventsResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once - file_daemon_proto_rawDescData = file_daemon_proto_rawDesc + file_daemon_proto_rawDescData []byte ) func file_daemon_proto_rawDescGZIP() []byte { file_daemon_proto_rawDescOnce.Do(func() { - file_daemon_proto_rawDescData = protoimpl.X.CompressGZIP(file_daemon_proto_rawDescData) + file_daemon_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc))) }) return file_daemon_proto_rawDescData } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 55) -var file_daemon_proto_goTypes = []interface{}{ +var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity (SystemEvent_Category)(0), // 2: daemon.SystemEvent.Category @@ -4465,656 +4036,18 @@ func file_daemon_proto_init() { if File_daemon_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_daemon_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EmptyRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WaitSSOLoginRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WaitSSOLoginResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StatusRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StatusResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DownRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DownResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetConfigRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetConfigResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LocalPeerState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SignalState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ManagementState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RelayState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NSGroupState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FullStatus); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListNetworksRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListNetworksResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SelectNetworksRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SelectNetworksResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IPList); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Network); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingRule); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingRulesResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DebugBundleRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DebugBundleResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetLogLevelRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetLogLevelResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetLogLevelRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetLogLevelResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*State); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListStatesRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListStatesResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CleanStateRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CleanStateResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteStateRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteStateResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetNetworkMapPersistenceRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetNetworkMapPersistenceResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TCPFlags); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TracePacketRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TraceStage); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TracePacketResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SubscribeRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SystemEvent); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetEventsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetEventsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_daemon_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo_Range); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_daemon_proto_msgTypes[1].OneofWrappers = []interface{}{} - file_daemon_proto_msgTypes[26].OneofWrappers = []interface{}{ + file_daemon_proto_msgTypes[1].OneofWrappers = []any{} + file_daemon_proto_msgTypes[26].OneofWrappers = []any{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } - file_daemon_proto_msgTypes[45].OneofWrappers = []interface{}{} - file_daemon_proto_msgTypes[46].OneofWrappers = []interface{}{} + file_daemon_proto_msgTypes[45].OneofWrappers = []any{} + file_daemon_proto_msgTypes[46].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_daemon_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 3, NumMessages: 55, NumExtensions: 0, @@ -5126,7 +4059,6 @@ func file_daemon_proto_init() { MessageInfos: file_daemon_proto_msgTypes, }.Build() File_daemon_proto = out.File - file_daemon_proto_rawDesc = nil file_daemon_proto_goTypes = nil file_daemon_proto_depIdxs = nil } diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 2a1a49d1b..f488e69e7 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -158,6 +158,7 @@ message UpResponse {} message StatusRequest{ bool getFullPeerStatus = 1; + bool shouldRunProbes = 2; } message StatusResponse{ diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 6251f7c52..e0612a6d1 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: daemon.proto package proto @@ -11,8 +15,31 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DaemonService_Login_FullMethodName = "/daemon.DaemonService/Login" + DaemonService_WaitSSOLogin_FullMethodName = "/daemon.DaemonService/WaitSSOLogin" + DaemonService_Up_FullMethodName = "/daemon.DaemonService/Up" + DaemonService_Status_FullMethodName = "/daemon.DaemonService/Status" + DaemonService_Down_FullMethodName = "/daemon.DaemonService/Down" + DaemonService_GetConfig_FullMethodName = "/daemon.DaemonService/GetConfig" + DaemonService_ListNetworks_FullMethodName = "/daemon.DaemonService/ListNetworks" + DaemonService_SelectNetworks_FullMethodName = "/daemon.DaemonService/SelectNetworks" + DaemonService_DeselectNetworks_FullMethodName = "/daemon.DaemonService/DeselectNetworks" + DaemonService_ForwardingRules_FullMethodName = "/daemon.DaemonService/ForwardingRules" + DaemonService_DebugBundle_FullMethodName = "/daemon.DaemonService/DebugBundle" + DaemonService_GetLogLevel_FullMethodName = "/daemon.DaemonService/GetLogLevel" + DaemonService_SetLogLevel_FullMethodName = "/daemon.DaemonService/SetLogLevel" + DaemonService_ListStates_FullMethodName = "/daemon.DaemonService/ListStates" + DaemonService_CleanState_FullMethodName = "/daemon.DaemonService/CleanState" + DaemonService_DeleteState_FullMethodName = "/daemon.DaemonService/DeleteState" + DaemonService_SetNetworkMapPersistence_FullMethodName = "/daemon.DaemonService/SetNetworkMapPersistence" + DaemonService_TracePacket_FullMethodName = "/daemon.DaemonService/TracePacket" + DaemonService_SubscribeEvents_FullMethodName = "/daemon.DaemonService/SubscribeEvents" + DaemonService_GetEvents_FullMethodName = "/daemon.DaemonService/GetEvents" +) // DaemonServiceClient is the client API for DaemonService service. // @@ -53,7 +80,7 @@ type DaemonServiceClient interface { // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) - SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) + SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SystemEvent], error) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) } @@ -66,8 +93,9 @@ func NewDaemonServiceClient(cc grpc.ClientConnInterface) DaemonServiceClient { } func (c *daemonServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(LoginResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/Login", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_Login_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -75,8 +103,9 @@ func (c *daemonServiceClient) Login(ctx context.Context, in *LoginRequest, opts } func (c *daemonServiceClient) WaitSSOLogin(ctx context.Context, in *WaitSSOLoginRequest, opts ...grpc.CallOption) (*WaitSSOLoginResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(WaitSSOLoginResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/WaitSSOLogin", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_WaitSSOLogin_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -84,8 +113,9 @@ func (c *daemonServiceClient) WaitSSOLogin(ctx context.Context, in *WaitSSOLogin } func (c *daemonServiceClient) Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UpResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/Up", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_Up_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -93,8 +123,9 @@ func (c *daemonServiceClient) Up(ctx context.Context, in *UpRequest, opts ...grp } func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StatusResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/Status", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_Status_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -102,8 +133,9 @@ func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opt } func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DownResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/Down", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_Down_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -111,8 +143,9 @@ func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts .. } func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetConfigResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetConfig", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_GetConfig_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -120,8 +153,9 @@ func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigReques } func (c *daemonServiceClient) ListNetworks(ctx context.Context, in *ListNetworksRequest, opts ...grpc.CallOption) (*ListNetworksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListNetworksResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListNetworks", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_ListNetworks_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -129,8 +163,9 @@ func (c *daemonServiceClient) ListNetworks(ctx context.Context, in *ListNetworks } func (c *daemonServiceClient) SelectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SelectNetworksResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/SelectNetworks", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_SelectNetworks_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -138,8 +173,9 @@ func (c *daemonServiceClient) SelectNetworks(ctx context.Context, in *SelectNetw } func (c *daemonServiceClient) DeselectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SelectNetworksResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeselectNetworks", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_DeselectNetworks_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -147,8 +183,9 @@ func (c *daemonServiceClient) DeselectNetworks(ctx context.Context, in *SelectNe } func (c *daemonServiceClient) ForwardingRules(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*ForwardingRulesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ForwardingRulesResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/ForwardingRules", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_ForwardingRules_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -156,8 +193,9 @@ func (c *daemonServiceClient) ForwardingRules(ctx context.Context, in *EmptyRequ } func (c *daemonServiceClient) DebugBundle(ctx context.Context, in *DebugBundleRequest, opts ...grpc.CallOption) (*DebugBundleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DebugBundleResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/DebugBundle", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_DebugBundle_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -165,8 +203,9 @@ func (c *daemonServiceClient) DebugBundle(ctx context.Context, in *DebugBundleRe } func (c *daemonServiceClient) GetLogLevel(ctx context.Context, in *GetLogLevelRequest, opts ...grpc.CallOption) (*GetLogLevelResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetLogLevelResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetLogLevel", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_GetLogLevel_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -174,8 +213,9 @@ func (c *daemonServiceClient) GetLogLevel(ctx context.Context, in *GetLogLevelRe } func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRequest, opts ...grpc.CallOption) (*SetLogLevelResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SetLogLevelResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetLogLevel", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_SetLogLevel_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -183,8 +223,9 @@ func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRe } func (c *daemonServiceClient) ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListStatesResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListStates", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_ListStates_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -192,8 +233,9 @@ func (c *daemonServiceClient) ListStates(ctx context.Context, in *ListStatesRequ } func (c *daemonServiceClient) CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CleanStateResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/CleanState", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_CleanState_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -201,8 +243,9 @@ func (c *daemonServiceClient) CleanState(ctx context.Context, in *CleanStateRequ } func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteStateResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeleteState", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_DeleteState_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -210,8 +253,9 @@ func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRe } func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SetNetworkMapPersistenceResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetNetworkMapPersistence", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_SetNetworkMapPersistence_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -219,20 +263,22 @@ func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in * } func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(TracePacketResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/TracePacket", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_TracePacket_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) { - stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeEvents", opts...) +func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SystemEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], DaemonService_SubscribeEvents_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &daemonServiceSubscribeEventsClient{stream} + x := &grpc.GenericClientStream[SubscribeRequest, SystemEvent]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -242,26 +288,13 @@ func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *Subscribe return x, nil } -type DaemonService_SubscribeEventsClient interface { - Recv() (*SystemEvent, error) - grpc.ClientStream -} - -type daemonServiceSubscribeEventsClient struct { - grpc.ClientStream -} - -func (x *daemonServiceSubscribeEventsClient) Recv() (*SystemEvent, error) { - m := new(SystemEvent) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DaemonService_SubscribeEventsClient = grpc.ServerStreamingClient[SystemEvent] func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetEventsResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetEvents", in, out, opts...) + err := c.cc.Invoke(ctx, DaemonService_GetEvents_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -270,7 +303,7 @@ func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsReques // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer -// for forward compatibility +// for forward compatibility. type DaemonServiceServer interface { // Login uses setup key to prepare configuration for the daemon. Login(context.Context, *LoginRequest) (*LoginResponse, error) @@ -307,14 +340,17 @@ type DaemonServiceServer interface { // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) - SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error + SubscribeEvents(*SubscribeRequest, grpc.ServerStreamingServer[SystemEvent]) error GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) mustEmbedUnimplementedDaemonServiceServer() } -// UnimplementedDaemonServiceServer must be embedded to have forward compatible implementations. -type UnimplementedDaemonServiceServer struct { -} +// UnimplementedDaemonServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDaemonServiceServer struct{} func (UnimplementedDaemonServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") @@ -370,13 +406,14 @@ func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented") } -func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error { +func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, grpc.ServerStreamingServer[SystemEvent]) error { return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented") } func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented") } func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} +func (UnimplementedDaemonServiceServer) testEmbeddedByValue() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to DaemonServiceServer will @@ -386,6 +423,13 @@ type UnsafeDaemonServiceServer interface { } func RegisterDaemonServiceServer(s grpc.ServiceRegistrar, srv DaemonServiceServer) { + // If the following call pancis, it indicates UnimplementedDaemonServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&DaemonService_ServiceDesc, srv) } @@ -399,7 +443,7 @@ func _DaemonService_Login_Handler(srv interface{}, ctx context.Context, dec func } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/Login", + FullMethod: DaemonService_Login_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Login(ctx, req.(*LoginRequest)) @@ -417,7 +461,7 @@ func _DaemonService_WaitSSOLogin_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/WaitSSOLogin", + FullMethod: DaemonService_WaitSSOLogin_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).WaitSSOLogin(ctx, req.(*WaitSSOLoginRequest)) @@ -435,7 +479,7 @@ func _DaemonService_Up_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/Up", + FullMethod: DaemonService_Up_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Up(ctx, req.(*UpRequest)) @@ -453,7 +497,7 @@ func _DaemonService_Status_Handler(srv interface{}, ctx context.Context, dec fun } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/Status", + FullMethod: DaemonService_Status_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Status(ctx, req.(*StatusRequest)) @@ -471,7 +515,7 @@ func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/Down", + FullMethod: DaemonService_Down_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Down(ctx, req.(*DownRequest)) @@ -489,7 +533,7 @@ func _DaemonService_GetConfig_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/GetConfig", + FullMethod: DaemonService_GetConfig_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).GetConfig(ctx, req.(*GetConfigRequest)) @@ -507,7 +551,7 @@ func _DaemonService_ListNetworks_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/ListNetworks", + FullMethod: DaemonService_ListNetworks_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).ListNetworks(ctx, req.(*ListNetworksRequest)) @@ -525,7 +569,7 @@ func _DaemonService_SelectNetworks_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/SelectNetworks", + FullMethod: DaemonService_SelectNetworks_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).SelectNetworks(ctx, req.(*SelectNetworksRequest)) @@ -543,7 +587,7 @@ func _DaemonService_DeselectNetworks_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/DeselectNetworks", + FullMethod: DaemonService_DeselectNetworks_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).DeselectNetworks(ctx, req.(*SelectNetworksRequest)) @@ -561,7 +605,7 @@ func _DaemonService_ForwardingRules_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/ForwardingRules", + FullMethod: DaemonService_ForwardingRules_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).ForwardingRules(ctx, req.(*EmptyRequest)) @@ -579,7 +623,7 @@ func _DaemonService_DebugBundle_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/DebugBundle", + FullMethod: DaemonService_DebugBundle_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).DebugBundle(ctx, req.(*DebugBundleRequest)) @@ -597,7 +641,7 @@ func _DaemonService_GetLogLevel_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/GetLogLevel", + FullMethod: DaemonService_GetLogLevel_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).GetLogLevel(ctx, req.(*GetLogLevelRequest)) @@ -615,7 +659,7 @@ func _DaemonService_SetLogLevel_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/SetLogLevel", + FullMethod: DaemonService_SetLogLevel_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).SetLogLevel(ctx, req.(*SetLogLevelRequest)) @@ -633,7 +677,7 @@ func _DaemonService_ListStates_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/ListStates", + FullMethod: DaemonService_ListStates_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).ListStates(ctx, req.(*ListStatesRequest)) @@ -651,7 +695,7 @@ func _DaemonService_CleanState_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/CleanState", + FullMethod: DaemonService_CleanState_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).CleanState(ctx, req.(*CleanStateRequest)) @@ -669,7 +713,7 @@ func _DaemonService_DeleteState_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/DeleteState", + FullMethod: DaemonService_DeleteState_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).DeleteState(ctx, req.(*DeleteStateRequest)) @@ -687,7 +731,7 @@ func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/SetNetworkMapPersistence", + FullMethod: DaemonService_SetNetworkMapPersistence_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, req.(*SetNetworkMapPersistenceRequest)) @@ -705,7 +749,7 @@ func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/TracePacket", + FullMethod: DaemonService_TracePacket_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).TracePacket(ctx, req.(*TracePacketRequest)) @@ -718,21 +762,11 @@ func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerS if err := stream.RecvMsg(m); err != nil { return err } - return srv.(DaemonServiceServer).SubscribeEvents(m, &daemonServiceSubscribeEventsServer{stream}) + return srv.(DaemonServiceServer).SubscribeEvents(m, &grpc.GenericServerStream[SubscribeRequest, SystemEvent]{ServerStream: stream}) } -type DaemonService_SubscribeEventsServer interface { - Send(*SystemEvent) error - grpc.ServerStream -} - -type daemonServiceSubscribeEventsServer struct { - grpc.ServerStream -} - -func (x *daemonServiceSubscribeEventsServer) Send(m *SystemEvent) error { - return x.ServerStream.SendMsg(m) -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DaemonService_SubscribeEventsServer = grpc.ServerStreamingServer[SystemEvent] func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetEventsRequest) @@ -744,7 +778,7 @@ func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/GetEvents", + FullMethod: DaemonService_GetEvents_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).GetEvents(ctx, req.(*GetEventsRequest)) diff --git a/client/server/server.go b/client/server/server.go index 31a437c99..e3ce1a2b4 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -707,7 +707,9 @@ func (s *Server) Status( s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) if msg.GetFullPeerStatus { - s.runProbes() + if msg.ShouldRunProbes { + s.runProbes() + } fullStatus := s.statusRecorder.GetFullStatus() pbFullStatus := toProtoFullStatus(fullStatus) From 6c633497bc84c19f452bf8d230901f1f2dabcd6b Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 2 Jul 2025 12:25:31 +0200 Subject: [PATCH 125/213] [management] fix network update test for delete policy (#4086) when adding a peer we calculate the network map an account using backpressure functions and some updates might arrive around the time we are deleting a policy. This change ensures we wait enough time for the updates from add peer to be sent and read before continuing with the test logic --- management/server/account_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/management/server/account_test.go b/management/server/account_test.go index 49fcedd56..7f319b81e 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1208,6 +1208,14 @@ func TestAccountManager_NetworkUpdates_DeletePolicy(t *testing.T) { updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) defer manager.peersUpdateManager.CloseChannel(context.Background(), peer1.ID) + // Ensure that we do not receive an update message before the policy is deleted + time.Sleep(time.Second) + select { + case <-updMsg: + t.Logf("received addPeer update message before policy deletion") + default: + } + wg := sync.WaitGroup{} wg.Add(1) go func() { From 22678bce7fd62e625c1dee1695bca5ed9ed6b326 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:13:10 +0200 Subject: [PATCH 126/213] [management] add uniqueness constraint for peer ip and label and optimize generation (#4042) --- management/server/account.go | 31 +- management/server/account_test.go | 20 +- management/server/migration/migration.go | 39 ++ management/server/peer.go | 394 +++++++++--------- management/server/peer/peer.go | 4 +- management/server/peer_test.go | 144 ++++++- management/server/store/file_store.go | 2 +- management/server/store/sql_store.go | 36 +- management/server/store/sql_store_test.go | 132 ++++-- management/server/store/store.go | 39 +- management/server/testdata/store.sql | 2 +- .../testdata/store_with_expired_peers.sql | 2 +- management/server/types/network.go | 64 ++- 13 files changed, 616 insertions(+), 293 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 3b7359502..8a80aefb6 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -106,6 +106,18 @@ type DefaultAccountManager struct { disableDefaultPolicy bool } +func isUniqueConstraintError(err error) bool { + switch { + case strings.Contains(err.Error(), "(SQLSTATE 23505)"), + strings.Contains(err.Error(), "Error 1062 (23000)"), + strings.Contains(err.Error(), "UNIQUE constraint failed"): + return true + + default: + return false + } +} + // getJWTGroupsChanges calculates the changes needed to sync a user's JWT groups. // Returns a bool indicating if there are changes in the JWT group membership, the updated user AutoGroups, // newly groups to create and an error if any occurred. @@ -1661,25 +1673,6 @@ func (am *DefaultAccountManager) handleUserPeer(ctx context.Context, transaction return false, nil } -func (am *DefaultAccountManager) getFreeDNSLabel(ctx context.Context, s store.Store, accountID string, peerHostName string) (string, error) { - existingLabels, err := s.GetPeerLabelsInAccount(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return "", fmt.Errorf("failed to get peer dns labels: %w", err) - } - - labelMap := ConvertSliceToMap(existingLabels) - newLabel, err := types.GetPeerHostLabel(peerHostName, labelMap) - if err != nil { - return "", fmt.Errorf("failed to get new host label: %w", err) - } - - if newLabel == "" { - return "", fmt.Errorf("failed to get new host label: %w", err) - } - - return newLabel, nil -} - func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) { allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Read) if err != nil { diff --git a/management/server/account_test.go b/management/server/account_test.go index 7f319b81e..60353389f 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -2623,11 +2623,11 @@ func TestAccount_SetJWTGroups(t *testing.T) { account := &types.Account{ Id: "accountID", Peers: map[string]*nbpeer.Peer{ - "peer1": {ID: "peer1", Key: "key1", UserID: "user1"}, - "peer2": {ID: "peer2", Key: "key2", UserID: "user1"}, - "peer3": {ID: "peer3", Key: "key3", UserID: "user1"}, - "peer4": {ID: "peer4", Key: "key4", UserID: "user2"}, - "peer5": {ID: "peer5", Key: "key5", UserID: "user2"}, + "peer1": {ID: "peer1", Key: "key1", UserID: "user1", IP: net.IP{1, 1, 1, 1}, DNSLabel: "peer1.domain.test"}, + "peer2": {ID: "peer2", Key: "key2", UserID: "user1", IP: net.IP{2, 2, 2, 2}, DNSLabel: "peer2.domain.test"}, + "peer3": {ID: "peer3", Key: "key3", UserID: "user1", IP: net.IP{3, 3, 3, 3}, DNSLabel: "peer3.domain.test"}, + "peer4": {ID: "peer4", Key: "key4", UserID: "user2", IP: net.IP{4, 4, 4, 4}, DNSLabel: "peer4.domain.test"}, + "peer5": {ID: "peer5", Key: "key5", UserID: "user2", IP: net.IP{5, 5, 5, 5}, DNSLabel: "peer5.domain.test"}, }, Groups: map[string]*types.Group{ "group1": {ID: "group1", Name: "group1", Issued: types.GroupIssuedAPI, Peers: []string{}}, @@ -3147,11 +3147,11 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 7, 20, 10, 80}, + {"Small", 50, 5, 7, 20, 5, 80}, {"Medium", 500, 100, 5, 40, 30, 140}, {"Large", 5000, 200, 80, 120, 140, 390}, - {"Small single", 50, 10, 7, 20, 10, 80}, - {"Medium single", 500, 10, 5, 40, 20, 85}, + {"Small single", 50, 10, 7, 20, 6, 80}, + {"Medium single", 500, 10, 5, 40, 15, 85}, {"Large 5", 5000, 15, 80, 120, 80, 200}, } @@ -3343,11 +3343,11 @@ func TestPropagateUserGroupMemberships(t *testing.T) { account, err := manager.GetOrCreateAccountByUser(ctx, initiatorId, domain) require.NoError(t, err) - peer1 := &nbpeer.Peer{ID: "peer1", AccountID: account.Id, UserID: initiatorId} + peer1 := &nbpeer.Peer{ID: "peer1", AccountID: account.Id, UserID: initiatorId, IP: net.IP{1, 1, 1, 1}, DNSLabel: "peer1.domain.test"} err = manager.Store.AddPeerToAccount(ctx, store.LockingStrengthUpdate, peer1) require.NoError(t, err) - peer2 := &nbpeer.Peer{ID: "peer2", AccountID: account.Id, UserID: initiatorId} + peer2 := &nbpeer.Peer{ID: "peer2", AccountID: account.Id, UserID: initiatorId, IP: net.IP{2, 2, 2, 2}, DNSLabel: "peer2.domain.test"} err = manager.Store.AddPeerToAccount(ctx, store.LockingStrengthUpdate, peer2) require.NoError(t, err) diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go index c8a852e0a..ab11be731 100644 --- a/management/server/migration/migration.go +++ b/management/server/migration/migration.go @@ -373,3 +373,42 @@ func DropIndex[T any](ctx context.Context, db *gorm.DB, indexName string) error log.WithContext(ctx).Infof("dropped index %s from table %T", indexName, model) return nil } + +func CreateIndexIfNotExists[T any](ctx context.Context, db *gorm.DB, indexName string, columns ...string) error { + var model T + + stmt := &gorm.Statement{DB: db} + if err := stmt.Parse(&model); err != nil { + return fmt.Errorf("failed to parse model schema: %w", err) + } + tableName := stmt.Schema.Table + dialect := db.Dialector.Name() + + var columnClause string + if dialect == "mysql" { + var withLength []string + for _, col := range columns { + if col == "ip" || col == "dns_label" { + withLength = append(withLength, fmt.Sprintf("%s(64)", col)) + } else { + withLength = append(withLength, col) + } + } + columnClause = strings.Join(withLength, ", ") + } else { + columnClause = strings.Join(columns, ", ") + } + + createStmt := fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (%s)", indexName, tableName, columnClause) + if dialect == "postgres" || dialect == "sqlite" { + createStmt = strings.Replace(createStmt, "CREATE UNIQUE INDEX", "CREATE UNIQUE INDEX IF NOT EXISTS", 1) + } + + log.WithContext(ctx).Infof("executing index creation: %s", createStmt) + if err := db.Exec(createStmt).Error; err != nil { + return fmt.Errorf("failed to create index %s: %w", indexName, err) + } + + log.WithContext(ctx).Infof("successfully created index %s on table %s", indexName, tableName) + return nil +} diff --git a/management/server/peer.go b/management/server/peer.go index 254048a96..2c1d8f64c 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -15,13 +15,14 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" + nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/geolocation" + "github.com/netbirdio/netbird/management/server/idp" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" @@ -234,14 +235,10 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user } if peer.Name != update.Name { - existingLabels, err := getPeerDNSLabels(ctx, transaction, accountID) + var newLabel string + newLabel, err = getPeerIPDNSLabel(ctx, transaction, peer.IP, accountID, update.Name) if err != nil { - return err - } - - newLabel, err := types.GetPeerHostLabel(update.Name, existingLabels) - if err != nil { - return err + return fmt.Errorf("failed to get free DNS label: %w", err) } peer.Name = update.Name @@ -463,208 +460,232 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s upperKey := strings.ToUpper(setupKey) hashedKey := sha256.Sum256([]byte(upperKey)) encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:]) - var accountID string - var err error - addedByUser := false - if len(userID) > 0 { - addedByUser = true - accountID, err = am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userID) - } else { - accountID, err = am.Store.GetAccountIDBySetupKey(ctx, encodedHashedKey) - } - if err != nil { - return nil, nil, nil, status.Errorf(status.NotFound, "failed adding new peer: account not found") - } - - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) - defer func() { - if unlock != nil { - unlock() - } - }() + addedByUser := len(userID) > 0 // This is a handling for the case when the same machine (with the same WireGuard pub key) tries to register twice. // Such case is possible when AddPeer function takes long time to finish after AcquireWriteLockByUID (e.g., database is slow) // and the peer disconnects with a timeout and tries to register again. // We just check if this machine has been registered before and reject the second registration. // The connecting peer should be able to recover with a retry. - _, err = am.Store.GetPeerByPeerPubKey(ctx, store.LockingStrengthShare, peer.Key) + _, err := am.Store.GetPeerByPeerPubKey(ctx, store.LockingStrengthNone, peer.Key) if err == nil { return nil, nil, nil, status.Errorf(status.PreconditionFailed, "peer has been already registered") } opEvent := &activity.Event{ Timestamp: time.Now().UTC(), - AccountID: accountID, } var newPeer *nbpeer.Peer var updateAccountPeers bool - err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - var setupKeyID string - var setupKeyName string - var ephemeral bool - var groupsToAdd []string - var allowExtraDNSLabels bool - if addedByUser { - user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, userID) - if err != nil { - return fmt.Errorf("failed to get user groups: %w", err) - } - groupsToAdd = user.AutoGroups - opEvent.InitiatorID = userID - opEvent.Activity = activity.PeerAddedByUser - } else { - // Validate the setup key - sk, err := transaction.GetSetupKeyBySecret(ctx, store.LockingStrengthUpdate, encodedHashedKey) - if err != nil { - return fmt.Errorf("failed to get setup key: %w", err) - } - - if !sk.IsValid() { - return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key is invalid") - } - - opEvent.InitiatorID = sk.Id - opEvent.Activity = activity.PeerAddedWithSetupKey - groupsToAdd = sk.AutoGroups - ephemeral = sk.Ephemeral - setupKeyID = sk.Id - setupKeyName = sk.Name - allowExtraDNSLabels = sk.AllowExtraDNSLabels - - if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 { - return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels") - } - } - - if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" { - if am.idpManager != nil { - userdata, err := am.idpManager.GetUserDataByID(ctx, userID, idp.AppMetadata{WTAccountID: accountID}) - if err == nil && userdata != nil { - peer.Meta.Hostname = fmt.Sprintf("%s-%s", peer.Meta.Hostname, strings.Split(userdata.Email, "@")[0]) - } - } - } - - freeLabel, err := am.getFreeDNSLabel(ctx, transaction, accountID, peer.Meta.Hostname) + var setupKeyID string + var setupKeyName string + var ephemeral bool + var groupsToAdd []string + var allowExtraDNSLabels bool + var accountID string + if addedByUser { + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { - return fmt.Errorf("failed to get free DNS label: %w", err) + return nil, nil, nil, fmt.Errorf("failed to get user groups: %w", err) } - - freeIP, err := getFreeIP(ctx, transaction, accountID) + groupsToAdd = user.AutoGroups + opEvent.InitiatorID = userID + opEvent.Activity = activity.PeerAddedByUser + accountID = user.AccountID + } else { + // Validate the setup key + sk, err := am.Store.GetSetupKeyBySecret(ctx, store.LockingStrengthNone, encodedHashedKey) if err != nil { - return fmt.Errorf("failed to get free IP: %w", err) + return nil, nil, nil, fmt.Errorf("failed to get setup key: %w", err) } - if err := domain.ValidateDomainsList(peer.ExtraDNSLabels); err != nil { - return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err) + // we will check key twice for early return + if !sk.IsValid() { + return nil, nil, nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key is invalid") } - registrationTime := time.Now().UTC() - newPeer = &nbpeer.Peer{ - ID: xid.New().String(), - AccountID: accountID, - Key: peer.Key, - IP: freeIP, - Meta: peer.Meta, - Name: peer.Meta.Hostname, - DNSLabel: freeLabel, - UserID: userID, - Status: &nbpeer.PeerStatus{Connected: false, LastSeen: registrationTime}, - SSHEnabled: false, - SSHKey: peer.SSHKey, - LastLogin: ®istrationTime, - CreatedAt: registrationTime, - LoginExpirationEnabled: addedByUser, - Ephemeral: ephemeral, - Location: peer.Location, - InactivityExpirationEnabled: addedByUser, - ExtraDNSLabels: peer.ExtraDNSLabels, - AllowExtraDNSLabels: allowExtraDNSLabels, - } - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return fmt.Errorf("failed to get account settings: %w", err) - } + opEvent.InitiatorID = sk.Id + opEvent.Activity = activity.PeerAddedWithSetupKey + groupsToAdd = sk.AutoGroups + ephemeral = sk.Ephemeral + setupKeyID = sk.Id + setupKeyName = sk.Name + allowExtraDNSLabels = sk.AllowExtraDNSLabels + accountID = sk.AccountID - opEvent.TargetID = newPeer.ID - opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain(settings)) - if !addedByUser { - opEvent.Meta["setup_key_name"] = setupKeyName + if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 { + return nil, nil, nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels") } + } + opEvent.AccountID = accountID - if am.geo != nil && newPeer.Location.ConnectionIP != nil { - location, err := am.geo.Lookup(newPeer.Location.ConnectionIP) - if err != nil { - log.WithContext(ctx).Warnf("failed to get location for new peer realip: [%s]: %v", newPeer.Location.ConnectionIP.String(), err) - } else { - newPeer.Location.CountryCode = location.Country.ISOCode - newPeer.Location.CityName = location.City.Names.En - newPeer.Location.GeoNameID = location.City.GeonameID + if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" { + if am.idpManager != nil { + userdata, err := am.idpManager.GetUserDataByID(ctx, userID, idp.AppMetadata{WTAccountID: accountID}) + if err == nil && userdata != nil { + peer.Meta.Hostname = fmt.Sprintf("%s-%s", peer.Meta.Hostname, strings.Split(userdata.Email, "@")[0]) } } + } - newPeer = am.integratedPeerValidator.PreparePeer(ctx, accountID, newPeer, groupsToAdd, settings.Extra) - - err = transaction.AddPeerToAllGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID) - if err != nil { - return fmt.Errorf("failed adding peer to All group: %w", err) - } - - if len(groupsToAdd) > 0 { - for _, g := range groupsToAdd { - err = transaction.AddPeerToGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID, g) - if err != nil { - return err - } - } - } - - err = transaction.AddPeerToAccount(ctx, store.LockingStrengthUpdate, newPeer) - if err != nil { - return fmt.Errorf("failed to add peer to account: %w", err) - } - - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID) - if err != nil { - return fmt.Errorf("failed to increment network serial: %w", err) - } - - if addedByUser { - err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin()) - if err != nil { - log.WithContext(ctx).Debugf("failed to update user last login: %v", err) - } - } else { - err = transaction.IncrementSetupKeyUsage(ctx, setupKeyID) - if err != nil { - return fmt.Errorf("failed to increment setup key usage: %w", err) - } - } - - updateAccountPeers, err = isPeerInActiveGroup(ctx, transaction, accountID, newPeer.ID) - if err != nil { - return err - } - - log.WithContext(ctx).Debugf("Peer %s added to account %s", newPeer.ID, accountID) - return nil - }) + if err := domain.ValidateDomainsList(peer.ExtraDNSLabels); err != nil { + return nil, nil, nil, status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err) + } + registrationTime := time.Now().UTC() + newPeer = &nbpeer.Peer{ + ID: xid.New().String(), + AccountID: accountID, + Key: peer.Key, + Meta: peer.Meta, + Name: peer.Meta.Hostname, + UserID: userID, + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: registrationTime}, + SSHEnabled: false, + SSHKey: peer.SSHKey, + LastLogin: ®istrationTime, + CreatedAt: registrationTime, + LoginExpirationEnabled: addedByUser, + Ephemeral: ephemeral, + Location: peer.Location, + InactivityExpirationEnabled: addedByUser, + ExtraDNSLabels: peer.ExtraDNSLabels, + AllowExtraDNSLabels: allowExtraDNSLabels, + } + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get account settings: %w", err) + } + + if am.geo != nil && newPeer.Location.ConnectionIP != nil { + location, err := am.geo.Lookup(newPeer.Location.ConnectionIP) + if err != nil { + log.WithContext(ctx).Warnf("failed to get location for new peer realip: [%s]: %v", newPeer.Location.ConnectionIP.String(), err) + } else { + newPeer.Location.CountryCode = location.Country.ISOCode + newPeer.Location.CityName = location.City.Names.En + newPeer.Location.GeoNameID = location.City.GeonameID + } + } + + newPeer = am.integratedPeerValidator.PreparePeer(ctx, accountID, newPeer, groupsToAdd, settings.Extra) + + network, err := am.Store.GetAccountNetwork(ctx, store.LockingStrengthNone, accountID) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed getting network: %w", err) + } + + maxAttempts := 10 + for attempt := 1; attempt <= maxAttempts; attempt++ { + var freeIP net.IP + freeIP, err = types.AllocateRandomPeerIP(network.Net) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get free IP: %w", err) + } + + var freeLabel string + freeLabel, err = getPeerIPDNSLabel(ctx, am.Store, freeIP, accountID, peer.Meta.Hostname) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) + } + + newPeer.DNSLabel = freeLabel + newPeer.IP = freeIP + + unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) + defer func() { + if unlock != nil { + unlock() + } + }() + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + err = transaction.AddPeerToAccount(ctx, store.LockingStrengthUpdate, newPeer) + if err != nil { + return err + } + + err = transaction.AddPeerToAllGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID) + if err != nil { + return fmt.Errorf("failed adding peer to All group: %w", err) + } + + if len(groupsToAdd) > 0 { + for _, g := range groupsToAdd { + err = transaction.AddPeerToGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID, g) + if err != nil { + return err + } + } + } + + if addedByUser { + err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin()) + if err != nil { + log.WithContext(ctx).Debugf("failed to update user last login: %v", err) + } + } else { + sk, err := transaction.GetSetupKeyBySecret(ctx, store.LockingStrengthUpdate, encodedHashedKey) + if err != nil { + return fmt.Errorf("failed to get setup key: %w", err) + } + + // we validate at the end to not block the setup key for too long + if !sk.IsValid() { + return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key is invalid") + } + + err = transaction.IncrementSetupKeyUsage(ctx, setupKeyID) + if err != nil { + return fmt.Errorf("failed to increment setup key usage: %w", err) + } + } + + err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID) + if err != nil { + return fmt.Errorf("failed to increment network serial: %w", err) + } + + log.WithContext(ctx).Debugf("Peer %s added to account %s", newPeer.ID, accountID) + return nil + }) + if err == nil { + unlock() + unlock = nil + break + } + + if isUniqueConstraintError(err) { + unlock() + unlock = nil + log.WithContext(ctx).Debugf("Failed to add peer in attempt %d, retrying: %v", attempt, err) + continue + } + return nil, nil, nil, fmt.Errorf("failed to add peer to database: %w", err) } + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to add peer to database after %d attempts: %w", maxAttempts, err) + } + + updateAccountPeers, err = isPeerInActiveGroup(ctx, am.Store, accountID, newPeer.ID) + if err != nil { + updateAccountPeers = true + } if newPeer == nil { return nil, nil, nil, fmt.Errorf("new peer is nil") } - am.StoreEvent(ctx, opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta) + opEvent.TargetID = newPeer.ID + opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain(settings)) + if !addedByUser { + opEvent.Meta["setup_key_name"] = setupKeyName + } - unlock() - unlock = nil + am.StoreEvent(ctx, opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta) if updateAccountPeers { am.BufferUpdateAccountPeers(ctx, accountID) @@ -673,23 +694,21 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s return am.getValidatedPeerWithMap(ctx, false, accountID, newPeer) } -func getFreeIP(ctx context.Context, transaction store.Store, accountID string) (net.IP, error) { - takenIps, err := transaction.GetTakenIPs(ctx, store.LockingStrengthShare, accountID) +func getPeerIPDNSLabel(ctx context.Context, tx store.Store, ip net.IP, accountID, peerHostName string) (string, error) { + ip = ip.To4() + + dnsName, err := nbdns.GetParsedDomainLabel(peerHostName) if err != nil { - return nil, fmt.Errorf("failed to get taken IPs: %w", err) + return "", fmt.Errorf("failed to parse peer host name %s: %w", peerHostName, err) } - network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthUpdate, accountID) + _, err = tx.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, dnsName) if err != nil { - return nil, fmt.Errorf("failed getting network: %w", err) + //nolint:nilerr + return dnsName, nil } - nextIp, err := types.AllocatePeerIP(network.Net, takenIps) - if err != nil { - return nil, fmt.Errorf("failed to allocate new peer ip: %w", err) - } - - return nextIp, nil + return fmt.Sprintf("%s-%d-%d", dnsName, ip[2], ip[3]), nil } // SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible @@ -1477,19 +1496,6 @@ func getPeerGroupIDs(ctx context.Context, transaction store.Store, accountID str return groupIDs, err } -func getPeerDNSLabels(ctx context.Context, transaction store.Store, accountID string) (types.LookupMap, error) { - dnsLabels, err := transaction.GetPeerLabelsInAccount(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return nil, err - } - - existingLabels := make(types.LookupMap) - for _, label := range dnsLabels { - existingLabels[label] = struct{}{} - } - return existingLabels, nil -} - // IsPeerInActiveGroup checks if the given peer is part of a group that is used // in an active DNS, route, or ACL configuration. func isPeerInActiveGroup(ctx context.Context, transaction store.Store, accountID, peerID string) (bool, error) { diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 8ce1dfb4e..f7140e254 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -20,14 +20,14 @@ type Peer struct { // WireGuard public key Key string `gorm:"index"` // IP address of the Peer - IP net.IP `gorm:"serializer:json"` + IP net.IP `gorm:"serializer:json"` // uniqueness index per accountID (check migrations) // Meta is a Peer system meta data Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"` // Name is peer's name (machine name) Name string // DNSLabel is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's // domain to the peer label. e.g. peer-dns-label.netbird.cloud - DNSLabel string + DNSLabel string // uniqueness index per accountID (check migrations) // Status peer's management connection status Status *PeerStatus `gorm:"embedded;embeddedPrefix:peer_status_"` // The user ID that registered the peer diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 775385a29..3edf7e82c 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -10,7 +10,9 @@ import ( "net/netip" "os" "runtime" + "strconv" "strings" + "sync" "testing" "time" @@ -19,6 +21,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" @@ -1391,7 +1394,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { name: "Absent setup key", existingSetupKeyID: "AAAAAAAA-38F5-4553-B31E-DD66C696CEBB", expectAddPeerError: true, - expectedErrorMsgSubstring: "failed adding new peer: account not found", + expectedErrorMsgSubstring: "failed to get setup key: setup key not found", }, } @@ -2057,10 +2060,14 @@ func Test_DeletePeer(t *testing.T) { "peer1": { ID: "peer1", AccountID: accountID, + IP: net.IP{1, 1, 1, 1}, + DNSLabel: "peer1.test", }, "peer2": { ID: "peer2", AccountID: accountID, + IP: net.IP{2, 2, 2, 2}, + DNSLabel: "peer2.test", }, } account.Groups = map[string]*types.Group{ @@ -2090,3 +2097,138 @@ func Test_DeletePeer(t *testing.T) { assert.NotContains(t, group.Peers, "peer1") } + +func Test_IsUniqueConstraintError(t *testing.T) { + tests := []struct { + name string + engine types.Engine + }{ + { + name: "PostgreSQL uniqueness error", + engine: types.PostgresStoreEngine, + }, + { + name: "MySQL uniqueness error", + engine: types.MysqlStoreEngine, + }, + { + name: "SQLite uniqueness error", + engine: types.SqliteStoreEngine, + }, + } + + peer := &nbpeer.Peer{ + ID: "test-peer-id", + AccountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + DNSLabel: "test-peer-dns-label", + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", string(tt.engine)) + s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "testdata/extended-store.sql", t.TempDir()) + if err != nil { + t.Fatalf("Error when creating store: %s", err) + } + t.Cleanup(cleanup) + + err = s.AddPeerToAccount(context.Background(), store.LockingStrengthUpdate, peer) + assert.NoError(t, err) + + err = s.AddPeerToAccount(context.Background(), store.LockingStrengthUpdate, peer) + result := isUniqueConstraintError(err) + assert.True(t, result) + }) + } +} + +func Test_AddPeer(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", string(types.PostgresStoreEngine)) + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + accountID := "testaccount" + userID := "testuser" + + _, err = createAccount(manager, accountID, userID, "domain.com") + if err != nil { + t.Fatal("error creating account") + return + } + + setupKey, err := manager.CreateSetupKey(context.Background(), accountID, "test-key", types.SetupKeyReusable, time.Hour, nil, 10000, userID, false, false) + if err != nil { + t.Fatal("error creating setup key") + return + } + + const totalPeers = 300 // totalPeers / differentHostnames should be less than 10 (due to concurrent retries) + const differentHostnames = 50 + + var wg sync.WaitGroup + errs := make(chan error, totalPeers+differentHostnames) + start := make(chan struct{}) + for i := 0; i < totalPeers; i++ { + wg.Add(1) + hostNameID := i % differentHostnames + + go func(i int) { + defer wg.Done() + + newPeer := &nbpeer.Peer{ + Key: "key" + strconv.Itoa(i), + Meta: nbpeer.PeerSystemMeta{Hostname: "peer" + strconv.Itoa(hostNameID), GoOS: "linux"}, + } + + <-start + + _, _, _, err := manager.AddPeer(context.Background(), setupKey.Key, "", newPeer) + if err != nil { + errs <- fmt.Errorf("AddPeer failed for peer %d: %w", i, err) + return + } + + }(i) + } + startTime := time.Now() + + close(start) + wg.Wait() + close(errs) + + t.Logf("time since start: %s", time.Since(startTime)) + + for err := range errs { + t.Fatal(err) + } + + account, err := manager.Store.GetAccount(context.Background(), accountID) + if err != nil { + t.Fatalf("Failed to get account %s: %v", accountID, err) + } + + assert.Equal(t, totalPeers, len(account.Peers), "Expected %d peers in account %s, got %d", totalPeers, accountID, len(account.Peers)) + + seenIP := make(map[string]bool) + for _, p := range account.Peers { + ipStr := p.IP.String() + if seenIP[ipStr] { + t.Fatalf("Duplicate IP found in account %s: %s", accountID, ipStr) + } + seenIP[ipStr] = true + } + + seenLabel := make(map[string]bool) + for _, p := range account.Peers { + if seenLabel[p.DNSLabel] { + t.Fatalf("Duplicate Label found in account %s: %s", accountID, p.DNSLabel) + } + seenLabel[p.DNSLabel] = true + } + + assert.Equal(t, totalPeers, maps.Values(account.SetupKeys)[0].UsedTimes) + assert.Equal(t, uint64(totalPeers), account.Network.Serial) +} diff --git a/management/server/store/file_store.go b/management/server/store/file_store.go index 3b95164f5..d5d9337ca 100644 --- a/management/server/store/file_store.go +++ b/management/server/store/file_store.go @@ -156,7 +156,7 @@ func restore(ctx context.Context, file string) (*FileStore, error) { allGroup, err := account.GetGroupAll() if err != nil { - log.WithContext(ctx).Errorf("unable to find the All group, this should happen only when migrate from a version that didn't support groups. Error: %v", err) + log.WithContext(ctx).Errorf("unable to find the All group, this should happen only when migratePreAuto from a version that didn't support groups. Error: %v", err) // if the All group didn't exist we probably don't have routes to update continue } diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 72a73a57a..197255ab6 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -92,8 +92,8 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1}, nil } - if err := migrate(ctx, db); err != nil { - return nil, fmt.Errorf("migrate: %w", err) + if err := migratePreAuto(ctx, db); err != nil { + return nil, fmt.Errorf("migratePreAuto: %w", err) } err = db.AutoMigrate( &types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.Group{}, @@ -102,7 +102,10 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, ) if err != nil { - return nil, fmt.Errorf("auto migrate: %w", err) + return nil, fmt.Errorf("auto migratePreAuto: %w", err) + } + if err := migratePostAuto(ctx, db); err != nil { + return nil, fmt.Errorf("migratePostAuto: %w", err) } return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1}, nil @@ -967,7 +970,7 @@ func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength return ips, nil } -func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string) ([]string, error) { +func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string, dnsLabel string) ([]string, error) { tx := s.db if lockStrength != LockingStrengthNone { tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) @@ -975,7 +978,7 @@ func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength Lock var labels []string result := tx.Model(&nbpeer.Peer{}). - Where("account_id = ?", accountID). + Where("account_id = ? AND dns_label LIKE ?", accountID, dnsLabel+"%"). Pluck("dns_label", &labels) if result.Error != nil { @@ -1254,7 +1257,7 @@ func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength Locking if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, status.NewSetupKeyNotFoundError(key) + return nil, status.Errorf(status.PreconditionFailed, "setup key not found") } log.WithContext(ctx).Errorf("failed to get setup key by secret from store: %v", result.Error) return nil, status.Errorf(status.Internal, "failed to get setup key by secret from store") @@ -2546,6 +2549,27 @@ func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength return &peer, nil } +func (s *SqlStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) { + tx := s.db.WithContext(ctx) + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + + var peerID string + result := tx.Model(&nbpeer.Peer{}). + Select("id"). + // Where(" = ?", hostname). + Where("account_id = ? AND dns_label = ?", accountID, hostname). + Limit(1). + Scan(&peerID) + + if peerID == "" { + return "", gorm.ErrRecordNotFound + } + + return peerID, result.Error +} + func (s *SqlStore) CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) { var count int64 result := s.db.Model(&types.Account{}). diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index f187be8c7..928486ab4 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -10,6 +10,7 @@ import ( "net/netip" "os" "runtime" + "sort" "sync" "testing" "time" @@ -630,7 +631,7 @@ func TestMigrate(t *testing.T) { t.Cleanup(cleanUp) assert.NoError(t, err) - err = migrate(context.Background(), store.(*SqlStore).db) + err = migratePreAuto(context.Background(), store.(*SqlStore).db) require.NoError(t, err, "Migration should not fail on empty db") _, ipnet, err := net.ParseCIDR("10.0.0.0/24") @@ -685,10 +686,10 @@ func TestMigrate(t *testing.T) { err = store.(*SqlStore).db.Save(rt).Error require.NoError(t, err, "Failed to insert Gob data") - err = migrate(context.Background(), store.(*SqlStore).db) + err = migratePreAuto(context.Background(), store.(*SqlStore).db) require.NoError(t, err, "Migration should not fail on gob populated db") - err = migrate(context.Background(), store.(*SqlStore).db) + err = migratePreAuto(context.Background(), store.(*SqlStore).db) require.NoError(t, err, "Migration should not fail on migrated db") err = store.(*SqlStore).db.Delete(rt).Where("id = ?", "route1").Error @@ -704,10 +705,10 @@ func TestMigrate(t *testing.T) { err = store.(*SqlStore).db.Save(nRT).Error require.NoError(t, err, "Failed to insert json nil slice data") - err = migrate(context.Background(), store.(*SqlStore).db) + err = migratePreAuto(context.Background(), store.(*SqlStore).db) require.NoError(t, err, "Migration should not fail on json nil slice populated db") - err = migrate(context.Background(), store.(*SqlStore).db) + err = migratePreAuto(context.Background(), store.(*SqlStore).db) require.NoError(t, err, "Migration should not fail on migrated db") } @@ -950,6 +951,7 @@ func TestSqlite_GetTakenIPs(t *testing.T) { peer1 := &nbpeer.Peer{ ID: "peer1", AccountID: existingAccountID, + DNSLabel: "peer1", IP: net.IP{1, 1, 1, 1}, } err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) @@ -961,8 +963,9 @@ func TestSqlite_GetTakenIPs(t *testing.T) { assert.Equal(t, []net.IP{ip1}, takenIPs) peer2 := &nbpeer.Peer{ - ID: "peer2", + ID: "peer1second", AccountID: existingAccountID, + DNSLabel: "peer1-1", IP: net.IP{2, 2, 2, 2}, } err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) @@ -972,49 +975,100 @@ func TestSqlite_GetTakenIPs(t *testing.T) { require.NoError(t, err) ip2 := net.IP{2, 2, 2, 2}.To16() assert.Equal(t, []net.IP{ip1, ip2}, takenIPs) - } func TestSqlite_GetPeerLabelsInAccount(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(types.SqliteStoreEngine)) - store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) - if err != nil { - return - } - t.Cleanup(cleanup) + runTestForAllEngines(t, "../testdata/extended-store.sql", func(t *testing.T, store Store) { + existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + peerHostname := "peer1" - existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + _, err := store.GetAccount(context.Background(), existingAccountID) + require.NoError(t, err) - _, err = store.GetAccount(context.Background(), existingAccountID) - require.NoError(t, err) + labels, err := store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID, peerHostname) + require.NoError(t, err) + assert.Equal(t, []string{}, labels) - labels, err := store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID) - require.NoError(t, err) - assert.Equal(t, []string{}, labels) + peer1 := &nbpeer.Peer{ + ID: "peer1", + AccountID: existingAccountID, + DNSLabel: "peer1", + IP: net.IP{1, 1, 1, 1}, + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + require.NoError(t, err) - peer1 := &nbpeer.Peer{ - ID: "peer1", - AccountID: existingAccountID, - DNSLabel: "peer1.domain.test", - } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) - require.NoError(t, err) + labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID, peerHostname) + require.NoError(t, err) + assert.Equal(t, []string{"peer1"}, labels) - labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID) - require.NoError(t, err) - assert.Equal(t, []string{"peer1.domain.test"}, labels) + peer2 := &nbpeer.Peer{ + ID: "peer1second", + AccountID: existingAccountID, + DNSLabel: "peer1-1", + IP: net.IP{2, 2, 2, 2}, + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + require.NoError(t, err) - peer2 := &nbpeer.Peer{ - ID: "peer2", - AccountID: existingAccountID, - DNSLabel: "peer2.domain.test", - } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) - require.NoError(t, err) + labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID, peerHostname) + require.NoError(t, err) - labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID) - require.NoError(t, err) - assert.Equal(t, []string{"peer1.domain.test", "peer2.domain.test"}, labels) + expected := []string{"peer1", "peer1-1"} + sort.Strings(expected) + sort.Strings(labels) + assert.Equal(t, expected, labels) + }) +} + +func Test_AddPeerWithSameDnsLabel(t *testing.T) { + runTestForAllEngines(t, "../testdata/extended-store.sql", func(t *testing.T, store Store) { + existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + _, err := store.GetAccount(context.Background(), existingAccountID) + require.NoError(t, err) + + peer1 := &nbpeer.Peer{ + ID: "peer1", + AccountID: existingAccountID, + DNSLabel: "peer1.domain.test", + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + require.NoError(t, err) + + peer2 := &nbpeer.Peer{ + ID: "peer1second", + AccountID: existingAccountID, + DNSLabel: "peer1.domain.test", + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + require.Error(t, err) + }) +} + +func Test_AddPeerWithSameIP(t *testing.T) { + runTestForAllEngines(t, "../testdata/extended-store.sql", func(t *testing.T, store Store) { + existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + _, err := store.GetAccount(context.Background(), existingAccountID) + require.NoError(t, err) + + peer1 := &nbpeer.Peer{ + ID: "peer1", + AccountID: existingAccountID, + IP: net.IP{1, 1, 1, 1}, + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + require.NoError(t, err) + + peer2 := &nbpeer.Peer{ + ID: "peer1second", + AccountID: existingAccountID, + IP: net.IP{1, 1, 1, 1}, + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + require.Error(t, err) + }) } func TestSqlite_GetAccountNetwork(t *testing.T) { diff --git a/management/server/store/store.go b/management/server/store/store.go index f66130ad3..30ff1549d 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -117,7 +117,7 @@ type Store interface { SavePostureChecks(ctx context.Context, lockStrength LockingStrength, postureCheck *posture.Checks) error DeletePostureChecks(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) error - GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountId string) ([]string, error) + GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountId string, hostname string) ([]string, error) AddPeerToAllGroup(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error AddPeerToGroup(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string, groupID string) error GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) @@ -193,6 +193,7 @@ type Store interface { SaveNetworkResource(ctx context.Context, lockStrength LockingStrength, resource *resourceTypes.NetworkResource) error DeleteNetworkResource(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) error GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) + GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) } const ( @@ -234,9 +235,9 @@ func getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) type if util.FileExists(jsonStoreFile) && !util.FileExists(sqliteStoreFile) { log.WithContext(ctx).Warnf("unsupported store engine specified, but found %s. Automatically migrating to SQLite.", jsonStoreFile) - // Attempt to migrate from JSON store to SQLite + // Attempt to migratePreAuto from JSON store to SQLite if err := MigrateFileStoreToSqlite(ctx, dataDir); err != nil { - log.WithContext(ctx).Errorf("failed to migrate filestore to SQLite: %v", err) + log.WithContext(ctx).Errorf("failed to migratePreAuto filestore to SQLite: %v", err) kind = types.FileStoreEngine } } @@ -280,9 +281,9 @@ func checkFileStoreEngine(kind types.Engine, dataDir string) error { return nil } -// migrate migrates the SQLite database to the latest schema -func migrate(ctx context.Context, db *gorm.DB) error { - migrations := getMigrations(ctx) +// migratePreAuto migrates the SQLite database to the latest schema +func migratePreAuto(ctx context.Context, db *gorm.DB) error { + migrations := getMigrationsPreAuto(ctx) for _, m := range migrations { if err := m(db); err != nil { @@ -293,7 +294,7 @@ func migrate(ctx context.Context, db *gorm.DB) error { return nil } -func getMigrations(ctx context.Context) []migrationFunc { +func getMigrationsPreAuto(ctx context.Context) []migrationFunc { return []migrationFunc{ func(db *gorm.DB) error { return migration.MigrateFieldFromGobToJSON[types.Account, net.IPNet](ctx, db, "network_net") @@ -329,6 +330,28 @@ func getMigrations(ctx context.Context) []migrationFunc { return migration.DropIndex[routerTypes.NetworkRouter](ctx, db, "idx_network_routers_id") }, } +} // migratePostAuto migrates the SQLite database to the latest schema +func migratePostAuto(ctx context.Context, db *gorm.DB) error { + migrations := getMigrationsPostAuto(ctx) + + for _, m := range migrations { + if err := m(db); err != nil { + return err + } + } + + return nil +} + +func getMigrationsPostAuto(ctx context.Context) []migrationFunc { + return []migrationFunc{ + func(db *gorm.DB) error { + return migration.CreateIndexIfNotExists[nbpeer.Peer](ctx, db, "idx_account_ip", "account_id", "ip") + }, + func(db *gorm.DB) error { + return migration.CreateIndexIfNotExists[nbpeer.Peer](ctx, db, "idx_account_dnslabel", "account_id", "dns_label") + }, + } } // NewTestStoreFromSQL is only used in tests. It will create a test database base of the store engine set in env. @@ -577,7 +600,7 @@ func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error { sqliteStoreAccounts := len(store.GetAllAccounts(ctx)) if fsStoreAccounts != sqliteStoreAccounts { - return fmt.Errorf("failed to migrate accounts from file to sqlite. Expected accounts: %d, got: %d", + return fmt.Errorf("failed to migratePreAuto accounts from file to sqlite. Expected accounts: %d, got: %d", fsStoreAccounts, sqliteStoreAccounts) } diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql index 41b8fa2f7..4b126c618 100644 --- a/management/server/testdata/store.sql +++ b/management/server/testdata/store.sql @@ -52,4 +52,4 @@ INSERT INTO policy_rules VALUES('cs387mkv2d4bgq41b6n0','cs1tnh0hhcjnqoiuebf0','D INSERT INTO network_routers VALUES('ctc20ji7qv9ck2sebc80','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','cs1tnh0hhcjnqoiuebeg',NULL,0,0); INSERT INTO network_resources VALUES ('ctc4nci7qv9061u6ilfg','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Host','192.168.1.1'); INSERT INTO networks VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Test Network','Test Network'); -INSERT INTO peers VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','','','192.168.0.0','','','','','','','','','','','','','','','','','test','test','2023-01-01 00:00:00+00:00',0,0,0,'a23efe53-63fb-11ec-90d6-0242ac120003','',0,0,'2023-01-01 00:00:00+00:00','2023-01-01 00:00:00+00:00',0,'','','',0); +INSERT INTO peers VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','','','"192.168.0.0"','','','','','','','','','','','','','','','','','test','test','2023-01-01 00:00:00+00:00',0,0,0,'a23efe53-63fb-11ec-90d6-0242ac120003','',0,0,'2023-01-01 00:00:00+00:00','2023-01-01 00:00:00+00:00',0,'','','',0); diff --git a/management/server/testdata/store_with_expired_peers.sql b/management/server/testdata/store_with_expired_peers.sql index 5990a0625..f2ef56a23 100644 --- a/management/server/testdata/store_with_expired_peers.sql +++ b/management/server/testdata/store_with_expired_peers.sql @@ -30,7 +30,7 @@ INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62 INSERT INTO peers VALUES('cfvprsrlo1hqoo49ohog','bf1c8084-ba50-4ce7-9439-34653001fc3b','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,0,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); INSERT INTO peers VALUES('cg05lnblo1hkg2j514p0','bf1c8084-ba50-4ce7-9439-34653001fc3b','RlSy2vzoG2HyMBTUImXOiVhCBiiBa5qD5xzMxkiFDW4=','','"100.64.39.54"','expiredhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'expiredhost','expiredhost','2023-03-02 09:19:57.276717255+01:00',0,1,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbK5ZXJsGOOWoBT4OmkPtgdPZe2Q7bDuS/zjn2CZxhK',0,1,0,'2023-03-02 09:14:21.791679181+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); INSERT INTO peers VALUES('cg3161rlo1hs9cq94gdg','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.96"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost','2023-03-06 18:21:27.252010027+01:00',0,0,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,0,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); -INSERT INTO peers VALUES('csrnkiq7qv9d8aitqd50','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.96"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost','2023-03-06 18:21:27.252010027+01:00',0,0,0,'f4f6d672-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,1,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); +INSERT INTO peers VALUES('csrnkiq7qv9d8aitqd50','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.97"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost-1','2023-03-06 18:21:27.252010027+01:00',0,0,0,'f4f6d672-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,1,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 17:00:32.528196+02:00','api',0,''); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 17:00:32.528196+02:00','api',0,''); INSERT INTO installations VALUES(1,''); diff --git a/management/server/types/network.go b/management/server/types/network.go index 00082bb41..eb8415264 100644 --- a/management/server/types/network.go +++ b/management/server/types/network.go @@ -1,6 +1,7 @@ package types import ( + "encoding/binary" "math/rand" "net" "sync" @@ -161,24 +162,65 @@ func (n *Network) Copy() *Network { // This method considers already taken IPs and reuses IPs if there are gaps in takenIps // E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3 func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) { - takenIPMap := make(map[string]struct{}) - takenIPMap[ipNet.IP.String()] = struct{}{} + baseIP := ipToUint32(ipNet.IP.Mask(ipNet.Mask)) + totalIPs := uint32(1 << SubnetSize) + + taken := make(map[uint32]struct{}, len(takenIps)+1) + taken[baseIP] = struct{}{} // reserve network IP + taken[baseIP+totalIPs-1] = struct{}{} // reserve broadcast IP + for _, ip := range takenIps { - takenIPMap[ip.String()] = struct{}{} + taken[ipToUint32(ip)] = struct{}{} } - ips, _ := generateIPs(&ipNet, takenIPMap) + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + maxAttempts := (int(totalIPs) - len(taken)) / 100 - if len(ips) == 0 { - return nil, status.Errorf(status.PreconditionFailed, "failed allocating new IP for the ipNet %s - network is out of IPs", ipNet.String()) + for i := 0; i < maxAttempts; i++ { + offset := uint32(rng.Intn(int(totalIPs-2))) + 1 + candidate := baseIP + offset + if _, exists := taken[candidate]; !exists { + return uint32ToIP(candidate), nil + } } - // pick a random IP - s := rand.NewSource(time.Now().Unix()) - r := rand.New(s) - intn := r.Intn(len(ips)) + for offset := uint32(1); offset < totalIPs-1; offset++ { + candidate := baseIP + offset + if _, exists := taken[candidate]; !exists { + return uint32ToIP(candidate), nil + } + } - return ips[intn], nil + return nil, status.Errorf(status.PreconditionFailed, "network %s is out of IPs", ipNet.String()) +} + +func AllocateRandomPeerIP(ipNet net.IPNet) (net.IP, error) { + baseIP := ipToUint32(ipNet.IP.Mask(ipNet.Mask)) + + ones, bits := ipNet.Mask.Size() + hostBits := bits - ones + + totalIPs := uint32(1 << hostBits) + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + offset := uint32(rng.Intn(int(totalIPs-2))) + 1 + + candidate := baseIP + offset + return uint32ToIP(candidate), nil +} + +func ipToUint32(ip net.IP) uint32 { + ip = ip.To4() + if len(ip) < 4 { + return 0 + } + return binary.BigEndian.Uint32(ip) +} + +func uint32ToIP(n uint32) net.IP { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, n) + return ip } // generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list From 57961afe9557ecc651a2f40c53243b8aaf0a2681 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 2 Jul 2025 18:40:07 +0200 Subject: [PATCH 127/213] [doc] Add forum link (#4093) * Add forum link * Add forum link --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1d2a976c2..c3b365694 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@
+ + +
@@ -29,13 +32,13 @@
See
Documentation
- Join our Slack channel + Join our Slack channel or our Community forum

- - New: NetBird Kubernetes Operator + + New: NetBird terraform provider

From 551cb4e467d44d9b83b7c636d33e985590329081 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:04:28 +0200 Subject: [PATCH 128/213] [management] expect specific error types on registration with setup key (#4094) --- management/server/peer.go | 6 +++--- management/server/peer_test.go | 11 ++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index 2c1d8f64c..50967cfb9 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -488,7 +488,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s if addedByUser { user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get user groups: %w", err) + return nil, nil, nil, status.Errorf(status.NotFound, "failed adding new peer: user not found") } groupsToAdd = user.AutoGroups opEvent.InitiatorID = userID @@ -498,12 +498,12 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s // Validate the setup key sk, err := am.Store.GetSetupKeyBySecret(ctx, store.LockingStrengthNone, encodedHashedKey) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get setup key: %w", err) + return nil, nil, nil, status.Errorf(status.NotFound, "couldn't add peer: setup key is invalid") } // we will check key twice for early return if !sk.IsValid() { - return nil, nil, nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key is invalid") + return nil, nil, nil, status.Errorf(status.NotFound, "couldn't add peer: setup key is invalid") } opEvent.InitiatorID = sk.Id diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 3edf7e82c..31439d670 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -27,6 +27,7 @@ import ( "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/settings" + "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/util" @@ -1376,6 +1377,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { existingSetupKeyID string expectedGroupIDsInAccount []string expectAddPeerError bool + errorType status.Type expectedErrorMsgSubstring string }{ { @@ -1388,13 +1390,15 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { name: "Failed registration with setup key not allowing extra DNS labels", existingSetupKeyID: "A2C8E62B-38F5-4553-B31E-DD66C696CEBB", expectAddPeerError: true, + errorType: status.PreconditionFailed, expectedErrorMsgSubstring: "setup key doesn't allow extra DNS labels", }, { name: "Absent setup key", existingSetupKeyID: "AAAAAAAA-38F5-4553-B31E-DD66C696CEBB", expectAddPeerError: true, - expectedErrorMsgSubstring: "failed to get setup key: setup key not found", + errorType: status.NotFound, + expectedErrorMsgSubstring: "couldn't add peer: setup key is invalid", }, } @@ -1419,6 +1423,11 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { if tc.expectAddPeerError { require.Error(t, err, "Expected an error when adding peer with setup key: %s", tc.existingSetupKeyID) assert.Contains(t, err.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch") + e, ok := status.FromError(err) + if !ok { + t.Fatal("Failed to map error") + } + assert.Equal(t, e.Type(), tc.errorType) return } From 2c81cf2c1ea3a55466090fbf9741880c861ace3a Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 3 Jul 2025 09:01:32 +0200 Subject: [PATCH 129/213] [management] Add account onboarding (#4084) This PR introduces a new onboarding feature to handle such flows in the dashboard by defining an AccountOnboarding model, persisting it in the store, exposing CRUD operations in the manager and HTTP handlers, and updating API schemas and tests accordingly. Add AccountOnboarding struct and embed it in Account Extend Store and DefaultAccountManager with onboarding methods and SQL migrations Update HTTP handlers, API types, OpenAPI spec, and add end-to-end tests --- management/server/account.go | 69 +++++++ management/server/account/manager.go | 2 + management/server/account_test.go | 71 +++++++ management/server/http/api/openapi.yml | 19 ++ management/server/http/api/types.gen.go | 17 +- .../handlers/accounts/accounts_handler.go | 32 ++- .../accounts/accounts_handler_test.go | 44 +++- management/server/mock_server/account_mock.go | 193 ++++++++++-------- management/server/status/error.go | 5 + management/server/store/sql_store.go | 28 ++- management/server/store/sql_store_test.go | 74 +++++++ management/server/store/store.go | 2 + management/server/testdata/store.sql | 4 +- management/server/types/account.go | 19 +- 14 files changed, 476 insertions(+), 103 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 8a80aefb6..cd0c933f0 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1204,6 +1204,71 @@ func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID s return am.Store.GetAccountMeta(ctx, store.LockingStrengthShare, accountID) } +// GetAccountOnboarding retrieves the onboarding information for a specific account. +func (am *DefaultAccountManager) GetAccountOnboarding(ctx context.Context, accountID string, userID string) (*types.AccountOnboarding, error) { + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read) + if err != nil { + return nil, status.NewPermissionValidationError(err) + } + + if !allowed { + return nil, status.NewPermissionDeniedError() + } + + onboarding, err := am.Store.GetAccountOnboarding(ctx, accountID) + if err != nil && err.Error() != status.NewAccountOnboardingNotFoundError(accountID).Error() { + log.Errorf("failed to get account onboarding for accountssssssss %s: %v", accountID, err) + return nil, err + } + + if onboarding == nil { + onboarding = &types.AccountOnboarding{ + AccountID: accountID, + } + } + + return onboarding, nil +} + +func (am *DefaultAccountManager) UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) { + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update) + if err != nil { + return nil, fmt.Errorf("failed to validate user permissions: %w", err) + } + + if !allowed { + return nil, status.NewPermissionDeniedError() + } + + oldOnboarding, err := am.Store.GetAccountOnboarding(ctx, accountID) + if err != nil && err.Error() != status.NewAccountOnboardingNotFoundError(accountID).Error() { + return nil, fmt.Errorf("failed to get account onboarding: %w", err) + } + + if oldOnboarding == nil { + oldOnboarding = &types.AccountOnboarding{ + AccountID: accountID, + } + } + + if newOnboarding == nil { + return oldOnboarding, nil + } + + if oldOnboarding.IsEqual(*newOnboarding) { + log.WithContext(ctx).Debugf("no changes in onboarding for account %s", accountID) + return oldOnboarding, nil + } + + newOnboarding.AccountID = accountID + err = am.Store.SaveAccountOnboarding(ctx, newOnboarding) + if err != nil { + return nil, fmt.Errorf("failed to update account onboarding: %w", err) + } + + return newOnboarding, nil +} + func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) { if userAuth.UserId == "" { return "", "", errors.New(emptyUserID) @@ -1726,6 +1791,10 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string, dis PeerInactivityExpiration: types.DefaultPeerInactivityExpiration, RoutingPeerDNSResolutionEnabled: true, }, + Onboarding: types.AccountOnboarding{ + OnboardingFlowPending: true, + SignupFormPending: true, + }, } if err := acc.AddAllGroup(disableDefaultPolicy); err != nil { diff --git a/management/server/account/manager.go b/management/server/account/manager.go index de5031c03..ed17fa5ec 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -39,6 +39,7 @@ type Manager interface { GetSetupKey(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) + GetAccountOnboarding(ctx context.Context, accountID string, userID string) (*types.AccountOnboarding, error) AccountExists(ctx context.Context, accountID string) (bool, error) GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) @@ -89,6 +90,7 @@ type Manager interface { SaveDNSSettings(ctx context.Context, accountID string, userID string, dnsSettingsToSave *types.DNSSettings) error GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) + UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API GetAllConnectedPeers() (map[string]struct{}, error) diff --git a/management/server/account_test.go b/management/server/account_test.go index 60353389f..fcd40b082 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3448,3 +3448,74 @@ func TestPropagateUserGroupMemberships(t *testing.T) { } }) } + +func TestDefaultAccountManager_GetAccountOnboarding(t *testing.T) { + manager, err := createManager(t) + require.NoError(t, err) + + account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "") + require.NoError(t, err) + + t.Run("should return account onboarding when onboarding exist", func(t *testing.T) { + onboarding, err := manager.GetAccountOnboarding(context.Background(), account.Id, userID) + require.NoError(t, err) + require.NotNil(t, onboarding) + assert.Equal(t, account.Id, onboarding.AccountID) + assert.Equal(t, true, onboarding.OnboardingFlowPending) + assert.Equal(t, true, onboarding.SignupFormPending) + if onboarding.UpdatedAt.IsZero() { + t.Errorf("Onboarding was not retrieved from the store") + } + }) + + t.Run("should return account onboarding when onboard don't exist", func(t *testing.T) { + account.Id = "with-zero-onboarding" + account.Onboarding = types.AccountOnboarding{} + err = manager.Store.SaveAccount(context.Background(), account) + require.NoError(t, err) + onboarding, err := manager.GetAccountOnboarding(context.Background(), account.Id, userID) + require.NoError(t, err) + require.NotNil(t, onboarding) + _, err = manager.Store.GetAccountOnboarding(context.Background(), account.Id) + require.Error(t, err, "should return error when onboarding is not set") + }) +} + +func TestDefaultAccountManager_UpdateAccountOnboarding(t *testing.T) { + manager, err := createManager(t) + require.NoError(t, err) + + account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "") + require.NoError(t, err) + + onboarding := &types.AccountOnboarding{ + OnboardingFlowPending: true, + SignupFormPending: true, + } + + t.Run("update onboarding with no change", func(t *testing.T) { + updated, err := manager.UpdateAccountOnboarding(context.Background(), account.Id, userID, onboarding) + require.NoError(t, err) + assert.Equal(t, onboarding.OnboardingFlowPending, updated.OnboardingFlowPending) + assert.Equal(t, onboarding.SignupFormPending, updated.SignupFormPending) + if updated.UpdatedAt.IsZero() { + t.Errorf("Onboarding was updated in the store") + } + }) + + onboarding.OnboardingFlowPending = false + onboarding.SignupFormPending = false + + t.Run("update onboarding", func(t *testing.T) { + updated, err := manager.UpdateAccountOnboarding(context.Background(), account.Id, userID, onboarding) + require.NoError(t, err) + require.NotNil(t, updated) + assert.Equal(t, onboarding.OnboardingFlowPending, updated.OnboardingFlowPending) + assert.Equal(t, onboarding.SignupFormPending, updated.SignupFormPending) + }) + + t.Run("update onboarding with no onboarding", func(t *testing.T) { + _, err = manager.UpdateAccountOnboarding(context.Background(), account.Id, userID, nil) + require.NoError(t, err) + }) +} diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 1c5ca9b04..f8c2b9854 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -60,6 +60,8 @@ components: description: Account creator type: string example: google-oauth2|277474792786460067937 + onboarding: + $ref: '#/components/schemas/AccountOnboarding' required: - id - settings @@ -67,6 +69,21 @@ components: - domain_category - created_at - created_by + - onboarding + AccountOnboarding: + type: object + properties: + signup_form_pending: + description: Indicates whether the account signup form is pending + type: boolean + example: true + onboarding_flow_pending: + description: Indicates whether the account onboarding flow is pending + type: boolean + example: false + required: + - signup_form_pending + - onboarding_flow_pending AccountSettings: type: object properties: @@ -153,6 +170,8 @@ components: properties: settings: $ref: '#/components/schemas/AccountSettings' + onboarding: + $ref: '#/components/schemas/AccountOnboarding' required: - settings User: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index d27fd2a57..a9f17aab4 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -250,8 +250,9 @@ type Account struct { DomainCategory string `json:"domain_category"` // Id Account ID - Id string `json:"id"` - Settings AccountSettings `json:"settings"` + Id string `json:"id"` + Onboarding AccountOnboarding `json:"onboarding"` + Settings AccountSettings `json:"settings"` } // AccountExtraSettings defines model for AccountExtraSettings. @@ -266,9 +267,19 @@ type AccountExtraSettings struct { PeerApprovalEnabled bool `json:"peer_approval_enabled"` } +// AccountOnboarding defines model for AccountOnboarding. +type AccountOnboarding struct { + // OnboardingFlowPending Indicates whether the account onboarding flow is pending + OnboardingFlowPending bool `json:"onboarding_flow_pending"` + + // SignupFormPending Indicates whether the account signup form is pending + SignupFormPending bool `json:"signup_form_pending"` +} + // AccountRequest defines model for AccountRequest. type AccountRequest struct { - Settings AccountSettings `json:"settings"` + Onboarding *AccountOnboarding `json:"onboarding,omitempty"` + Settings AccountSettings `json:"settings"` } // AccountSettings defines model for AccountSettings. diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index dfc782b3f..ab59434d1 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -59,7 +59,13 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { return } - resp := toAccountResponse(accountID, settings, meta) + onboarding, err := h.accountManager.GetAccountOnboarding(r.Context(), accountID, userID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + resp := toAccountResponse(accountID, settings, meta, onboarding) util.WriteJSONObject(r.Context(), w, []*api.Account{resp}) } @@ -126,6 +132,20 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { settings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled } + var onboarding *types.AccountOnboarding + if req.Onboarding != nil { + onboarding = &types.AccountOnboarding{ + OnboardingFlowPending: req.Onboarding.OnboardingFlowPending, + SignupFormPending: req.Onboarding.SignupFormPending, + } + } + + updatedOnboarding, err := h.accountManager.UpdateAccountOnboarding(r.Context(), accountID, userID, onboarding) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + updatedSettings, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings) if err != nil { util.WriteError(r.Context(), err, w) @@ -138,7 +158,7 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { return } - resp := toAccountResponse(accountID, updatedSettings, meta) + resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding) util.WriteJSONObject(r.Context(), w, &resp) } @@ -167,7 +187,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) { util.WriteJSONObject(r.Context(), w, util.EmptyObject{}) } -func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta) *api.Account { +func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta, onboarding *types.AccountOnboarding) *api.Account { jwtAllowGroups := settings.JWTAllowGroups if jwtAllowGroups == nil { jwtAllowGroups = []string{} @@ -188,6 +208,11 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A DnsDomain: &settings.DNSDomain, } + apiOnboarding := api.AccountOnboarding{ + OnboardingFlowPending: onboarding.OnboardingFlowPending, + SignupFormPending: onboarding.SignupFormPending, + } + if settings.Extra != nil { apiSettings.Extra = &api.AccountExtraSettings{ PeerApprovalEnabled: settings.Extra.PeerApprovalEnabled, @@ -203,5 +228,6 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A CreatedBy: meta.CreatedBy, Domain: meta.Domain, DomainCategory: meta.DomainCategory, + Onboarding: apiOnboarding, } } diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index a18798743..dbf0c22bc 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -54,6 +54,18 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler { GetAccountMetaFunc: func(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) { return account.GetMeta(), nil }, + GetAccountOnboardingFunc: func(ctx context.Context, accountID string, userID string) (*types.AccountOnboarding, error) { + return &types.AccountOnboarding{ + OnboardingFlowPending: true, + SignupFormPending: true, + }, nil + }, + UpdateAccountOnboardingFunc: func(ctx context.Context, accountID, userID string, onboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) { + return &types.AccountOnboarding{ + OnboardingFlowPending: true, + SignupFormPending: true, + }, nil + }, }, settingsManager: settingsMockManager, } @@ -117,7 +129,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, - requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true}}"), + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), expectedStatus: http.StatusOK, expectedSettings: api.AccountSettings{ PeerLoginExpiration: 15552000, @@ -139,7 +151,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, - requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\",\"jwt_allow_groups\":[\"test\"],\"regular_users_view_blocked\":true}}"), + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\",\"jwt_allow_groups\":[\"test\"],\"regular_users_view_blocked\":true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), expectedStatus: http.StatusOK, expectedSettings: api.AccountSettings{ PeerLoginExpiration: 15552000, @@ -161,7 +173,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, - requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true,\"regular_users_view_blocked\":true}}"), + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true,\"regular_users_view_blocked\":true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), expectedStatus: http.StatusOK, expectedSettings: api.AccountSettings{ PeerLoginExpiration: 554400, @@ -178,12 +190,34 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedArray: false, expectedID: accountID, }, + { + name: "PutAccount OK without onboarding", + expectedBody: true, + requestType: http.MethodPut, + requestPath: "/api/accounts/" + accountID, + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\",\"jwt_allow_groups\":[\"test\"],\"regular_users_view_blocked\":true}}"), + expectedStatus: http.StatusOK, + expectedSettings: api.AccountSettings{ + PeerLoginExpiration: 15552000, + PeerLoginExpirationEnabled: false, + GroupsPropagationEnabled: br(false), + JwtGroupsClaimName: sr("roles"), + JwtGroupsEnabled: br(true), + JwtAllowGroups: &[]string{"test"}, + RegularUsersViewBlocked: true, + RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), + DnsDomain: sr(""), + }, + expectedArray: false, + expectedID: accountID, + }, { name: "Update account failure with high peer_login_expiration more than 180 days", expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, - requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552001,\"peer_login_expiration_enabled\": true}}"), + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552001,\"peer_login_expiration_enabled\": true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), expectedStatus: http.StatusUnprocessableEntity, expectedArray: false, }, @@ -192,7 +226,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, - requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 3599,\"peer_login_expiration_enabled\": true}}"), + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 3599,\"peer_login_expiration_enabled\": true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), expectedStatus: http.StatusUnprocessableEntity, expectedArray: false, }, diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 3caa6744a..8837f9f50 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -30,94 +30,95 @@ type MockAccountManager struct { GetAccountFunc func(ctx context.Context, accountID string) (*types.Account, error) CreateSetupKeyFunc func(ctx context.Context, accountId string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) - GetSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) - AccountExistsFunc func(ctx context.Context, accountID string) (bool, error) - GetAccountIDByUserIdFunc func(ctx context.Context, userId, domain string) (string, error) - GetUserFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) - ListUsersFunc func(ctx context.Context, accountID string) ([]*types.User, error) - GetPeersFunc func(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) - MarkPeerConnectedFunc func(ctx context.Context, peerKey string, connected bool, realIP net.IP) error - SyncAndMarkPeerFunc func(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - DeletePeerFunc func(ctx context.Context, accountID, peerKey, userID string) error - GetNetworkMapFunc func(ctx context.Context, peerKey string) (*types.NetworkMap, error) - GetPeerNetworkFunc func(ctx context.Context, peerKey string) (*types.Network, error) - AddPeerFunc func(ctx context.Context, setupKey string, userId string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - GetGroupFunc func(ctx context.Context, accountID, groupID, userID string) (*types.Group, error) - GetAllGroupsFunc func(ctx context.Context, accountID, userID string) ([]*types.Group, error) - GetGroupByNameFunc func(ctx context.Context, accountID, groupName string) (*types.Group, error) - SaveGroupFunc func(ctx context.Context, accountID, userID string, group *types.Group, create bool) error - SaveGroupsFunc func(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error - DeleteGroupFunc func(ctx context.Context, accountID, userId, groupID string) error - DeleteGroupsFunc func(ctx context.Context, accountId, userId string, groupIDs []string) error - GroupAddPeerFunc func(ctx context.Context, accountID, groupID, peerID string) error - GroupDeletePeerFunc func(ctx context.Context, accountID, groupID, peerID string) error - GetPeerGroupsFunc func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) - DeleteRuleFunc func(ctx context.Context, accountID, ruleID, userID string) error - GetPolicyFunc func(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error) - SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy, create bool) (*types.Policy, error) - DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error - ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error) - GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) - UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error - UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) - CreateRouteFunc func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) - GetRouteFunc func(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error) - SaveRouteFunc func(ctx context.Context, accountID string, userID string, route *route.Route) error - DeleteRouteFunc func(ctx context.Context, accountID string, routeID route.ID, userID string) error - ListRoutesFunc func(ctx context.Context, accountID, userID string) ([]*route.Route, error) - SaveSetupKeyFunc func(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) - ListSetupKeysFunc func(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) - SaveUserFunc func(ctx context.Context, accountID, userID string, user *types.User) (*types.UserInfo, error) - SaveOrAddUserFunc func(ctx context.Context, accountID, userID string, user *types.User, addIfNotExists bool) (*types.UserInfo, error) - SaveOrAddUsersFunc func(ctx context.Context, accountID, initiatorUserID string, update []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) - DeleteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error - DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error - CreatePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) - DeletePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) error - GetPATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) (*types.PersonalAccessToken, error) - GetAllPATsFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string) ([]*types.PersonalAccessToken, error) - GetNameServerGroupFunc func(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) - CreateNameServerGroupFunc func(ctx context.Context, accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) - SaveNameServerGroupFunc func(ctx context.Context, accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error - DeleteNameServerGroupFunc func(ctx context.Context, accountID, nsGroupID, userID string) error - ListNameServerGroupsFunc func(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error) - CreateUserFunc func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) - GetAccountIDFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) - DeleteAccountFunc func(ctx context.Context, accountID, userID string) error - GetDNSDomainFunc func(settings *types.Settings) string - StoreEventFunc func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) - GetEventsFunc func(ctx context.Context, accountID, userID string) ([]*activity.Event, error) - GetDNSSettingsFunc func(ctx context.Context, accountID, userID string) (*types.DNSSettings, error) - SaveDNSSettingsFunc func(ctx context.Context, accountID, userID string, dnsSettingsToSave *types.DNSSettings) error - GetPeerFunc func(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) - UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) - LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error - GetAllConnectedPeersFunc func() (map[string]struct{}, error) - HasConnectedChannelFunc func(peerID string) bool - GetExternalCacheManagerFunc func() account.ExternalCacheManager - GetPostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error) - SavePostureChecksFunc func(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) - DeletePostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) error - ListPostureChecksFunc func(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) - GetIdpManagerFunc func() idp.Manager - UpdateIntegratedValidatorGroupsFunc func(ctx context.Context, accountID string, userID string, groups []string) error - GroupValidationFunc func(ctx context.Context, accountId string, groups []string) (bool, error) - SyncPeerMetaFunc func(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error - FindExistingPostureCheckFunc func(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) - GetAccountIDForPeerKeyFunc func(ctx context.Context, peerKey string) (string, error) - GetAccountByIDFunc func(ctx context.Context, accountID string, userID string) (*types.Account, error) - GetUserByIDFunc func(ctx context.Context, id string) (*types.User, error) - GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error) - DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error - BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) - GetStoreFunc func() store.Store - UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error) - GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error) - GetCurrentUserInfoFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) - GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error) - + GetSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) + AccountExistsFunc func(ctx context.Context, accountID string) (bool, error) + GetAccountIDByUserIdFunc func(ctx context.Context, userId, domain string) (string, error) + GetUserFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) + ListUsersFunc func(ctx context.Context, accountID string) ([]*types.User, error) + GetPeersFunc func(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) + MarkPeerConnectedFunc func(ctx context.Context, peerKey string, connected bool, realIP net.IP) error + SyncAndMarkPeerFunc func(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + DeletePeerFunc func(ctx context.Context, accountID, peerKey, userID string) error + GetNetworkMapFunc func(ctx context.Context, peerKey string) (*types.NetworkMap, error) + GetPeerNetworkFunc func(ctx context.Context, peerKey string) (*types.Network, error) + AddPeerFunc func(ctx context.Context, setupKey string, userId string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + GetGroupFunc func(ctx context.Context, accountID, groupID, userID string) (*types.Group, error) + GetAllGroupsFunc func(ctx context.Context, accountID, userID string) ([]*types.Group, error) + GetGroupByNameFunc func(ctx context.Context, accountID, groupName string) (*types.Group, error) + SaveGroupFunc func(ctx context.Context, accountID, userID string, group *types.Group, create bool) error + SaveGroupsFunc func(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error + DeleteGroupFunc func(ctx context.Context, accountID, userId, groupID string) error + DeleteGroupsFunc func(ctx context.Context, accountId, userId string, groupIDs []string) error + GroupAddPeerFunc func(ctx context.Context, accountID, groupID, peerID string) error + GroupDeletePeerFunc func(ctx context.Context, accountID, groupID, peerID string) error + GetPeerGroupsFunc func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) + DeleteRuleFunc func(ctx context.Context, accountID, ruleID, userID string) error + GetPolicyFunc func(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error) + SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy, create bool) (*types.Policy, error) + DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error + ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error) + GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) + UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error + UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) + CreateRouteFunc func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) + GetRouteFunc func(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error) + SaveRouteFunc func(ctx context.Context, accountID string, userID string, route *route.Route) error + DeleteRouteFunc func(ctx context.Context, accountID string, routeID route.ID, userID string) error + ListRoutesFunc func(ctx context.Context, accountID, userID string) ([]*route.Route, error) + SaveSetupKeyFunc func(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) + ListSetupKeysFunc func(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) + SaveUserFunc func(ctx context.Context, accountID, userID string, user *types.User) (*types.UserInfo, error) + SaveOrAddUserFunc func(ctx context.Context, accountID, userID string, user *types.User, addIfNotExists bool) (*types.UserInfo, error) + SaveOrAddUsersFunc func(ctx context.Context, accountID, initiatorUserID string, update []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) + DeleteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error + DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error + CreatePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) + DeletePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) error + GetPATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) (*types.PersonalAccessToken, error) + GetAllPATsFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string) ([]*types.PersonalAccessToken, error) + GetNameServerGroupFunc func(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) + CreateNameServerGroupFunc func(ctx context.Context, accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) + SaveNameServerGroupFunc func(ctx context.Context, accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error + DeleteNameServerGroupFunc func(ctx context.Context, accountID, nsGroupID, userID string) error + ListNameServerGroupsFunc func(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error) + CreateUserFunc func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) + GetAccountIDFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) + DeleteAccountFunc func(ctx context.Context, accountID, userID string) error + GetDNSDomainFunc func(settings *types.Settings) string + StoreEventFunc func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) + GetEventsFunc func(ctx context.Context, accountID, userID string) ([]*activity.Event, error) + GetDNSSettingsFunc func(ctx context.Context, accountID, userID string) (*types.DNSSettings, error) + SaveDNSSettingsFunc func(ctx context.Context, accountID, userID string, dnsSettingsToSave *types.DNSSettings) error + GetPeerFunc func(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) + UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) + LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error + GetAllConnectedPeersFunc func() (map[string]struct{}, error) + HasConnectedChannelFunc func(peerID string) bool + GetExternalCacheManagerFunc func() account.ExternalCacheManager + GetPostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error) + SavePostureChecksFunc func(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) + DeletePostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) error + ListPostureChecksFunc func(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) + GetIdpManagerFunc func() idp.Manager + UpdateIntegratedValidatorGroupsFunc func(ctx context.Context, accountID string, userID string, groups []string) error + GroupValidationFunc func(ctx context.Context, accountId string, groups []string) (bool, error) + SyncPeerMetaFunc func(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error + FindExistingPostureCheckFunc func(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) + GetAccountIDForPeerKeyFunc func(ctx context.Context, peerKey string) (string, error) + GetAccountByIDFunc func(ctx context.Context, accountID string, userID string) (*types.Account, error) + GetUserByIDFunc func(ctx context.Context, id string) (*types.User, error) + GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error) + DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error + BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) + GetStoreFunc func() store.Store + UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error) + GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error) + GetCurrentUserInfoFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) + GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error) + GetAccountOnboardingFunc func(ctx context.Context, accountID, userID string) (*types.AccountOnboarding, error) + UpdateAccountOnboardingFunc func(ctx context.Context, accountID, userID string, onboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) GetOrCreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) } @@ -814,6 +815,22 @@ func (am *MockAccountManager) GetAccountMeta(ctx context.Context, accountID stri return nil, status.Errorf(codes.Unimplemented, "method GetAccountMeta is not implemented") } +// GetAccountOnboarding mocks GetAccountOnboarding of the AccountManager interface +func (am *MockAccountManager) GetAccountOnboarding(ctx context.Context, accountID string, userID string) (*types.AccountOnboarding, error) { + if am.GetAccountOnboardingFunc != nil { + return am.GetAccountOnboardingFunc(ctx, accountID, userID) + } + return nil, status.Errorf(codes.Unimplemented, "method GetAccountOnboarding is not implemented") +} + +// UpdateAccountOnboarding mocks UpdateAccountOnboarding of the AccountManager interface +func (am *MockAccountManager) UpdateAccountOnboarding(ctx context.Context, accountID string, userID string, onboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) { + if am.UpdateAccountOnboardingFunc != nil { + return am.UpdateAccountOnboardingFunc(ctx, accountID, userID, onboarding) + } + return nil, status.Errorf(codes.Unimplemented, "method UpdateAccountOnboarding is not implemented") +} + // GetUserByID mocks GetUserByID of the AccountManager interface func (am *MockAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) { if am.GetUserByIDFunc != nil { diff --git a/management/server/status/error.go b/management/server/status/error.go index 5a6f6d1a7..47c236e93 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -90,6 +90,11 @@ func NewAccountNotFoundError(accountKey string) error { return Errorf(NotFound, "account not found: %s", accountKey) } +// NewAccountOnboardingNotFoundError creates a new Error with NotFound type for a missing account onboarding +func NewAccountOnboardingNotFoundError(accountKey string) error { + return Errorf(NotFound, "account onboarding not found: %s", accountKey) +} + // NewPeerNotPartOfAccountError creates a new Error with PermissionDenied type for a peer not being part of an account func NewPeerNotPartOfAccountError() error { return Errorf(PermissionDenied, "peer is not part of this account") diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 197255ab6..baee4ad28 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -99,7 +99,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met &types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.Group{}, &types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, &installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{}, - &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, + &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{}, ) if err != nil { return nil, fmt.Errorf("auto migratePreAuto: %w", err) @@ -728,6 +728,32 @@ func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStren return &accountMeta, nil } +// GetAccountOnboarding retrieves the onboarding information for a specific account. +func (s *SqlStore) GetAccountOnboarding(ctx context.Context, accountID string) (*types.AccountOnboarding, error) { + var accountOnboarding types.AccountOnboarding + result := s.db.Model(&accountOnboarding).First(&accountOnboarding, accountIDCondition, accountID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewAccountOnboardingNotFoundError(accountID) + } + log.WithContext(ctx).Errorf("error when getting account onboarding %s from the store: %s", accountID, result.Error) + return nil, status.NewGetAccountFromStoreError(result.Error) + } + + return &accountOnboarding, nil +} + +// SaveAccountOnboarding updates the onboarding information for a specific account. +func (s *SqlStore) SaveAccountOnboarding(ctx context.Context, onboarding *types.AccountOnboarding) error { + result := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(onboarding) + if result.Error != nil { + log.WithContext(ctx).Errorf("error when saving account onboarding %s in the store: %s", onboarding.AccountID, result.Error) + return status.Errorf(status.Internal, "error when saving account onboarding %s in the store: %s", onboarding.AccountID, result.Error) + } + + return nil +} + func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) { start := time.Now() defer func() { diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 928486ab4..738c5a28c 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -354,9 +354,16 @@ func TestSqlite_DeleteAccount(t *testing.T) { t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") } + o, err := store.GetAccountOnboarding(context.Background(), account.Id) + require.NoError(t, err) + require.Equal(t, o.AccountID, account.Id) + err = store.DeleteAccount(context.Background(), account) require.NoError(t, err) + _, err = store.GetAccountOnboarding(context.Background(), account.Id) + require.Error(t, err, "expecting error after removing DeleteAccount when getting onboarding") + if len(store.GetAllAccounts(context.Background())) != 0 { t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") } @@ -414,12 +421,21 @@ func Test_GetAccount(t *testing.T) { account, err := store.GetAccount(context.Background(), id) require.NoError(t, err) require.Equal(t, id, account.Id, "account id should match") + require.Equal(t, false, account.Onboarding.OnboardingFlowPending) + + id = "9439-34653001fc3b-bf1c8084-ba50-4ce7" + + account, err = store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") + require.Equal(t, true, account.Onboarding.OnboardingFlowPending) _, err = store.GetAccount(context.Background(), "non-existing-account") assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } @@ -2096,6 +2112,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty PeerInactivityExpirationEnabled: false, PeerInactivityExpiration: types.DefaultPeerInactivityExpiration, }, + Onboarding: types.AccountOnboarding{SignupFormPending: true, OnboardingFlowPending: true}, } if err := acc.AddAllGroup(false); err != nil { @@ -3440,6 +3457,63 @@ func TestSqlStore_GetAccountMeta(t *testing.T) { require.Equal(t, time.Date(2024, time.October, 2, 14, 1, 38, 210000000, time.UTC), accountMeta.CreatedAt.UTC()) } +func TestSqlStore_GetAccountOnboarding(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "9439-34653001fc3b-bf1c8084-ba50-4ce7" + a, err := store.GetAccount(context.Background(), accountID) + require.NoError(t, err) + t.Logf("Onboarding: %+v", a.Onboarding) + err = store.SaveAccount(context.Background(), a) + require.NoError(t, err) + onboarding, err := store.GetAccountOnboarding(context.Background(), accountID) + require.NoError(t, err) + require.NotNil(t, onboarding) + require.Equal(t, accountID, onboarding.AccountID) + require.Equal(t, time.Date(2024, time.October, 2, 14, 1, 38, 210000000, time.UTC), onboarding.CreatedAt.UTC()) +} + +func TestSqlStore_SaveAccountOnboarding(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + t.Run("New onboarding should be saved correctly", func(t *testing.T) { + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + onboarding := &types.AccountOnboarding{ + AccountID: accountID, + SignupFormPending: true, + OnboardingFlowPending: true, + } + + err = store.SaveAccountOnboarding(context.Background(), onboarding) + require.NoError(t, err) + + savedOnboarding, err := store.GetAccountOnboarding(context.Background(), accountID) + require.NoError(t, err) + require.Equal(t, onboarding.SignupFormPending, savedOnboarding.SignupFormPending) + require.Equal(t, onboarding.OnboardingFlowPending, savedOnboarding.OnboardingFlowPending) + }) + + t.Run("Existing onboarding should be updated correctly", func(t *testing.T) { + accountID := "9439-34653001fc3b-bf1c8084-ba50-4ce7" + onboarding, err := store.GetAccountOnboarding(context.Background(), accountID) + require.NoError(t, err) + + onboarding.OnboardingFlowPending = !onboarding.OnboardingFlowPending + onboarding.SignupFormPending = !onboarding.SignupFormPending + + err = store.SaveAccountOnboarding(context.Background(), onboarding) + require.NoError(t, err) + + savedOnboarding, err := store.GetAccountOnboarding(context.Background(), accountID) + require.NoError(t, err) + require.Equal(t, onboarding.SignupFormPending, savedOnboarding.SignupFormPending) + require.Equal(t, onboarding.OnboardingFlowPending, savedOnboarding.OnboardingFlowPending) + }) +} + func TestSqlStore_GetAnyAccountID(t *testing.T) { t.Run("should return account ID when accounts exist", func(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) diff --git a/management/server/store/store.go b/management/server/store/store.go index 30ff1549d..b3254c4c9 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -52,6 +52,7 @@ type Store interface { GetAllAccounts(ctx context.Context) []*types.Account GetAccount(ctx context.Context, accountID string) (*types.Account, error) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) + GetAccountOnboarding(ctx context.Context, accountID string) (*types.AccountOnboarding, error) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) @@ -74,6 +75,7 @@ type Store interface { SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error SaveAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.Settings) error CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) + SaveAccountOnboarding(ctx context.Context, onboarding *types.AccountOnboarding) error GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql index 4b126c618..a21783857 100644 --- a/management/server/testdata/store.sql +++ b/management/server/testdata/store.sql @@ -1,4 +1,5 @@ CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`)); +CREATE TABLE `account_onboardings` (`account_id` text, `created_at` datetime,`updated_at` datetime, `onboarding_flow_pending` numeric, `signup_form_pending` numeric, PRIMARY KEY (`account_id`)); CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); @@ -38,7 +39,8 @@ CREATE INDEX `idx_networks_id` ON `networks`(`id`); CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`); INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); -INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,''); +INSERT INTO accounts VALUES('9439-34653001fc3b-bf1c8084-ba50-4ce7','90d6-0242ac120003-edafee4e-63fb-11ec','2024-10-02 16:01:38.210000+02:00','test2.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); +INSERT INTO account_onboardings VALUES('9439-34653001fc3b-bf1c8084-ba50-4ce7','2024-10-02 16:01:38.210000+02:00','2021-08-19 20:46:20.005936822+02:00',1,0);INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,''); INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0); INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); diff --git a/management/server/types/account.go b/management/server/types/account.go index 5a62ee4c6..f0887be07 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -82,11 +82,11 @@ type Account struct { DNSSettings DNSSettings `gorm:"embedded;embeddedPrefix:dns_settings_"` PostureChecks []*posture.Checks `gorm:"foreignKey:AccountID;references:id"` // Settings is a dictionary of Account settings - Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` - + Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` Networks []*networkTypes.Network `gorm:"foreignKey:AccountID;references:id"` NetworkRouters []*routerTypes.NetworkRouter `gorm:"foreignKey:AccountID;references:id"` NetworkResources []*resourceTypes.NetworkResource `gorm:"foreignKey:AccountID;references:id"` + Onboarding AccountOnboarding `gorm:"foreignKey:AccountID;references:id;constraint:OnDelete:CASCADE"` } // Subclass used in gorm to only load network and not whole account @@ -104,6 +104,20 @@ type AccountSettings struct { Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` } +type AccountOnboarding struct { + AccountID string `gorm:"primaryKey"` + OnboardingFlowPending bool + SignupFormPending bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// IsEqual compares two AccountOnboarding objects and returns true if they are equal +func (o AccountOnboarding) IsEqual(onboarding AccountOnboarding) bool { + return o.OnboardingFlowPending == onboarding.OnboardingFlowPending && + o.SignupFormPending == onboarding.SignupFormPending +} + // GetRoutesToSync returns the enabled routes for the peer ID and the routes // from the ACL peers that have distribution groups associated with the peer ID. // Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID. @@ -866,6 +880,7 @@ func (a *Account) Copy() *Account { Networks: nets, NetworkRouters: networkRouters, NetworkResources: networkResources, + Onboarding: a.Onboarding, } } From 9afbecb7acf0af5876bfd4c47e95fba3e15d4289 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:02:53 +0200 Subject: [PATCH 130/213] [client] Use unique sequence numbers for bsd routes (#4081) updates the route manager on Unix to use a unique, incrementing sequence number for each route message instead of a fixed value. Replace the static Seq: 1 with a call to r.getSeq() Add an atomic seq field and the getSeq method in SysOps --- client/internal/routemanager/systemops/systemops.go | 9 +++++++++ client/internal/routemanager/systemops/systemops_unix.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index 8caf22f81..106c520da 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -5,6 +5,7 @@ import ( "net" "net/netip" "sync" + "sync/atomic" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" @@ -52,6 +53,9 @@ type SysOps struct { mu sync.Mutex // notifier is used to notify the system of route changes (also used on mobile) notifier *notifier.Notifier + // seq is an atomic counter for generating unique sequence numbers for route messages + //nolint:unused // only used on BSD systems + seq atomic.Uint32 } func NewSysOps(wgInterface wgIface, notifier *notifier.Notifier) *SysOps { @@ -61,6 +65,11 @@ func NewSysOps(wgInterface wgIface, notifier *notifier.Notifier) *SysOps { } } +//nolint:unused // only used on BSD systems +func (r *SysOps) getSeq() int { + return int(r.seq.Add(1)) +} + func (r *SysOps) validateRoute(prefix netip.Prefix) error { addr := prefix.Addr() diff --git a/client/internal/routemanager/systemops/systemops_unix.go b/client/internal/routemanager/systemops/systemops_unix.go index f284e131b..46e5ca915 100644 --- a/client/internal/routemanager/systemops/systemops_unix.go +++ b/client/internal/routemanager/systemops/systemops_unix.go @@ -108,7 +108,7 @@ func (r *SysOps) buildRouteMessage(action int, prefix netip.Prefix, nexthop Next Type: action, Flags: unix.RTF_UP, Version: unix.RTM_VERSION, - Seq: 1, + Seq: r.getSeq(), } const numAddrs = unix.RTAX_NETMASK + 1 From c4ed11d447dfe5a7dffb209f6071aaa5b915c7ea Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 3 Jul 2025 16:22:18 +0200 Subject: [PATCH 131/213] [client] Avoid logging setup keys on error message (#3962) --- client/internal/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/internal/login.go b/client/internal/login.go index bbf844eb3..53fa17d90 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -148,7 +148,7 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm. ) loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey, config.DNSLabels) if err != nil { - log.Errorf("failed registering peer %v,%s", err, validSetupKey.String()) + log.Errorf("failed registering peer %v", err) return nil, err } From 996b8c600c94c35159ffab260470a4556cae0499 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Thu, 3 Jul 2025 16:36:36 +0200 Subject: [PATCH 132/213] [management] replace `invalid user` with a clear error message about mismatched logins (#4097) --- management/server/peer.go | 4 ++-- management/server/status/error.go | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index 50967cfb9..1dd390dd9 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -857,7 +857,7 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer if login.UserID != "" { if peer.UserID != login.UserID { log.Warnf("user mismatch when logging in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, login.UserID) - return status.Errorf(status.Unauthenticated, "invalid user") + return status.NewPeerLoginMismatchError() } changed, err := am.handleUserPeer(ctx, transaction, peer, settings) @@ -1106,7 +1106,7 @@ func checkAuth(ctx context.Context, loginUserID string, peer *nbpeer.Peer) error } if peer.UserID != loginUserID { log.WithContext(ctx).Warnf("user mismatch when logging in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, loginUserID) - return status.Errorf(status.Unauthenticated, "can't login with this credentials") + return status.NewPeerLoginMismatchError() } return nil } diff --git a/management/server/status/error.go b/management/server/status/error.go index 47c236e93..e3cc27b29 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -110,11 +110,16 @@ func NewUserBlockedError() error { return Errorf(PermissionDenied, "user is blocked") } -// NewPeerNotRegisteredError creates a new Error with NotFound type for a missing peer +// NewPeerNotRegisteredError creates a new Error with Unauthenticated type unregistered peer func NewPeerNotRegisteredError() error { return Errorf(Unauthenticated, "peer is not registered") } +// NewPeerLoginMismatchError creates a new Error with Unauthenticated type for a peer that is already registered for another user +func NewPeerLoginMismatchError() error { + return Errorf(Unauthenticated, "peer is already registered by a different User or a Setup Key") +} + // NewPeerLoginExpiredError creates a new Error with PermissionDenied type for an expired peer func NewPeerLoginExpiredError() error { return Errorf(PermissionDenied, "peer login has expired, please log in once more") From f603ddf35ec8729b98d384692727d6cb2e2898a6 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Fri, 4 Jul 2025 08:44:08 +0100 Subject: [PATCH 133/213] management: fix store get account peers without lock (#4092) --- management/server/store/sql_store.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index baee4ad28..e380a7da7 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -1439,7 +1439,11 @@ func (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStreng // GetAccountPeers retrieves peers for an account. func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) { var peers []*nbpeer.Peer - query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Where(accountIDCondition, accountID) + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + query := tx.Where(accountIDCondition, accountID) if nameFilter != "" { query = query.Where("name LIKE ?", "%"+nameFilter+"%") From 8c09a55057ee30406af600d72bbd0ffb9b357667 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 4 Jul 2025 10:51:58 +0300 Subject: [PATCH 134/213] [management] Log user id on account mismatch (#4101) --- management/server/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/user.go b/management/server/user.go index a1f1c46d5..7d8382978 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -550,7 +550,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, ctx, transaction, groupsMap, accountID, initiatorUserID, initiatorUser, update, addIfNotExists, settings, ) if err != nil { - return fmt.Errorf("failed to process user update: %w", err) + return fmt.Errorf("failed to process update for user %s: %w", update.Id, err) } usersToSave = append(usersToSave, updatedUser) addUserEvents = append(addUserEvents, userEvents...) From 77ec32dd6fc87f8498c4bff55dba12f31dc9b2e1 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:43:11 +0200 Subject: [PATCH 135/213] [client] Implement dns routes for Android (#3989) --- client/android/client.go | 8 +- .../uspfilter/{uspfilter.go => filter.go} | 54 ++- ...ter_bench_test.go => filter_bench_test.go} | 80 ++-- ...r_filter_test.go => filter_filter_test.go} | 6 +- .../{uspfilter_test.go => filter_test.go} | 14 +- client/firewall/uspfilter/nat.go | 408 +++++++++++++++++ client/firewall/uspfilter/nat_bench_test.go | 416 ++++++++++++++++++ client/firewall/uspfilter/nat_test.go | 145 ++++++ client/firewall/uspfilter/tracer.go | 2 +- client/iface/device/device_filter.go | 12 +- client/iface/device/device_filter_test.go | 4 +- client/iface/mocks/filter.go | 24 +- client/iface/mocks/iface/mocks/filter.go | 24 +- client/internal/dns/server_test.go | 2 +- client/internal/engine.go | 44 +- client/internal/routemanager/client/client.go | 42 +- .../routemanager/client/client_test.go | 7 +- client/internal/routemanager/common/params.go | 28 ++ .../routemanager/dnsinterceptor/handler.go | 306 ++++++++++--- client/internal/routemanager/dynamic/route.go | 25 +- client/internal/routemanager/fakeip/fakeip.go | 93 ++++ .../routemanager/fakeip/fakeip_test.go | 240 ++++++++++ client/internal/routemanager/manager.go | 71 ++- client/internal/routemanager/mock.go | 2 +- .../routemanager/notifier/notifier.go | 124 ------ .../routemanager/notifier/notifier_android.go | 127 ++++++ .../routemanager/notifier/notifier_ios.go | 80 ++++ .../routemanager/notifier/notifier_other.go | 36 ++ client/internal/routemanager/static/route.go | 9 +- 29 files changed, 2036 insertions(+), 397 deletions(-) rename client/firewall/uspfilter/{uspfilter.go => filter.go} (96%) rename client/firewall/uspfilter/{uspfilter_bench_test.go => filter_bench_test.go} (94%) rename client/firewall/uspfilter/{uspfilter_filter_test.go => filter_filter_test.go} (99%) rename client/firewall/uspfilter/{uspfilter_test.go => filter_test.go} (98%) create mode 100644 client/firewall/uspfilter/nat.go create mode 100644 client/firewall/uspfilter/nat_bench_test.go create mode 100644 client/firewall/uspfilter/nat_test.go create mode 100644 client/internal/routemanager/common/params.go create mode 100644 client/internal/routemanager/fakeip/fakeip.go create mode 100644 client/internal/routemanager/fakeip/fakeip_test.go delete mode 100644 client/internal/routemanager/notifier/notifier.go create mode 100644 client/internal/routemanager/notifier/notifier_android.go create mode 100644 client/internal/routemanager/notifier/notifier_ios.go create mode 100644 client/internal/routemanager/notifier/notifier_other.go diff --git a/client/android/client.go b/client/android/client.go index 3b8a5bd0f..a17439696 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -203,8 +203,10 @@ func (c *Client) Networks() *NetworkArray { continue } - if routes[0].IsDynamic() { - continue + r := routes[0] + netStr := r.Network.String() + if r.IsDynamic() { + netStr = r.Domains.SafeString() } peer, err := c.recorder.GetPeer(routes[0].Peer) @@ -214,7 +216,7 @@ func (c *Client) Networks() *NetworkArray { } network := Network{ Name: string(id), - Network: routes[0].Network.String(), + Network: netStr, Peer: peer.FQDN, Status: peer.ConnStatus.String(), } diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/filter.go similarity index 96% rename from client/firewall/uspfilter/uspfilter.go rename to client/firewall/uspfilter/filter.go index dcff92c61..7120d7d64 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/filter.go @@ -104,6 +104,12 @@ type Manager struct { flowLogger nftypes.FlowLogger blockRule firewall.Rule + + // Internal 1:1 DNAT + dnatEnabled atomic.Bool + dnatMappings map[netip.Addr]netip.Addr + dnatMutex sync.RWMutex + dnatBiMap *biDNATMap } // decoder for packages @@ -189,6 +195,7 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe flowLogger: flowLogger, netstack: netstack.IsEnabled(), localForwarding: enableLocalForwarding, + dnatMappings: make(map[netip.Addr]netip.Addr), } m.routingEnabled.Store(false) @@ -519,22 +526,6 @@ func (m *Manager) SetLegacyManagement(isLegacy bool) error { // Flush doesn't need to be implemented for this manager func (m *Manager) Flush() error { return nil } -// AddDNATRule adds a DNAT rule -func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) { - if m.nativeFirewall == nil { - return nil, errNatNotSupported - } - return m.nativeFirewall.AddDNATRule(rule) -} - -// DeleteDNATRule deletes a DNAT rule -func (m *Manager) DeleteDNATRule(rule firewall.Rule) error { - if m.nativeFirewall == nil { - return errNatNotSupported - } - return m.nativeFirewall.DeleteDNATRule(rule) -} - // UpdateSet updates the rule destinations associated with the given set // by merging the existing prefixes with the new ones, then deduplicating. func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error { @@ -581,14 +572,14 @@ func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error { return nil } -// DropOutgoing filter outgoing packets -func (m *Manager) DropOutgoing(packetData []byte, size int) bool { - return m.processOutgoingHooks(packetData, size) +// FilterOutBound filters outgoing packets +func (m *Manager) FilterOutbound(packetData []byte, size int) bool { + return m.filterOutbound(packetData, size) } -// DropIncoming filter incoming packets -func (m *Manager) DropIncoming(packetData []byte, size int) bool { - return m.dropFilter(packetData, size) +// FilterInbound filters incoming packets +func (m *Manager) FilterInbound(packetData []byte, size int) bool { + return m.filterInbound(packetData, size) } // UpdateLocalIPs updates the list of local IPs @@ -596,7 +587,7 @@ func (m *Manager) UpdateLocalIPs() error { return m.localipmanager.UpdateLocalIPs(m.wgIface) } -func (m *Manager) processOutgoingHooks(packetData []byte, size int) bool { +func (m *Manager) filterOutbound(packetData []byte, size int) bool { d := m.decoders.Get().(*decoder) defer m.decoders.Put(d) @@ -618,8 +609,8 @@ func (m *Manager) processOutgoingHooks(packetData []byte, size int) bool { return true } - // for netflow we keep track even if the firewall is stateless m.trackOutbound(d, srcIP, dstIP, size) + m.translateOutboundDNAT(packetData, d) return false } @@ -723,9 +714,9 @@ func (m *Manager) udpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte return false } -// dropFilter implements filtering logic for incoming packets. +// filterInbound implements filtering logic for incoming packets. // If it returns true, the packet should be dropped. -func (m *Manager) dropFilter(packetData []byte, size int) bool { +func (m *Manager) filterInbound(packetData []byte, size int) bool { d := m.decoders.Get().(*decoder) defer m.decoders.Put(d) @@ -747,8 +738,15 @@ func (m *Manager) dropFilter(packetData []byte, size int) bool { return false } - // For all inbound traffic, first check if it matches a tracked connection. - // This must happen before any other filtering because the packets are statefully tracked. + if translated := m.translateInboundReverse(packetData, d); translated { + // Re-decode after translation to get original addresses + if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { + m.logger.Error("Failed to re-decode packet after reverse DNAT: %v", err) + return true + } + srcIP, dstIP = m.extractIPs(d) + } + if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP, size) { return false } diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/filter_bench_test.go similarity index 94% rename from client/firewall/uspfilter/uspfilter_bench_test.go rename to client/firewall/uspfilter/filter_bench_test.go index c03e60640..0cffcc1a7 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/filter_bench_test.go @@ -188,13 +188,13 @@ func BenchmarkCoreFiltering(b *testing.B) { // For stateful scenarios, establish the connection if sc.stateful { - manager.processOutgoingHooks(outbound, 0) + manager.filterOutbound(outbound, 0) } // Measure inbound packet processing b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, 0) + manager.filterInbound(inbound, 0) } }) } @@ -220,7 +220,7 @@ func BenchmarkStateScaling(b *testing.B) { for i := 0; i < count; i++ { outbound := generatePacket(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, layers.IPProtocolTCP) - manager.processOutgoingHooks(outbound, 0) + manager.filterOutbound(outbound, 0) } // Test packet @@ -228,11 +228,11 @@ func BenchmarkStateScaling(b *testing.B) { testIn := generatePacket(b, dstIPs[0], srcIPs[0], 80, 1024, layers.IPProtocolTCP) // First establish our test connection - manager.processOutgoingHooks(testOut, 0) + manager.filterOutbound(testOut, 0) b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(testIn, 0) + manager.filterInbound(testIn, 0) } }) } @@ -263,12 +263,12 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { inbound := generatePacket(b, dstIP, srcIP, 80, 1024, layers.IPProtocolTCP) if sc.established { - manager.processOutgoingHooks(outbound, 0) + manager.filterOutbound(outbound, 0) } b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, 0) + manager.filterInbound(inbound, 0) } }) } @@ -426,25 +426,25 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { // For stateful cases and established connections if !strings.Contains(sc.name, "allow_non_wg") || (strings.Contains(sc.state, "established") || sc.state == "post_handshake") { - manager.processOutgoingHooks(outbound, 0) + manager.filterOutbound(outbound, 0) // For TCP post-handshake, simulate full handshake if sc.state == "post_handshake" { // SYN syn := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPSyn)) - manager.processOutgoingHooks(syn, 0) + manager.filterOutbound(syn, 0) // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIP, srcIP, 80, 1024, uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, 0) + manager.filterInbound(synack, 0) // ACK ack := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPAck)) - manager.processOutgoingHooks(ack, 0) + manager.filterOutbound(ack, 0) } } b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, 0) + manager.filterInbound(inbound, 0) } }) } @@ -568,17 +568,17 @@ func BenchmarkLongLivedConnections(b *testing.B) { // Initial SYN syn := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPSyn)) - manager.processOutgoingHooks(syn, 0) + manager.filterOutbound(syn, 0) // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, 0) + manager.filterInbound(synack, 0) // ACK ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPAck)) - manager.processOutgoingHooks(ack, 0) + manager.filterOutbound(ack, 0) } // Prepare test packets simulating bidirectional traffic @@ -599,9 +599,9 @@ func BenchmarkLongLivedConnections(b *testing.B) { // Simulate bidirectional traffic // First outbound data - manager.processOutgoingHooks(outPackets[connIdx], 0) + manager.filterOutbound(outPackets[connIdx], 0) // Then inbound response - this is what we're actually measuring - manager.dropFilter(inPackets[connIdx], 0) + manager.filterInbound(inPackets[connIdx], 0) } }) } @@ -700,19 +700,19 @@ func BenchmarkShortLivedConnections(b *testing.B) { p := patterns[connIdx] // Connection establishment - manager.processOutgoingHooks(p.syn, 0) - manager.dropFilter(p.synAck, 0) - manager.processOutgoingHooks(p.ack, 0) + manager.filterOutbound(p.syn, 0) + manager.filterInbound(p.synAck, 0) + manager.filterOutbound(p.ack, 0) // Data transfer - manager.processOutgoingHooks(p.request, 0) - manager.dropFilter(p.response, 0) + manager.filterOutbound(p.request, 0) + manager.filterInbound(p.response, 0) // Connection teardown - manager.processOutgoingHooks(p.finClient, 0) - manager.dropFilter(p.ackServer, 0) - manager.dropFilter(p.finServer, 0) - manager.processOutgoingHooks(p.ackClient, 0) + manager.filterOutbound(p.finClient, 0) + manager.filterInbound(p.ackServer, 0) + manager.filterInbound(p.finServer, 0) + manager.filterOutbound(p.ackClient, 0) } }) } @@ -760,15 +760,15 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { for i := 0; i < sc.connCount; i++ { syn := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPSyn)) - manager.processOutgoingHooks(syn, 0) + manager.filterOutbound(syn, 0) synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, 0) + manager.filterInbound(synack, 0) ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPAck)) - manager.processOutgoingHooks(ack, 0) + manager.filterOutbound(ack, 0) } // Pre-generate test packets @@ -790,8 +790,8 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { counter++ // Simulate bidirectional traffic - manager.processOutgoingHooks(outPackets[connIdx], 0) - manager.dropFilter(inPackets[connIdx], 0) + manager.filterOutbound(outPackets[connIdx], 0) + manager.filterInbound(inPackets[connIdx], 0) } }) }) @@ -879,17 +879,17 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { p := patterns[connIdx] // Full connection lifecycle - manager.processOutgoingHooks(p.syn, 0) - manager.dropFilter(p.synAck, 0) - manager.processOutgoingHooks(p.ack, 0) + manager.filterOutbound(p.syn, 0) + manager.filterInbound(p.synAck, 0) + manager.filterOutbound(p.ack, 0) - manager.processOutgoingHooks(p.request, 0) - manager.dropFilter(p.response, 0) + manager.filterOutbound(p.request, 0) + manager.filterInbound(p.response, 0) - manager.processOutgoingHooks(p.finClient, 0) - manager.dropFilter(p.ackServer, 0) - manager.dropFilter(p.finServer, 0) - manager.processOutgoingHooks(p.ackClient, 0) + manager.filterOutbound(p.finClient, 0) + manager.filterInbound(p.ackServer, 0) + manager.filterInbound(p.finServer, 0) + manager.filterOutbound(p.ackClient, 0) } }) }) diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/filter_filter_test.go similarity index 99% rename from client/firewall/uspfilter/uspfilter_filter_test.go rename to client/firewall/uspfilter/filter_filter_test.go index 318f86a87..b630c9e66 100644 --- a/client/firewall/uspfilter/uspfilter_filter_test.go +++ b/client/firewall/uspfilter/filter_filter_test.go @@ -462,7 +462,7 @@ func TestPeerACLFiltering(t *testing.T) { t.Run("Implicit DROP (no rules)", func(t *testing.T) { packet := createTestPacket(t, "100.10.0.1", "100.10.0.100", fw.ProtocolTCP, 12345, 443) - isDropped := manager.DropIncoming(packet, 0) + isDropped := manager.FilterInbound(packet, 0) require.True(t, isDropped, "Packet should be dropped when no rules exist") }) @@ -509,7 +509,7 @@ func TestPeerACLFiltering(t *testing.T) { }) packet := createTestPacket(t, tc.srcIP, tc.dstIP, tc.proto, tc.srcPort, tc.dstPort) - isDropped := manager.DropIncoming(packet, 0) + isDropped := manager.FilterInbound(packet, 0) require.Equal(t, tc.shouldBeBlocked, isDropped) }) } @@ -1233,7 +1233,7 @@ func TestRouteACLFiltering(t *testing.T) { srcIP := netip.MustParseAddr(tc.srcIP) dstIP := netip.MustParseAddr(tc.dstIP) - // testing routeACLsPass only and not DropIncoming, as routed packets are dropped after being passed + // testing routeACLsPass only and not FilterInbound, as routed packets are dropped after being passed // to the forwarder _, isAllowed := manager.routeACLsPass(srcIP, dstIP, tc.proto, tc.srcPort, tc.dstPort) require.Equal(t, tc.shouldPass, isAllowed) diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/filter_test.go similarity index 98% rename from client/firewall/uspfilter/uspfilter_test.go rename to client/firewall/uspfilter/filter_test.go index 88de1ddcd..5b5cd5a53 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/filter_test.go @@ -321,7 +321,7 @@ func TestNotMatchByIP(t *testing.T) { return } - if m.dropFilter(buf.Bytes(), 0) { + if m.filterInbound(buf.Bytes(), 0) { t.Errorf("expected packet to be accepted") return } @@ -447,7 +447,7 @@ func TestProcessOutgoingHooks(t *testing.T) { require.NoError(t, err) // Test hook gets called - result := manager.processOutgoingHooks(buf.Bytes(), 0) + result := manager.filterOutbound(buf.Bytes(), 0) require.True(t, result) require.True(t, hookCalled) @@ -457,7 +457,7 @@ func TestProcessOutgoingHooks(t *testing.T) { err = gopacket.SerializeLayers(buf, opts, ipv4) require.NoError(t, err) - result = manager.processOutgoingHooks(buf.Bytes(), 0) + result = manager.filterOutbound(buf.Bytes(), 0) require.False(t, result) } @@ -553,7 +553,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { require.NoError(t, err) // Process outbound packet and verify connection tracking - drop := manager.DropOutgoing(outboundBuf.Bytes(), 0) + drop := manager.FilterOutbound(outboundBuf.Bytes(), 0) require.False(t, drop, "Initial outbound packet should not be dropped") // Verify connection was tracked @@ -620,7 +620,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { for _, cp := range checkPoints { time.Sleep(cp.sleep) - drop = manager.dropFilter(inboundBuf.Bytes(), 0) + drop = manager.filterInbound(inboundBuf.Bytes(), 0) require.Equal(t, cp.shouldAllow, !drop, cp.description) // If the connection should still be valid, verify it exists @@ -669,7 +669,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { } // Create a new outbound connection for invalid tests - drop = manager.processOutgoingHooks(outboundBuf.Bytes(), 0) + drop = manager.filterOutbound(outboundBuf.Bytes(), 0) require.False(t, drop, "Second outbound packet should not be dropped") for _, tc := range invalidCases { @@ -691,7 +691,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { require.NoError(t, err) // Verify the invalid packet is dropped - drop = manager.dropFilter(testBuf.Bytes(), 0) + drop = manager.filterInbound(testBuf.Bytes(), 0) require.True(t, drop, tc.description) }) } diff --git a/client/firewall/uspfilter/nat.go b/client/firewall/uspfilter/nat.go new file mode 100644 index 000000000..4539f7da5 --- /dev/null +++ b/client/firewall/uspfilter/nat.go @@ -0,0 +1,408 @@ +package uspfilter + +import ( + "encoding/binary" + "errors" + "fmt" + "net/netip" + + "github.com/google/gopacket/layers" + + firewall "github.com/netbirdio/netbird/client/firewall/manager" +) + +var ErrIPv4Only = errors.New("only IPv4 is supported for DNAT") + +func ipv4Checksum(header []byte) uint16 { + if len(header) < 20 { + return 0 + } + + var sum1, sum2 uint32 + + // Parallel processing - unroll and compute two sums simultaneously + sum1 += uint32(binary.BigEndian.Uint16(header[0:2])) + sum2 += uint32(binary.BigEndian.Uint16(header[2:4])) + sum1 += uint32(binary.BigEndian.Uint16(header[4:6])) + sum2 += uint32(binary.BigEndian.Uint16(header[6:8])) + sum1 += uint32(binary.BigEndian.Uint16(header[8:10])) + // Skip checksum field at [10:12] + sum2 += uint32(binary.BigEndian.Uint16(header[12:14])) + sum1 += uint32(binary.BigEndian.Uint16(header[14:16])) + sum2 += uint32(binary.BigEndian.Uint16(header[16:18])) + sum1 += uint32(binary.BigEndian.Uint16(header[18:20])) + + sum := sum1 + sum2 + + // Handle remaining bytes for headers > 20 bytes + for i := 20; i < len(header)-1; i += 2 { + sum += uint32(binary.BigEndian.Uint16(header[i : i+2])) + } + + if len(header)%2 == 1 { + sum += uint32(header[len(header)-1]) << 8 + } + + // Optimized carry fold - single iteration handles most cases + sum = (sum & 0xFFFF) + (sum >> 16) + if sum > 0xFFFF { + sum++ + } + + return ^uint16(sum) +} + +func icmpChecksum(data []byte) uint16 { + var sum1, sum2, sum3, sum4 uint32 + i := 0 + + // Process 16 bytes at once with 4 parallel accumulators + for i <= len(data)-16 { + sum1 += uint32(binary.BigEndian.Uint16(data[i : i+2])) + sum2 += uint32(binary.BigEndian.Uint16(data[i+2 : i+4])) + sum3 += uint32(binary.BigEndian.Uint16(data[i+4 : i+6])) + sum4 += uint32(binary.BigEndian.Uint16(data[i+6 : i+8])) + sum1 += uint32(binary.BigEndian.Uint16(data[i+8 : i+10])) + sum2 += uint32(binary.BigEndian.Uint16(data[i+10 : i+12])) + sum3 += uint32(binary.BigEndian.Uint16(data[i+12 : i+14])) + sum4 += uint32(binary.BigEndian.Uint16(data[i+14 : i+16])) + i += 16 + } + + sum := sum1 + sum2 + sum3 + sum4 + + // Handle remaining bytes + for i < len(data)-1 { + sum += uint32(binary.BigEndian.Uint16(data[i : i+2])) + i += 2 + } + + if len(data)%2 == 1 { + sum += uint32(data[len(data)-1]) << 8 + } + + sum = (sum & 0xFFFF) + (sum >> 16) + if sum > 0xFFFF { + sum++ + } + + return ^uint16(sum) +} + +type biDNATMap struct { + forward map[netip.Addr]netip.Addr + reverse map[netip.Addr]netip.Addr +} + +func newBiDNATMap() *biDNATMap { + return &biDNATMap{ + forward: make(map[netip.Addr]netip.Addr), + reverse: make(map[netip.Addr]netip.Addr), + } +} + +func (b *biDNATMap) set(original, translated netip.Addr) { + b.forward[original] = translated + b.reverse[translated] = original +} + +func (b *biDNATMap) delete(original netip.Addr) { + if translated, exists := b.forward[original]; exists { + delete(b.forward, original) + delete(b.reverse, translated) + } +} + +func (b *biDNATMap) getTranslated(original netip.Addr) (netip.Addr, bool) { + translated, exists := b.forward[original] + return translated, exists +} + +func (b *biDNATMap) getOriginal(translated netip.Addr) (netip.Addr, bool) { + original, exists := b.reverse[translated] + return original, exists +} + +func (m *Manager) AddInternalDNATMapping(originalAddr, translatedAddr netip.Addr) error { + if !originalAddr.IsValid() || !translatedAddr.IsValid() { + return fmt.Errorf("invalid IP addresses") + } + + if m.localipmanager.IsLocalIP(translatedAddr) { + return fmt.Errorf("cannot map to local IP: %s", translatedAddr) + } + + m.dnatMutex.Lock() + defer m.dnatMutex.Unlock() + + // Initialize both maps together if either is nil + if m.dnatMappings == nil || m.dnatBiMap == nil { + m.dnatMappings = make(map[netip.Addr]netip.Addr) + m.dnatBiMap = newBiDNATMap() + } + + m.dnatMappings[originalAddr] = translatedAddr + m.dnatBiMap.set(originalAddr, translatedAddr) + + if len(m.dnatMappings) == 1 { + m.dnatEnabled.Store(true) + } + + return nil +} + +// RemoveInternalDNATMapping removes a 1:1 IP address mapping +func (m *Manager) RemoveInternalDNATMapping(originalAddr netip.Addr) error { + m.dnatMutex.Lock() + defer m.dnatMutex.Unlock() + + if _, exists := m.dnatMappings[originalAddr]; !exists { + return fmt.Errorf("mapping not found for: %s", originalAddr) + } + + delete(m.dnatMappings, originalAddr) + m.dnatBiMap.delete(originalAddr) + if len(m.dnatMappings) == 0 { + m.dnatEnabled.Store(false) + } + + return nil +} + +// getDNATTranslation returns the translated address if a mapping exists +func (m *Manager) getDNATTranslation(addr netip.Addr) (netip.Addr, bool) { + if !m.dnatEnabled.Load() { + return addr, false + } + + m.dnatMutex.RLock() + translated, exists := m.dnatBiMap.getTranslated(addr) + m.dnatMutex.RUnlock() + return translated, exists +} + +// findReverseDNATMapping finds original address for return traffic +func (m *Manager) findReverseDNATMapping(translatedAddr netip.Addr) (netip.Addr, bool) { + if !m.dnatEnabled.Load() { + return translatedAddr, false + } + + m.dnatMutex.RLock() + original, exists := m.dnatBiMap.getOriginal(translatedAddr) + m.dnatMutex.RUnlock() + return original, exists +} + +// translateOutboundDNAT applies DNAT translation to outbound packets +func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool { + if !m.dnatEnabled.Load() { + return false + } + + if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 { + return false + } + + dstIP := netip.AddrFrom4([4]byte{packetData[16], packetData[17], packetData[18], packetData[19]}) + + translatedIP, exists := m.getDNATTranslation(dstIP) + if !exists { + return false + } + + if err := m.rewritePacketDestination(packetData, d, translatedIP); err != nil { + m.logger.Error("Failed to rewrite packet destination: %v", err) + return false + } + + m.logger.Trace("DNAT: %s -> %s", dstIP, translatedIP) + return true +} + +// translateInboundReverse applies reverse DNAT to inbound return traffic +func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool { + if !m.dnatEnabled.Load() { + return false + } + + if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 { + return false + } + + srcIP := netip.AddrFrom4([4]byte{packetData[12], packetData[13], packetData[14], packetData[15]}) + + originalIP, exists := m.findReverseDNATMapping(srcIP) + if !exists { + return false + } + + if err := m.rewritePacketSource(packetData, d, originalIP); err != nil { + m.logger.Error("Failed to rewrite packet source: %v", err) + return false + } + + m.logger.Trace("Reverse DNAT: %s -> %s", srcIP, originalIP) + return true +} + +// rewritePacketDestination replaces destination IP in the packet +func (m *Manager) rewritePacketDestination(packetData []byte, d *decoder, newIP netip.Addr) error { + if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 || !newIP.Is4() { + return ErrIPv4Only + } + + var oldDst [4]byte + copy(oldDst[:], packetData[16:20]) + newDst := newIP.As4() + + copy(packetData[16:20], newDst[:]) + + ipHeaderLen := int(d.ip4.IHL) * 4 + if ipHeaderLen < 20 || ipHeaderLen > len(packetData) { + return fmt.Errorf("invalid IP header length") + } + + binary.BigEndian.PutUint16(packetData[10:12], 0) + ipChecksum := ipv4Checksum(packetData[:ipHeaderLen]) + binary.BigEndian.PutUint16(packetData[10:12], ipChecksum) + + if len(d.decoded) > 1 { + switch d.decoded[1] { + case layers.LayerTypeTCP: + m.updateTCPChecksum(packetData, ipHeaderLen, oldDst[:], newDst[:]) + case layers.LayerTypeUDP: + m.updateUDPChecksum(packetData, ipHeaderLen, oldDst[:], newDst[:]) + case layers.LayerTypeICMPv4: + m.updateICMPChecksum(packetData, ipHeaderLen) + } + } + + return nil +} + +// rewritePacketSource replaces the source IP address in the packet +func (m *Manager) rewritePacketSource(packetData []byte, d *decoder, newIP netip.Addr) error { + if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 || !newIP.Is4() { + return ErrIPv4Only + } + + var oldSrc [4]byte + copy(oldSrc[:], packetData[12:16]) + newSrc := newIP.As4() + + copy(packetData[12:16], newSrc[:]) + + ipHeaderLen := int(d.ip4.IHL) * 4 + if ipHeaderLen < 20 || ipHeaderLen > len(packetData) { + return fmt.Errorf("invalid IP header length") + } + + binary.BigEndian.PutUint16(packetData[10:12], 0) + ipChecksum := ipv4Checksum(packetData[:ipHeaderLen]) + binary.BigEndian.PutUint16(packetData[10:12], ipChecksum) + + if len(d.decoded) > 1 { + switch d.decoded[1] { + case layers.LayerTypeTCP: + m.updateTCPChecksum(packetData, ipHeaderLen, oldSrc[:], newSrc[:]) + case layers.LayerTypeUDP: + m.updateUDPChecksum(packetData, ipHeaderLen, oldSrc[:], newSrc[:]) + case layers.LayerTypeICMPv4: + m.updateICMPChecksum(packetData, ipHeaderLen) + } + } + + return nil +} + +func (m *Manager) updateTCPChecksum(packetData []byte, ipHeaderLen int, oldIP, newIP []byte) { + tcpStart := ipHeaderLen + if len(packetData) < tcpStart+18 { + return + } + + checksumOffset := tcpStart + 16 + oldChecksum := binary.BigEndian.Uint16(packetData[checksumOffset : checksumOffset+2]) + newChecksum := incrementalUpdate(oldChecksum, oldIP, newIP) + binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum) +} + +func (m *Manager) updateUDPChecksum(packetData []byte, ipHeaderLen int, oldIP, newIP []byte) { + udpStart := ipHeaderLen + if len(packetData) < udpStart+8 { + return + } + + checksumOffset := udpStart + 6 + oldChecksum := binary.BigEndian.Uint16(packetData[checksumOffset : checksumOffset+2]) + + if oldChecksum == 0 { + return + } + + newChecksum := incrementalUpdate(oldChecksum, oldIP, newIP) + binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum) +} + +func (m *Manager) updateICMPChecksum(packetData []byte, ipHeaderLen int) { + icmpStart := ipHeaderLen + if len(packetData) < icmpStart+8 { + return + } + + icmpData := packetData[icmpStart:] + binary.BigEndian.PutUint16(icmpData[2:4], 0) + checksum := icmpChecksum(icmpData) + binary.BigEndian.PutUint16(icmpData[2:4], checksum) +} + +// incrementalUpdate performs incremental checksum update per RFC 1624 +func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 { + sum := uint32(^oldChecksum) + + // Fast path for IPv4 addresses (4 bytes) - most common case + if len(oldBytes) == 4 && len(newBytes) == 4 { + sum += uint32(^binary.BigEndian.Uint16(oldBytes[0:2])) + sum += uint32(^binary.BigEndian.Uint16(oldBytes[2:4])) + sum += uint32(binary.BigEndian.Uint16(newBytes[0:2])) + sum += uint32(binary.BigEndian.Uint16(newBytes[2:4])) + } else { + // Fallback for other lengths + for i := 0; i < len(oldBytes)-1; i += 2 { + sum += uint32(^binary.BigEndian.Uint16(oldBytes[i : i+2])) + } + if len(oldBytes)%2 == 1 { + sum += uint32(^oldBytes[len(oldBytes)-1]) << 8 + } + + for i := 0; i < len(newBytes)-1; i += 2 { + sum += uint32(binary.BigEndian.Uint16(newBytes[i : i+2])) + } + if len(newBytes)%2 == 1 { + sum += uint32(newBytes[len(newBytes)-1]) << 8 + } + } + + sum = (sum & 0xFFFF) + (sum >> 16) + if sum > 0xFFFF { + sum++ + } + + return ^uint16(sum) +} + +// AddDNATRule adds a DNAT rule (delegates to native firewall for port forwarding) +func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) { + if m.nativeFirewall == nil { + return nil, errNatNotSupported + } + return m.nativeFirewall.AddDNATRule(rule) +} + +// DeleteDNATRule deletes a DNAT rule (delegates to native firewall) +func (m *Manager) DeleteDNATRule(rule firewall.Rule) error { + if m.nativeFirewall == nil { + return errNatNotSupported + } + return m.nativeFirewall.DeleteDNATRule(rule) +} diff --git a/client/firewall/uspfilter/nat_bench_test.go b/client/firewall/uspfilter/nat_bench_test.go new file mode 100644 index 000000000..16dba682e --- /dev/null +++ b/client/firewall/uspfilter/nat_bench_test.go @@ -0,0 +1,416 @@ +package uspfilter + +import ( + "fmt" + "net/netip" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/iface/device" +) + +// BenchmarkDNATTranslation measures the performance of DNAT operations +func BenchmarkDNATTranslation(b *testing.B) { + scenarios := []struct { + name string + proto layers.IPProtocol + setupDNAT bool + description string + }{ + { + name: "tcp_with_dnat", + proto: layers.IPProtocolTCP, + setupDNAT: true, + description: "TCP packet with DNAT translation enabled", + }, + { + name: "tcp_without_dnat", + proto: layers.IPProtocolTCP, + setupDNAT: false, + description: "TCP packet without DNAT (baseline)", + }, + { + name: "udp_with_dnat", + proto: layers.IPProtocolUDP, + setupDNAT: true, + description: "UDP packet with DNAT translation enabled", + }, + { + name: "udp_without_dnat", + proto: layers.IPProtocolUDP, + setupDNAT: false, + description: "UDP packet without DNAT (baseline)", + }, + { + name: "icmp_with_dnat", + proto: layers.IPProtocolICMPv4, + setupDNAT: true, + description: "ICMP packet with DNAT translation enabled", + }, + { + name: "icmp_without_dnat", + proto: layers.IPProtocolICMPv4, + setupDNAT: false, + description: "ICMP packet without DNAT (baseline)", + }, + } + + for _, sc := range scenarios { + b.Run(sc.name, func(b *testing.B) { + manager, err := Create(&IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + }, false, flowLogger) + require.NoError(b, err) + defer func() { + require.NoError(b, manager.Close(nil)) + }() + + // Set logger to error level to reduce noise during benchmarking + manager.SetLogLevel(log.ErrorLevel) + defer func() { + // Restore to info level after benchmark + manager.SetLogLevel(log.InfoLevel) + }() + + // Setup DNAT mapping if needed + originalIP := netip.MustParseAddr("192.168.1.100") + translatedIP := netip.MustParseAddr("10.0.0.100") + + if sc.setupDNAT { + err := manager.AddInternalDNATMapping(originalIP, translatedIP) + require.NoError(b, err) + } + + // Create test packets + srcIP := netip.MustParseAddr("172.16.0.1") + outboundPacket := generateDNATTestPacket(b, srcIP, originalIP, sc.proto, 12345, 80) + + // Pre-establish connection for reverse DNAT test + if sc.setupDNAT { + manager.filterOutbound(outboundPacket, 0) + } + + b.ResetTimer() + + // Benchmark outbound DNAT translation + b.Run("outbound", func(b *testing.B) { + for i := 0; i < b.N; i++ { + // Create fresh packet each time since translation modifies it + packet := generateDNATTestPacket(b, srcIP, originalIP, sc.proto, 12345, 80) + manager.filterOutbound(packet, 0) + } + }) + + // Benchmark inbound reverse DNAT translation + if sc.setupDNAT { + b.Run("inbound_reverse", func(b *testing.B) { + for i := 0; i < b.N; i++ { + // Create fresh packet each time since translation modifies it + packet := generateDNATTestPacket(b, translatedIP, srcIP, sc.proto, 80, 12345) + manager.filterInbound(packet, 0) + } + }) + } + }) + } +} + +// BenchmarkDNATConcurrency tests DNAT performance under concurrent load +func BenchmarkDNATConcurrency(b *testing.B) { + manager, err := Create(&IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + }, false, flowLogger) + require.NoError(b, err) + defer func() { + require.NoError(b, manager.Close(nil)) + }() + + // Set logger to error level to reduce noise during benchmarking + manager.SetLogLevel(log.ErrorLevel) + defer func() { + // Restore to info level after benchmark + manager.SetLogLevel(log.InfoLevel) + }() + + // Setup multiple DNAT mappings + numMappings := 100 + originalIPs := make([]netip.Addr, numMappings) + translatedIPs := make([]netip.Addr, numMappings) + + for i := 0; i < numMappings; i++ { + originalIPs[i] = netip.MustParseAddr(fmt.Sprintf("192.168.%d.%d", (i/254)+1, (i%254)+1)) + translatedIPs[i] = netip.MustParseAddr(fmt.Sprintf("10.0.%d.%d", (i/254)+1, (i%254)+1)) + err := manager.AddInternalDNATMapping(originalIPs[i], translatedIPs[i]) + require.NoError(b, err) + } + + srcIP := netip.MustParseAddr("172.16.0.1") + + // Pre-generate packets + outboundPackets := make([][]byte, numMappings) + inboundPackets := make([][]byte, numMappings) + for i := 0; i < numMappings; i++ { + outboundPackets[i] = generateDNATTestPacket(b, srcIP, originalIPs[i], layers.IPProtocolTCP, 12345, 80) + inboundPackets[i] = generateDNATTestPacket(b, translatedIPs[i], srcIP, layers.IPProtocolTCP, 80, 12345) + // Establish connections + manager.filterOutbound(outboundPackets[i], 0) + } + + b.ResetTimer() + + b.Run("concurrent_outbound", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + idx := i % numMappings + packet := generateDNATTestPacket(b, srcIP, originalIPs[idx], layers.IPProtocolTCP, 12345, 80) + manager.filterOutbound(packet, 0) + i++ + } + }) + }) + + b.Run("concurrent_inbound", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + idx := i % numMappings + packet := generateDNATTestPacket(b, translatedIPs[idx], srcIP, layers.IPProtocolTCP, 80, 12345) + manager.filterInbound(packet, 0) + i++ + } + }) + }) +} + +// BenchmarkDNATScaling tests how DNAT performance scales with number of mappings +func BenchmarkDNATScaling(b *testing.B) { + mappingCounts := []int{1, 10, 100, 1000} + + for _, count := range mappingCounts { + b.Run(fmt.Sprintf("mappings_%d", count), func(b *testing.B) { + manager, err := Create(&IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + }, false, flowLogger) + require.NoError(b, err) + defer func() { + require.NoError(b, manager.Close(nil)) + }() + + // Set logger to error level to reduce noise during benchmarking + manager.SetLogLevel(log.ErrorLevel) + defer func() { + // Restore to info level after benchmark + manager.SetLogLevel(log.InfoLevel) + }() + + // Setup DNAT mappings + for i := 0; i < count; i++ { + originalIP := netip.MustParseAddr(fmt.Sprintf("192.168.%d.%d", (i/254)+1, (i%254)+1)) + translatedIP := netip.MustParseAddr(fmt.Sprintf("10.0.%d.%d", (i/254)+1, (i%254)+1)) + err := manager.AddInternalDNATMapping(originalIP, translatedIP) + require.NoError(b, err) + } + + // Test with the last mapping added (worst case for lookup) + srcIP := netip.MustParseAddr("172.16.0.1") + lastOriginal := netip.MustParseAddr(fmt.Sprintf("192.168.%d.%d", ((count-1)/254)+1, ((count-1)%254)+1)) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + packet := generateDNATTestPacket(b, srcIP, lastOriginal, layers.IPProtocolTCP, 12345, 80) + manager.filterOutbound(packet, 0) + } + }) + } +} + +// generateDNATTestPacket creates a test packet for DNAT benchmarking +func generateDNATTestPacket(tb testing.TB, srcIP, dstIP netip.Addr, proto layers.IPProtocol, srcPort, dstPort uint16) []byte { + tb.Helper() + + ipv4 := &layers.IPv4{ + TTL: 64, + Version: 4, + SrcIP: srcIP.AsSlice(), + DstIP: dstIP.AsSlice(), + Protocol: proto, + } + + var transportLayer gopacket.SerializableLayer + switch proto { + case layers.IPProtocolTCP: + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(srcPort), + DstPort: layers.TCPPort(dstPort), + SYN: true, + } + require.NoError(tb, tcp.SetNetworkLayerForChecksum(ipv4)) + transportLayer = tcp + case layers.IPProtocolUDP: + udp := &layers.UDP{ + SrcPort: layers.UDPPort(srcPort), + DstPort: layers.UDPPort(dstPort), + } + require.NoError(tb, udp.SetNetworkLayerForChecksum(ipv4)) + transportLayer = udp + case layers.IPProtocolICMPv4: + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0), + } + transportLayer = icmp + } + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true} + err := gopacket.SerializeLayers(buf, opts, ipv4, transportLayer, gopacket.Payload("test")) + require.NoError(tb, err) + return buf.Bytes() +} + +// BenchmarkChecksumUpdate specifically benchmarks checksum calculation performance +func BenchmarkChecksumUpdate(b *testing.B) { + // Create test data for checksum calculations + testData := make([]byte, 64) // Typical packet size for checksum testing + for i := range testData { + testData[i] = byte(i) + } + + b.Run("ipv4_checksum", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = ipv4Checksum(testData[:20]) // IPv4 header is typically 20 bytes + } + }) + + b.Run("icmp_checksum", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = icmpChecksum(testData) + } + }) + + b.Run("incremental_update", func(b *testing.B) { + oldBytes := []byte{192, 168, 1, 100} + newBytes := []byte{10, 0, 0, 100} + oldChecksum := uint16(0x1234) + + for i := 0; i < b.N; i++ { + _ = incrementalUpdate(oldChecksum, oldBytes, newBytes) + } + }) +} + +// BenchmarkDNATMemoryAllocations checks for memory allocations in DNAT operations +func BenchmarkDNATMemoryAllocations(b *testing.B) { + manager, err := Create(&IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + }, false, flowLogger) + require.NoError(b, err) + defer func() { + require.NoError(b, manager.Close(nil)) + }() + + // Set logger to error level to reduce noise during benchmarking + manager.SetLogLevel(log.ErrorLevel) + defer func() { + // Restore to info level after benchmark + manager.SetLogLevel(log.InfoLevel) + }() + + originalIP := netip.MustParseAddr("192.168.1.100") + translatedIP := netip.MustParseAddr("10.0.0.100") + srcIP := netip.MustParseAddr("172.16.0.1") + + err = manager.AddInternalDNATMapping(originalIP, translatedIP) + require.NoError(b, err) + + packet := generateDNATTestPacket(b, srcIP, originalIP, layers.IPProtocolTCP, 12345, 80) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + // Create fresh packet each time to isolate allocation testing + testPacket := make([]byte, len(packet)) + copy(testPacket, packet) + + // Parse the packet fresh each time to get a clean decoder + d := &decoder{decoded: []gopacket.LayerType{}} + d.parser = gopacket.NewDecodingLayerParser( + layers.LayerTypeIPv4, + &d.eth, &d.ip4, &d.ip6, &d.icmp4, &d.icmp6, &d.tcp, &d.udp, + ) + d.parser.IgnoreUnsupported = true + err = d.parser.DecodeLayers(testPacket, &d.decoded) + assert.NoError(b, err) + + manager.translateOutboundDNAT(testPacket, d) + } +} + +// BenchmarkDirectIPExtraction tests the performance improvement of direct IP extraction +func BenchmarkDirectIPExtraction(b *testing.B) { + // Create a test packet + srcIP := netip.MustParseAddr("172.16.0.1") + dstIP := netip.MustParseAddr("192.168.1.100") + packet := generateDNATTestPacket(b, srcIP, dstIP, layers.IPProtocolTCP, 12345, 80) + + b.Run("direct_byte_access", func(b *testing.B) { + for i := 0; i < b.N; i++ { + // Direct extraction from packet bytes + _ = netip.AddrFrom4([4]byte{packet[16], packet[17], packet[18], packet[19]}) + } + }) + + b.Run("decoder_extraction", func(b *testing.B) { + // Create decoder once for comparison + d := &decoder{decoded: []gopacket.LayerType{}} + d.parser = gopacket.NewDecodingLayerParser( + layers.LayerTypeIPv4, + &d.eth, &d.ip4, &d.ip6, &d.icmp4, &d.icmp6, &d.tcp, &d.udp, + ) + d.parser.IgnoreUnsupported = true + err := d.parser.DecodeLayers(packet, &d.decoded) + assert.NoError(b, err) + + for i := 0; i < b.N; i++ { + // Extract using decoder (traditional method) + dst, _ := netip.AddrFromSlice(d.ip4.DstIP) + _ = dst + } + }) +} + +// BenchmarkChecksumOptimizations compares optimized vs standard checksum implementations +func BenchmarkChecksumOptimizations(b *testing.B) { + // Create test IPv4 header (20 bytes) + header := make([]byte, 20) + for i := range header { + header[i] = byte(i) + } + // Clear checksum field + header[10] = 0 + header[11] = 0 + + b.Run("optimized_ipv4_checksum", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = ipv4Checksum(header) + } + }) + + // Test incremental checksum updates + oldIP := []byte{192, 168, 1, 100} + newIP := []byte{10, 0, 0, 100} + oldChecksum := uint16(0x1234) + + b.Run("optimized_incremental_update", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = incrementalUpdate(oldChecksum, oldIP, newIP) + } + }) +} diff --git a/client/firewall/uspfilter/nat_test.go b/client/firewall/uspfilter/nat_test.go new file mode 100644 index 000000000..710abd445 --- /dev/null +++ b/client/firewall/uspfilter/nat_test.go @@ -0,0 +1,145 @@ +package uspfilter + +import ( + "net/netip" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/iface/device" +) + +// TestDNATTranslationCorrectness verifies DNAT translation works correctly +func TestDNATTranslationCorrectness(t *testing.T) { + manager, err := Create(&IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + }, false, flowLogger) + require.NoError(t, err) + defer func() { + require.NoError(t, manager.Close(nil)) + }() + + originalIP := netip.MustParseAddr("192.168.1.100") + translatedIP := netip.MustParseAddr("10.0.0.100") + srcIP := netip.MustParseAddr("172.16.0.1") + + // Add DNAT mapping + err = manager.AddInternalDNATMapping(originalIP, translatedIP) + require.NoError(t, err) + + testCases := []struct { + name string + protocol layers.IPProtocol + srcPort uint16 + dstPort uint16 + }{ + {"TCP", layers.IPProtocolTCP, 12345, 80}, + {"UDP", layers.IPProtocolUDP, 12345, 53}, + {"ICMP", layers.IPProtocolICMPv4, 0, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Test outbound DNAT translation + outboundPacket := generateDNATTestPacket(t, srcIP, originalIP, tc.protocol, tc.srcPort, tc.dstPort) + originalOutbound := make([]byte, len(outboundPacket)) + copy(originalOutbound, outboundPacket) + + // Process outbound packet (should translate destination) + translated := manager.translateOutboundDNAT(outboundPacket, parsePacket(t, outboundPacket)) + require.True(t, translated, "Outbound packet should be translated") + + // Verify destination IP was changed + dstIPAfter := netip.AddrFrom4([4]byte{outboundPacket[16], outboundPacket[17], outboundPacket[18], outboundPacket[19]}) + require.Equal(t, translatedIP, dstIPAfter, "Destination IP should be translated") + + // Test inbound reverse DNAT translation + inboundPacket := generateDNATTestPacket(t, translatedIP, srcIP, tc.protocol, tc.dstPort, tc.srcPort) + originalInbound := make([]byte, len(inboundPacket)) + copy(originalInbound, inboundPacket) + + // Process inbound packet (should reverse translate source) + reversed := manager.translateInboundReverse(inboundPacket, parsePacket(t, inboundPacket)) + require.True(t, reversed, "Inbound packet should be reverse translated") + + // Verify source IP was changed back to original + srcIPAfter := netip.AddrFrom4([4]byte{inboundPacket[12], inboundPacket[13], inboundPacket[14], inboundPacket[15]}) + require.Equal(t, originalIP, srcIPAfter, "Source IP should be reverse translated") + + // Test that checksums are recalculated correctly + if tc.protocol != layers.IPProtocolICMPv4 { + // For TCP/UDP, verify the transport checksum was updated + require.NotEqual(t, originalOutbound, outboundPacket, "Outbound packet should be modified") + require.NotEqual(t, originalInbound, inboundPacket, "Inbound packet should be modified") + } + }) + } +} + +// parsePacket helper to create a decoder for testing +func parsePacket(t testing.TB, packetData []byte) *decoder { + t.Helper() + d := &decoder{ + decoded: []gopacket.LayerType{}, + } + d.parser = gopacket.NewDecodingLayerParser( + layers.LayerTypeIPv4, + &d.eth, &d.ip4, &d.ip6, &d.icmp4, &d.icmp6, &d.tcp, &d.udp, + ) + d.parser.IgnoreUnsupported = true + + err := d.parser.DecodeLayers(packetData, &d.decoded) + require.NoError(t, err) + return d +} + +// TestDNATMappingManagement tests adding/removing DNAT mappings +func TestDNATMappingManagement(t *testing.T) { + manager, err := Create(&IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + }, false, flowLogger) + require.NoError(t, err) + defer func() { + require.NoError(t, manager.Close(nil)) + }() + + originalIP := netip.MustParseAddr("192.168.1.100") + translatedIP := netip.MustParseAddr("10.0.0.100") + + // Test adding mapping + err = manager.AddInternalDNATMapping(originalIP, translatedIP) + require.NoError(t, err) + + // Verify mapping exists + result, exists := manager.getDNATTranslation(originalIP) + require.True(t, exists) + require.Equal(t, translatedIP, result) + + // Test reverse lookup + reverseResult, exists := manager.findReverseDNATMapping(translatedIP) + require.True(t, exists) + require.Equal(t, originalIP, reverseResult) + + // Test removing mapping + err = manager.RemoveInternalDNATMapping(originalIP) + require.NoError(t, err) + + // Verify mapping no longer exists + _, exists = manager.getDNATTranslation(originalIP) + require.False(t, exists) + + _, exists = manager.findReverseDNATMapping(translatedIP) + require.False(t, exists) + + // Test error cases + err = manager.AddInternalDNATMapping(netip.Addr{}, translatedIP) + require.Error(t, err, "Should reject invalid original IP") + + err = manager.AddInternalDNATMapping(originalIP, netip.Addr{}) + require.Error(t, err, "Should reject invalid translated IP") + + err = manager.RemoveInternalDNATMapping(originalIP) + require.Error(t, err, "Should error when removing non-existent mapping") +} diff --git a/client/firewall/uspfilter/tracer.go b/client/firewall/uspfilter/tracer.go index 53350797c..ef04f2700 100644 --- a/client/firewall/uspfilter/tracer.go +++ b/client/firewall/uspfilter/tracer.go @@ -401,7 +401,7 @@ func (m *Manager) addForwardingResult(trace *PacketTrace, action, remoteAddr str func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace { // will create or update the connection state - dropped := m.processOutgoingHooks(packetData, 0) + dropped := m.filterOutbound(packetData, 0) if dropped { trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false) } else { diff --git a/client/iface/device/device_filter.go b/client/iface/device/device_filter.go index 5a1a0e96a..015f71ff4 100644 --- a/client/iface/device/device_filter.go +++ b/client/iface/device/device_filter.go @@ -9,11 +9,11 @@ import ( // PacketFilter interface for firewall abilities type PacketFilter interface { - // DropOutgoing filter outgoing packets from host to external destinations - DropOutgoing(packetData []byte, size int) bool + // FilterOutbound filter outgoing packets from host to external destinations + FilterOutbound(packetData []byte, size int) bool - // DropIncoming filter incoming packets from external sources to host - DropIncoming(packetData []byte, size int) bool + // FilterInbound filter incoming packets from external sources to host + FilterInbound(packetData []byte, size int) bool // AddUDPPacketHook calls hook when UDP packet from given direction matched // @@ -54,7 +54,7 @@ func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, er } for i := 0; i < n; i++ { - if filter.DropOutgoing(bufs[i][offset:offset+sizes[i]], sizes[i]) { + if filter.FilterOutbound(bufs[i][offset:offset+sizes[i]], sizes[i]) { bufs = append(bufs[:i], bufs[i+1:]...) sizes = append(sizes[:i], sizes[i+1:]...) n-- @@ -78,7 +78,7 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) { filteredBufs := make([][]byte, 0, len(bufs)) dropped := 0 for _, buf := range bufs { - if !filter.DropIncoming(buf[offset:], len(buf)) { + if !filter.FilterInbound(buf[offset:], len(buf)) { filteredBufs = append(filteredBufs, buf) dropped++ } diff --git a/client/iface/device/device_filter_test.go b/client/iface/device/device_filter_test.go index c90269e82..eef783542 100644 --- a/client/iface/device/device_filter_test.go +++ b/client/iface/device/device_filter_test.go @@ -146,7 +146,7 @@ func TestDeviceWrapperRead(t *testing.T) { tun.EXPECT().Write(mockBufs, 0).Return(0, nil) filter := mocks.NewMockPacketFilter(ctrl) - filter.EXPECT().DropIncoming(gomock.Any(), gomock.Any()).Return(true) + filter.EXPECT().FilterInbound(gomock.Any(), gomock.Any()).Return(true) wrapped := newDeviceFilter(tun) wrapped.filter = filter @@ -201,7 +201,7 @@ func TestDeviceWrapperRead(t *testing.T) { return 1, nil }) filter := mocks.NewMockPacketFilter(ctrl) - filter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).Return(true) + filter.EXPECT().FilterOutbound(gomock.Any(), gomock.Any()).Return(true) wrapped := newDeviceFilter(tun) wrapped.filter = filter diff --git a/client/iface/mocks/filter.go b/client/iface/mocks/filter.go index 8cd2a1231..566068aa5 100644 --- a/client/iface/mocks/filter.go +++ b/client/iface/mocks/filter.go @@ -48,32 +48,32 @@ func (mr *MockPacketFilterMockRecorder) AddUDPPacketHook(arg0, arg1, arg2, arg3 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUDPPacketHook", reflect.TypeOf((*MockPacketFilter)(nil).AddUDPPacketHook), arg0, arg1, arg2, arg3) } -// DropIncoming mocks base method. -func (m *MockPacketFilter) DropIncoming(arg0 []byte, arg1 int) bool { +// FilterInbound mocks base method. +func (m *MockPacketFilter) FilterInbound(arg0 []byte, arg1 int) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropIncoming", arg0, arg1) + ret := m.ctrl.Call(m, "FilterInbound", arg0, arg1) ret0, _ := ret[0].(bool) return ret0 } -// DropIncoming indicates an expected call of DropIncoming. -func (mr *MockPacketFilterMockRecorder) DropIncoming(arg0 interface{}, arg1 any) *gomock.Call { +// FilterInbound indicates an expected call of FilterInbound. +func (mr *MockPacketFilterMockRecorder) FilterInbound(arg0 interface{}, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropIncoming", reflect.TypeOf((*MockPacketFilter)(nil).DropIncoming), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterInbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterInbound), arg0, arg1) } -// DropOutgoing mocks base method. -func (m *MockPacketFilter) DropOutgoing(arg0 []byte, arg1 int) bool { +// FilterOutbound mocks base method. +func (m *MockPacketFilter) FilterOutbound(arg0 []byte, arg1 int) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropOutgoing", arg0, arg1) + ret := m.ctrl.Call(m, "FilterOutbound", arg0, arg1) ret0, _ := ret[0].(bool) return ret0 } -// DropOutgoing indicates an expected call of DropOutgoing. -func (mr *MockPacketFilterMockRecorder) DropOutgoing(arg0 interface{}, arg1 any) *gomock.Call { +// FilterOutbound indicates an expected call of FilterOutbound. +func (mr *MockPacketFilterMockRecorder) FilterOutbound(arg0 interface{}, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropOutgoing", reflect.TypeOf((*MockPacketFilter)(nil).DropOutgoing), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterOutbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterOutbound), arg0, arg1) } // RemovePacketHook mocks base method. diff --git a/client/iface/mocks/iface/mocks/filter.go b/client/iface/mocks/iface/mocks/filter.go index 17e123abb..291ab9ab5 100644 --- a/client/iface/mocks/iface/mocks/filter.go +++ b/client/iface/mocks/iface/mocks/filter.go @@ -46,32 +46,32 @@ func (mr *MockPacketFilterMockRecorder) AddUDPPacketHook(arg0, arg1, arg2, arg3 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUDPPacketHook", reflect.TypeOf((*MockPacketFilter)(nil).AddUDPPacketHook), arg0, arg1, arg2, arg3) } -// DropIncoming mocks base method. -func (m *MockPacketFilter) DropIncoming(arg0 []byte) bool { +// FilterInbound mocks base method. +func (m *MockPacketFilter) FilterInbound(arg0 []byte) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropIncoming", arg0) + ret := m.ctrl.Call(m, "FilterInbound", arg0) ret0, _ := ret[0].(bool) return ret0 } -// DropIncoming indicates an expected call of DropIncoming. -func (mr *MockPacketFilterMockRecorder) DropIncoming(arg0 interface{}) *gomock.Call { +// FilterInbound indicates an expected call of FilterInbound. +func (mr *MockPacketFilterMockRecorder) FilterInbound(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropIncoming", reflect.TypeOf((*MockPacketFilter)(nil).DropIncoming), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterInbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterInbound), arg0) } -// DropOutgoing mocks base method. -func (m *MockPacketFilter) DropOutgoing(arg0 []byte) bool { +// FilterOutbound mocks base method. +func (m *MockPacketFilter) FilterOutbound(arg0 []byte) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropOutgoing", arg0) + ret := m.ctrl.Call(m, "FilterOutbound", arg0) ret0, _ := ret[0].(bool) return ret0 } -// DropOutgoing indicates an expected call of DropOutgoing. -func (mr *MockPacketFilterMockRecorder) DropOutgoing(arg0 interface{}) *gomock.Call { +// FilterOutbound indicates an expected call of FilterOutbound. +func (mr *MockPacketFilterMockRecorder) FilterOutbound(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropOutgoing", reflect.TypeOf((*MockPacketFilter)(nil).DropOutgoing), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterOutbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterOutbound), arg0) } // SetNetwork mocks base method. diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 1cf59fb5b..21a9e2f2d 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -464,7 +464,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { defer ctrl.Finish() packetfilter := pfmock.NewMockPacketFilter(ctrl) - packetfilter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).AnyTimes() + packetfilter.EXPECT().FilterOutbound(gomock.Any(), gomock.Any()).AnyTimes() packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) packetfilter.EXPECT().RemovePacketHook(gomock.Any()) diff --git a/client/internal/engine.go b/client/internal/engine.go index 4ea6fbd94..74d84569a 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -383,7 +383,13 @@ func (e *Engine) Start() error { } e.stateManager.Start() - initialRoutes, dnsServer, err := e.newDnsServer() + initialRoutes, dnsConfig, dnsFeatureFlag, err := e.readInitialSettings() + if err != nil { + e.close() + return fmt.Errorf("read initial settings: %w", err) + } + + dnsServer, err := e.newDnsServer(dnsConfig) if err != nil { e.close() return fmt.Errorf("create dns server: %w", err) @@ -400,6 +406,7 @@ func (e *Engine) Start() error { InitialRoutes: initialRoutes, StateManager: e.stateManager, DNSServer: dnsServer, + DNSFeatureFlag: dnsFeatureFlag, PeerStore: e.peerStore, DisableClientRoutes: e.config.DisableClientRoutes, DisableServerRoutes: e.config.DisableServerRoutes, @@ -488,9 +495,9 @@ func (e *Engine) createFirewall() error { } func (e *Engine) initFirewall() error { - if err := e.routeManager.EnableServerRouter(e.firewall); err != nil { + if err := e.routeManager.SetFirewall(e.firewall); err != nil { e.close() - return fmt.Errorf("enable server router: %w", err) + return fmt.Errorf("set firewall: %w", err) } if e.config.BlockLANAccess { @@ -1009,8 +1016,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { log.Errorf("failed to update dns server, err: %v", err) } - dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap) - // apply routes first, route related actions might depend on routing being enabled routes := toRoutes(networkMap.GetRoutes()) serverRoutes, clientRoutes := e.routeManager.ClassifyRoutes(routes) @@ -1021,6 +1026,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { log.Debugf("updated lazy connection manager with %d HA groups", len(clientRoutes)) } + dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap) if err := e.routeManager.UpdateRoutes(serial, serverRoutes, clientRoutes, dnsRouteFeatureFlag); err != nil { log.Errorf("failed to update routes: %v", err) } @@ -1489,7 +1495,12 @@ func (e *Engine) close() { } } -func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { +func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, error) { + if runtime.GOOS != "android" { + // nolint:nilnil + return nil, nil, false, nil + } + info := system.GetInfo(e.ctx) info.SetFlags( e.config.RosenpassEnabled, @@ -1506,11 +1517,12 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { netMap, err := e.mgmClient.GetNetworkMap(info) if err != nil { - return nil, nil, err + return nil, nil, false, err } routes := toRoutes(netMap.GetRoutes()) dnsCfg := toDNSConfig(netMap.GetDNSConfig(), e.wgInterface.Address().Network) - return routes, &dnsCfg, nil + dnsFeatureFlag := toDNSFeatureFlag(netMap) + return routes, &dnsCfg, dnsFeatureFlag, nil } func (e *Engine) newWgIface() (*iface.WGIface, error) { @@ -1558,18 +1570,14 @@ func (e *Engine) wgInterfaceCreate() (err error) { return err } -func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) { +func (e *Engine) newDnsServer(dnsConfig *nbdns.Config) (dns.Server, error) { // due to tests where we are using a mocked version of the DNS server if e.dnsServer != nil { - return nil, e.dnsServer, nil + return e.dnsServer, nil } switch runtime.GOOS { case "android": - routes, dnsConfig, err := e.readInitialSettings() - if err != nil { - return nil, nil, err - } dnsServer := dns.NewDefaultServerPermanentUpstream( e.ctx, e.wgInterface, @@ -1580,19 +1588,19 @@ func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) { e.config.DisableDNS, ) go e.mobileDep.DnsReadyListener.OnReady() - return routes, dnsServer, nil + return dnsServer, nil case "ios": dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.statusRecorder, e.config.DisableDNS) - return nil, dnsServer, nil + return dnsServer, nil default: dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, e.statusRecorder, e.stateManager, e.config.DisableDNS) if err != nil { - return nil, nil, err + return nil, err } - return nil, dnsServer, nil + return dnsServer, nil } } diff --git a/client/internal/routemanager/client/client.go b/client/internal/routemanager/client/client.go index 46bff96db..0b8e161d2 100644 --- a/client/internal/routemanager/client/client.go +++ b/client/internal/routemanager/client/client.go @@ -10,11 +10,10 @@ import ( nbdns "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/client/internal/routemanager/common" "github.com/netbirdio/netbird/client/internal/routemanager/dnsinterceptor" "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" "github.com/netbirdio/netbird/client/internal/routemanager/iface" - "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/static" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/route" @@ -553,41 +552,16 @@ func (w *Watcher) Stop() { w.currentChosenStatus = nil } -func HandlerFromRoute( - rt *route.Route, - routeRefCounter *refcounter.RouteRefCounter, - allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, - dnsRouterInteval time.Duration, - statusRecorder *peer.Status, - wgInterface iface.WGIface, - dnsServer nbdns.Server, - peerStore *peerstore.Store, - useNewDNSRoute bool, -) RouteHandler { - switch handlerType(rt, useNewDNSRoute) { +func HandlerFromRoute(params common.HandlerParams) RouteHandler { + switch handlerType(params.Route, params.UseNewDNSRoute) { case handlerTypeDnsInterceptor: - return dnsinterceptor.New( - rt, - routeRefCounter, - allowedIPsRefCounter, - statusRecorder, - dnsServer, - wgInterface, - peerStore, - ) + return dnsinterceptor.New(params) case handlerTypeDynamic: - dns := nbdns.NewServiceViaMemory(wgInterface) - return dynamic.NewRoute( - rt, - routeRefCounter, - allowedIPsRefCounter, - dnsRouterInteval, - statusRecorder, - wgInterface, - fmt.Sprintf("%s:%d", dns.RuntimeIP(), dns.RuntimePort()), - ) + dns := nbdns.NewServiceViaMemory(params.WgInterface) + dnsAddr := fmt.Sprintf("%s:%d", dns.RuntimeIP(), dns.RuntimePort()) + return dynamic.NewRoute(params, dnsAddr) default: - return static.NewRoute(rt, routeRefCounter, allowedIPsRefCounter) + return static.NewRoute(params) } } diff --git a/client/internal/routemanager/client/client_test.go b/client/internal/routemanager/client/client_test.go index e7aff28b6..ec8e0e944 100644 --- a/client/internal/routemanager/client/client_test.go +++ b/client/internal/routemanager/client/client_test.go @@ -7,12 +7,12 @@ import ( "time" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/common" "github.com/netbirdio/netbird/client/internal/routemanager/static" "github.com/netbirdio/netbird/route" ) func TestGetBestrouteFromStatuses(t *testing.T) { - testCases := []struct { name string statuses map[route.ID]routerPeerStatus @@ -811,9 +811,12 @@ func TestGetBestrouteFromStatuses(t *testing.T) { currentRoute = tc.existingRoutes[tc.currentRoute] } + params := common.HandlerParams{ + Route: &route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, + } // create new clientNetwork client := &Watcher{ - handler: static.NewRoute(&route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, nil, nil), + handler: static.NewRoute(params), routes: tc.existingRoutes, currentChosen: currentRoute, } diff --git a/client/internal/routemanager/common/params.go b/client/internal/routemanager/common/params.go new file mode 100644 index 000000000..def18411f --- /dev/null +++ b/client/internal/routemanager/common/params.go @@ -0,0 +1,28 @@ +package common + +import ( + "time" + + "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/internal/dns" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/client/internal/routemanager/fakeip" + "github.com/netbirdio/netbird/client/internal/routemanager/iface" + "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/route" +) + +type HandlerParams struct { + Route *route.Route + RouteRefCounter *refcounter.RouteRefCounter + AllowedIPsRefCounter *refcounter.AllowedIPsRefCounter + DnsRouterInterval time.Duration + StatusRecorder *peer.Status + WgInterface iface.WGIface + DnsServer dns.Server + PeerStore *peerstore.Store + UseNewDNSRoute bool + Firewall manager.Manager + FakeIPManager *fakeip.Manager +} diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index 66557e888..c7c3aeb0b 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/netip" + "runtime" "strings" "sync" @@ -12,11 +13,14 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" + firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface/wgaddr" nbdns "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dnsfwd" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/client/internal/routemanager/common" + "github.com/netbirdio/netbird/client/internal/routemanager/fakeip" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/route" @@ -24,6 +28,11 @@ import ( type domainMap map[domain.Domain][]netip.Prefix +type internalDNATer interface { + RemoveInternalDNATMapping(netip.Addr) error + AddInternalDNATMapping(netip.Addr, netip.Addr) error +} + type wgInterface interface { Name() string Address() wgaddr.Address @@ -40,26 +49,22 @@ type DnsInterceptor struct { interceptedDomains domainMap wgInterface wgInterface peerStore *peerstore.Store + firewall firewall.Manager + fakeIPManager *fakeip.Manager } -func New( - rt *route.Route, - routeRefCounter *refcounter.RouteRefCounter, - allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, - statusRecorder *peer.Status, - dnsServer nbdns.Server, - wgInterface wgInterface, - peerStore *peerstore.Store, -) *DnsInterceptor { +func New(params common.HandlerParams) *DnsInterceptor { return &DnsInterceptor{ - route: rt, - routeRefCounter: routeRefCounter, - allowedIPsRefcounter: allowedIPsRefCounter, - statusRecorder: statusRecorder, - dnsServer: dnsServer, - wgInterface: wgInterface, + route: params.Route, + routeRefCounter: params.RouteRefCounter, + allowedIPsRefcounter: params.AllowedIPsRefCounter, + statusRecorder: params.StatusRecorder, + dnsServer: params.DnsServer, + wgInterface: params.WgInterface, + peerStore: params.PeerStore, + firewall: params.Firewall, + fakeIPManager: params.FakeIPManager, interceptedDomains: make(domainMap), - peerStore: peerStore, } } @@ -78,9 +83,13 @@ func (d *DnsInterceptor) RemoveRoute() error { var merr *multierror.Error for domain, prefixes := range d.interceptedDomains { for _, prefix := range prefixes { - if _, err := d.routeRefCounter.Decrement(prefix); err != nil { - merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %v", prefix, err)) + // Routes should use fake IPs + routePrefix := d.transformRealToFakePrefix(prefix) + if _, err := d.routeRefCounter.Decrement(routePrefix); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %v", routePrefix, err)) } + + // AllowedIPs should use real IPs if d.currentPeerKey != "" { if _, err := d.allowedIPsRefcounter.Decrement(prefix); err != nil { merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %v", prefix, err)) @@ -88,8 +97,10 @@ func (d *DnsInterceptor) RemoveRoute() error { } } log.Debugf("removed dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", prefixes), " ", ", ")) - } + + d.cleanupDNATMappings() + for _, domain := range d.route.Domains { d.statusRecorder.DeleteResolvedDomainsStates(domain) } @@ -102,6 +113,68 @@ func (d *DnsInterceptor) RemoveRoute() error { return nberrors.FormatErrorOrNil(merr) } +// transformRealToFakePrefix returns fake IP prefix for routes (if DNAT enabled) +func (d *DnsInterceptor) transformRealToFakePrefix(realPrefix netip.Prefix) netip.Prefix { + if _, hasDNAT := d.internalDnatFw(); !hasDNAT { + return realPrefix + } + + if fakeIP, ok := d.fakeIPManager.GetFakeIP(realPrefix.Addr()); ok { + return netip.PrefixFrom(fakeIP, realPrefix.Bits()) + } + + return realPrefix +} + +// addAllowedIPForPrefix handles the AllowedIPs logic for a single prefix (uses real IPs) +func (d *DnsInterceptor) addAllowedIPForPrefix(realPrefix netip.Prefix, peerKey string, domain domain.Domain) error { + // AllowedIPs always use real IPs + ref, err := d.allowedIPsRefcounter.Increment(realPrefix, peerKey) + if err != nil { + return fmt.Errorf("add allowed IP %s: %v", realPrefix, err) + } + + if ref.Count > 1 && ref.Out != peerKey { + log.Warnf("IP [%s] for domain [%s] is already routed by peer [%s]. HA routing disabled", + realPrefix.Addr(), + domain.SafeString(), + ref.Out, + ) + } + + return nil +} + +// addRouteAndAllowedIP handles both route and AllowedIPs addition for a prefix +func (d *DnsInterceptor) addRouteAndAllowedIP(realPrefix netip.Prefix, domain domain.Domain) error { + // Routes use fake IPs (so traffic to fake IPs gets routed to interface) + routePrefix := d.transformRealToFakePrefix(realPrefix) + if _, err := d.routeRefCounter.Increment(routePrefix, struct{}{}); err != nil { + return fmt.Errorf("add route for IP %s: %v", routePrefix, err) + } + + // Add to AllowedIPs if we have a current peer (uses real IPs) + if d.currentPeerKey == "" { + return nil + } + + return d.addAllowedIPForPrefix(realPrefix, d.currentPeerKey, domain) +} + +// removeAllowedIP handles AllowedIPs removal for a prefix (uses real IPs) +func (d *DnsInterceptor) removeAllowedIP(realPrefix netip.Prefix) error { + if d.currentPeerKey == "" { + return nil + } + + // AllowedIPs use real IPs + if _, err := d.allowedIPsRefcounter.Decrement(realPrefix); err != nil { + return fmt.Errorf("remove allowed IP %s: %v", realPrefix, err) + } + + return nil +} + func (d *DnsInterceptor) AddAllowedIPs(peerKey string) error { d.mu.Lock() defer d.mu.Unlock() @@ -109,14 +182,9 @@ func (d *DnsInterceptor) AddAllowedIPs(peerKey string) error { var merr *multierror.Error for domain, prefixes := range d.interceptedDomains { for _, prefix := range prefixes { - if ref, err := d.allowedIPsRefcounter.Increment(prefix, peerKey); err != nil { - merr = multierror.Append(merr, fmt.Errorf("add allowed IP %s: %v", prefix, err)) - } else if ref.Count > 1 && ref.Out != peerKey { - log.Warnf("IP [%s] for domain [%s] is already routed by peer [%s]. HA routing disabled", - prefix.Addr(), - domain.SafeString(), - ref.Out, - ) + // AllowedIPs use real IPs + if err := d.addAllowedIPForPrefix(prefix, peerKey, domain); err != nil { + merr = multierror.Append(merr, err) } } } @@ -132,6 +200,7 @@ func (d *DnsInterceptor) RemoveAllowedIPs() error { var merr *multierror.Error for _, prefixes := range d.interceptedDomains { for _, prefix := range prefixes { + // AllowedIPs use real IPs if _, err := d.allowedIPsRefcounter.Decrement(prefix); err != nil { merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %v", prefix, err)) } @@ -287,6 +356,8 @@ func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg) error { if err := d.updateDomainPrefixes(resolvedDomain, originalDomain, newPrefixes); err != nil { log.Errorf("failed to update domain prefixes: %v", err) } + + d.replaceIPsInDNSResponse(r, newPrefixes) } } @@ -297,6 +368,22 @@ func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg) error { return nil } +// logPrefixChanges handles the logging for prefix changes +func (d *DnsInterceptor) logPrefixChanges(resolvedDomain, originalDomain domain.Domain, toAdd, toRemove []netip.Prefix) { + if len(toAdd) > 0 { + log.Debugf("added dynamic route(s) for domain=%s (pattern: domain=%s): %s", + resolvedDomain.SafeString(), + originalDomain.SafeString(), + toAdd) + } + if len(toRemove) > 0 && !d.route.KeepRoute { + log.Debugf("removed dynamic route(s) for domain=%s (pattern: domain=%s): %s", + resolvedDomain.SafeString(), + originalDomain.SafeString(), + toRemove) + } +} + func (d *DnsInterceptor) updateDomainPrefixes(resolvedDomain, originalDomain domain.Domain, newPrefixes []netip.Prefix) error { d.mu.Lock() defer d.mu.Unlock() @@ -305,70 +392,163 @@ func (d *DnsInterceptor) updateDomainPrefixes(resolvedDomain, originalDomain dom toAdd, toRemove := determinePrefixChanges(oldPrefixes, newPrefixes) var merr *multierror.Error + var dnatMappings map[netip.Addr]netip.Addr + + // Handle DNAT mappings for new prefixes + if _, hasDNAT := d.internalDnatFw(); hasDNAT { + dnatMappings = make(map[netip.Addr]netip.Addr) + for _, prefix := range toAdd { + realIP := prefix.Addr() + if fakeIP, err := d.fakeIPManager.AllocateFakeIP(realIP); err == nil { + dnatMappings[fakeIP] = realIP + log.Tracef("allocated fake IP %s for real IP %s", fakeIP, realIP) + } else { + log.Errorf("Failed to allocate fake IP for %s: %v", realIP, err) + } + } + } // Add new prefixes for _, prefix := range toAdd { - if _, err := d.routeRefCounter.Increment(prefix, struct{}{}); err != nil { - merr = multierror.Append(merr, fmt.Errorf("add route for IP %s: %v", prefix, err)) - continue - } - - if d.currentPeerKey == "" { - continue - } - if ref, err := d.allowedIPsRefcounter.Increment(prefix, d.currentPeerKey); err != nil { - merr = multierror.Append(merr, fmt.Errorf("add allowed IP %s: %v", prefix, err)) - } else if ref.Count > 1 && ref.Out != d.currentPeerKey { - log.Warnf("IP [%s] for domain [%s] is already routed by peer [%s]. HA routing disabled", - prefix.Addr(), - resolvedDomain.SafeString(), - ref.Out, - ) + if err := d.addRouteAndAllowedIP(prefix, resolvedDomain); err != nil { + merr = multierror.Append(merr, err) } } + d.addDNATMappings(dnatMappings) + if !d.route.KeepRoute { // Remove old prefixes for _, prefix := range toRemove { - if _, err := d.routeRefCounter.Decrement(prefix); err != nil { - merr = multierror.Append(merr, fmt.Errorf("remove route for IP %s: %v", prefix, err)) + // Routes use fake IPs + routePrefix := d.transformRealToFakePrefix(prefix) + if _, err := d.routeRefCounter.Decrement(routePrefix); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove route for IP %s: %v", routePrefix, err)) } - if d.currentPeerKey != "" { - if _, err := d.allowedIPsRefcounter.Decrement(prefix); err != nil { - merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %v", prefix, err)) - } + // AllowedIPs use real IPs + if err := d.removeAllowedIP(prefix); err != nil { + merr = multierror.Append(merr, err) } } + + d.removeDNATMappings(toRemove) } - // Update domain prefixes using resolved domain as key + // Update domain prefixes using resolved domain as key - store real IPs if len(toAdd) > 0 || len(toRemove) > 0 { if d.route.KeepRoute { - // replace stored prefixes with old + added // nolint:gocritic newPrefixes = append(oldPrefixes, toAdd...) } d.interceptedDomains[resolvedDomain] = newPrefixes originalDomain = domain.Domain(strings.TrimSuffix(string(originalDomain), ".")) + + // Store real IPs for status (user-facing), not fake IPs d.statusRecorder.UpdateResolvedDomainsStates(originalDomain, resolvedDomain, newPrefixes, d.route.GetResourceID()) - if len(toAdd) > 0 { - log.Debugf("added dynamic route(s) for domain=%s (pattern: domain=%s): %s", - resolvedDomain.SafeString(), - originalDomain.SafeString(), - toAdd) - } - if len(toRemove) > 0 && !d.route.KeepRoute { - log.Debugf("removed dynamic route(s) for domain=%s (pattern: domain=%s): %s", - resolvedDomain.SafeString(), - originalDomain.SafeString(), - toRemove) - } + d.logPrefixChanges(resolvedDomain, originalDomain, toAdd, toRemove) } return nberrors.FormatErrorOrNil(merr) } +// removeDNATMappings removes DNAT mappings from the firewall for real IP prefixes +func (d *DnsInterceptor) removeDNATMappings(realPrefixes []netip.Prefix) { + if len(realPrefixes) == 0 { + return + } + + dnatFirewall, ok := d.internalDnatFw() + if !ok { + return + } + + for _, prefix := range realPrefixes { + realIP := prefix.Addr() + if fakeIP, exists := d.fakeIPManager.GetFakeIP(realIP); exists { + if err := dnatFirewall.RemoveInternalDNATMapping(fakeIP); err != nil { + log.Errorf("Failed to remove DNAT mapping for %s: %v", fakeIP, err) + } else { + log.Debugf("Removed DNAT mapping for: %s -> %s", fakeIP, realIP) + } + } + } +} + +// internalDnatFw checks if the firewall supports internal DNAT +func (d *DnsInterceptor) internalDnatFw() (internalDNATer, bool) { + if d.firewall == nil || runtime.GOOS != "android" { + return nil, false + } + fw, ok := d.firewall.(internalDNATer) + return fw, ok +} + +// addDNATMappings adds DNAT mappings to the firewall +func (d *DnsInterceptor) addDNATMappings(mappings map[netip.Addr]netip.Addr) { + if len(mappings) == 0 { + return + } + + dnatFirewall, ok := d.internalDnatFw() + if !ok { + return + } + + for fakeIP, realIP := range mappings { + if err := dnatFirewall.AddInternalDNATMapping(fakeIP, realIP); err != nil { + log.Errorf("Failed to add DNAT mapping %s -> %s: %v", fakeIP, realIP, err) + } else { + log.Debugf("Added DNAT mapping: %s -> %s", fakeIP, realIP) + } + } +} + +// cleanupDNATMappings removes all DNAT mappings for this interceptor +func (d *DnsInterceptor) cleanupDNATMappings() { + if _, ok := d.internalDnatFw(); !ok { + return + } + + for _, prefixes := range d.interceptedDomains { + d.removeDNATMappings(prefixes) + } +} + +// replaceIPsInDNSResponse replaces real IPs with fake IPs in the DNS response +func (d *DnsInterceptor) replaceIPsInDNSResponse(reply *dns.Msg, realPrefixes []netip.Prefix) { + if _, ok := d.internalDnatFw(); !ok { + return + } + + // Replace A and AAAA records with fake IPs + for _, answer := range reply.Answer { + switch rr := answer.(type) { + case *dns.A: + realIP, ok := netip.AddrFromSlice(rr.A) + if !ok { + continue + } + + if fakeIP, exists := d.fakeIPManager.GetFakeIP(realIP); exists { + rr.A = fakeIP.AsSlice() + log.Tracef("Replaced real IP %s with fake IP %s in DNS response", realIP, fakeIP) + } + + case *dns.AAAA: + realIP, ok := netip.AddrFromSlice(rr.AAAA) + if !ok { + continue + } + + if fakeIP, exists := d.fakeIPManager.GetFakeIP(realIP); exists { + rr.AAAA = fakeIP.AsSlice() + log.Tracef("Replaced real IP %s with fake IP %s in DNS response", realIP, fakeIP) + } + } + } +} + func determinePrefixChanges(oldPrefixes, newPrefixes []netip.Prefix) (toAdd, toRemove []netip.Prefix) { prefixSet := make(map[netip.Prefix]bool) for _, prefix := range oldPrefixes { diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go index 47511d4af..5d561f0cf 100644 --- a/client/internal/routemanager/dynamic/route.go +++ b/client/internal/routemanager/dynamic/route.go @@ -14,6 +14,7 @@ import ( nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/common" "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/util" @@ -52,24 +53,16 @@ type Route struct { resolverAddr string } -func NewRoute( - rt *route.Route, - routeRefCounter *refcounter.RouteRefCounter, - allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, - interval time.Duration, - statusRecorder *peer.Status, - wgInterface iface.WGIface, - resolverAddr string, -) *Route { +func NewRoute(params common.HandlerParams, resolverAddr string) *Route { return &Route{ - route: rt, - routeRefCounter: routeRefCounter, - allowedIPsRefcounter: allowedIPsRefCounter, - interval: interval, - dynamicDomains: domainMap{}, - statusRecorder: statusRecorder, - wgInterface: wgInterface, + route: params.Route, + routeRefCounter: params.RouteRefCounter, + allowedIPsRefcounter: params.AllowedIPsRefCounter, + interval: params.DnsRouterInterval, + statusRecorder: params.StatusRecorder, + wgInterface: params.WgInterface, resolverAddr: resolverAddr, + dynamicDomains: domainMap{}, } } diff --git a/client/internal/routemanager/fakeip/fakeip.go b/client/internal/routemanager/fakeip/fakeip.go new file mode 100644 index 000000000..1592045d2 --- /dev/null +++ b/client/internal/routemanager/fakeip/fakeip.go @@ -0,0 +1,93 @@ +package fakeip + +import ( + "fmt" + "net/netip" + "sync" +) + +// Manager manages allocation of fake IPs from the 240.0.0.0/8 block +type Manager struct { + mu sync.Mutex + nextIP netip.Addr // Next IP to allocate + allocated map[netip.Addr]netip.Addr // real IP -> fake IP + fakeToReal map[netip.Addr]netip.Addr // fake IP -> real IP + baseIP netip.Addr // First usable IP: 240.0.0.1 + maxIP netip.Addr // Last usable IP: 240.255.255.254 +} + +// NewManager creates a new fake IP manager using 240.0.0.0/8 block +func NewManager() *Manager { + baseIP := netip.AddrFrom4([4]byte{240, 0, 0, 1}) + maxIP := netip.AddrFrom4([4]byte{240, 255, 255, 254}) + + return &Manager{ + nextIP: baseIP, + allocated: make(map[netip.Addr]netip.Addr), + fakeToReal: make(map[netip.Addr]netip.Addr), + baseIP: baseIP, + maxIP: maxIP, + } +} + +// AllocateFakeIP allocates a fake IP for the given real IP +// Returns the fake IP, or existing fake IP if already allocated +func (m *Manager) AllocateFakeIP(realIP netip.Addr) (netip.Addr, error) { + if !realIP.Is4() { + return netip.Addr{}, fmt.Errorf("only IPv4 addresses supported") + } + + m.mu.Lock() + defer m.mu.Unlock() + + if fakeIP, exists := m.allocated[realIP]; exists { + return fakeIP, nil + } + + startIP := m.nextIP + for { + currentIP := m.nextIP + + // Advance to next IP, wrapping at boundary + if m.nextIP.Compare(m.maxIP) >= 0 { + m.nextIP = m.baseIP + } else { + m.nextIP = m.nextIP.Next() + } + + // Check if current IP is available + if _, inUse := m.fakeToReal[currentIP]; !inUse { + m.allocated[realIP] = currentIP + m.fakeToReal[currentIP] = realIP + return currentIP, nil + } + + // Prevent infinite loop if all IPs exhausted + if m.nextIP.Compare(startIP) == 0 { + return netip.Addr{}, fmt.Errorf("no more fake IPs available in 240.0.0.0/8 block") + } + } +} + +// GetFakeIP returns the fake IP for a real IP if it exists +func (m *Manager) GetFakeIP(realIP netip.Addr) (netip.Addr, bool) { + m.mu.Lock() + defer m.mu.Unlock() + + fakeIP, exists := m.allocated[realIP] + return fakeIP, exists +} + +// GetRealIP returns the real IP for a fake IP if it exists, otherwise false +func (m *Manager) GetRealIP(fakeIP netip.Addr) (netip.Addr, bool) { + m.mu.Lock() + defer m.mu.Unlock() + + realIP, exists := m.fakeToReal[fakeIP] + return realIP, exists +} + +// GetFakeIPBlock returns the fake IP block used by this manager +func (m *Manager) GetFakeIPBlock() netip.Prefix { + return netip.MustParsePrefix("240.0.0.0/8") +} diff --git a/client/internal/routemanager/fakeip/fakeip_test.go b/client/internal/routemanager/fakeip/fakeip_test.go new file mode 100644 index 000000000..ad3e4bd4e --- /dev/null +++ b/client/internal/routemanager/fakeip/fakeip_test.go @@ -0,0 +1,240 @@ +package fakeip + +import ( + "net/netip" + "sync" + "testing" +) + +func TestNewManager(t *testing.T) { + manager := NewManager() + + if manager.baseIP.String() != "240.0.0.1" { + t.Errorf("Expected base IP 240.0.0.1, got %s", manager.baseIP.String()) + } + + if manager.maxIP.String() != "240.255.255.254" { + t.Errorf("Expected max IP 240.255.255.254, got %s", manager.maxIP.String()) + } + + if manager.nextIP.Compare(manager.baseIP) != 0 { + t.Errorf("Expected nextIP to start at baseIP") + } +} + +func TestAllocateFakeIP(t *testing.T) { + manager := NewManager() + realIP := netip.MustParseAddr("8.8.8.8") + + fakeIP, err := manager.AllocateFakeIP(realIP) + if err != nil { + t.Fatalf("Failed to allocate fake IP: %v", err) + } + + if !fakeIP.Is4() { + t.Error("Fake IP should be IPv4") + } + + // Check it's in the correct range + if fakeIP.As4()[0] != 240 { + t.Errorf("Fake IP should be in 240.0.0.0/8 range, got %s", fakeIP.String()) + } + + // Should return same fake IP for same real IP + fakeIP2, err := manager.AllocateFakeIP(realIP) + if err != nil { + t.Fatalf("Failed to get existing fake IP: %v", err) + } + + if fakeIP.Compare(fakeIP2) != 0 { + t.Errorf("Expected same fake IP for same real IP, got %s and %s", fakeIP.String(), fakeIP2.String()) + } +} + +func TestAllocateFakeIPIPv6Rejection(t *testing.T) { + manager := NewManager() + realIPv6 := netip.MustParseAddr("2001:db8::1") + + _, err := manager.AllocateFakeIP(realIPv6) + if err == nil { + t.Error("Expected error for IPv6 address") + } +} + +func TestGetFakeIP(t *testing.T) { + manager := NewManager() + realIP := netip.MustParseAddr("1.1.1.1") + + // Should not exist initially + _, exists := manager.GetFakeIP(realIP) + if exists { + t.Error("Fake IP should not exist before allocation") + } + + // Allocate and check + expectedFakeIP, err := manager.AllocateFakeIP(realIP) + if err != nil { + t.Fatalf("Failed to allocate: %v", err) + } + + fakeIP, exists := manager.GetFakeIP(realIP) + if !exists { + t.Error("Fake IP should exist after allocation") + } + + if fakeIP.Compare(expectedFakeIP) != 0 { + t.Errorf("Expected %s, got %s", expectedFakeIP.String(), fakeIP.String()) + } +} + +func TestMultipleAllocations(t *testing.T) { + manager := NewManager() + + allocations := make(map[netip.Addr]netip.Addr) + + // Allocate multiple IPs + for i := 1; i <= 100; i++ { + realIP := netip.AddrFrom4([4]byte{10, 0, byte(i / 256), byte(i % 256)}) + fakeIP, err := manager.AllocateFakeIP(realIP) + if err != nil { + t.Fatalf("Failed to allocate fake IP for %s: %v", realIP.String(), err) + } + + // Check for duplicates + for _, existingFake := range allocations { + if fakeIP.Compare(existingFake) == 0 { + t.Errorf("Duplicate fake IP allocated: %s", fakeIP.String()) + } + } + + allocations[realIP] = fakeIP + } + + // Verify all allocations can be retrieved + for realIP, expectedFake := range allocations { + actualFake, exists := manager.GetFakeIP(realIP) + if !exists { + t.Errorf("Missing allocation for %s", realIP.String()) + } + if actualFake.Compare(expectedFake) != 0 { + t.Errorf("Mismatch for %s: expected %s, got %s", realIP.String(), expectedFake.String(), actualFake.String()) + } + } +} + +func TestGetFakeIPBlock(t *testing.T) { + manager := NewManager() + block := manager.GetFakeIPBlock() + + expected := "240.0.0.0/8" + if block.String() != expected { + t.Errorf("Expected %s, got %s", expected, block.String()) + } +} + +func TestConcurrentAccess(t *testing.T) { + manager := NewManager() + + const numGoroutines = 50 + const allocationsPerGoroutine = 10 + + var wg sync.WaitGroup + results := make(chan netip.Addr, numGoroutines*allocationsPerGoroutine) + + // Concurrent allocations + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(goroutineID int) { + defer wg.Done() + for j := 0; j < allocationsPerGoroutine; j++ { + realIP := netip.AddrFrom4([4]byte{192, 168, byte(goroutineID), byte(j)}) + fakeIP, err := manager.AllocateFakeIP(realIP) + if err != nil { + t.Errorf("Failed to allocate in goroutine %d: %v", goroutineID, err) + return + } + results <- fakeIP + } + }(i) + } + + wg.Wait() + close(results) + + // Check for duplicates + seen := make(map[netip.Addr]bool) + count := 0 + for fakeIP := range results { + if seen[fakeIP] { + t.Errorf("Duplicate fake IP in concurrent test: %s", fakeIP.String()) + } + seen[fakeIP] = true + count++ + } + + if count != numGoroutines*allocationsPerGoroutine { + t.Errorf("Expected %d allocations, got %d", numGoroutines*allocationsPerGoroutine, count) + } +} + +func TestIPExhaustion(t *testing.T) { + // Create a manager with limited range for testing + manager := &Manager{ + nextIP: netip.AddrFrom4([4]byte{240, 0, 0, 1}), + allocated: make(map[netip.Addr]netip.Addr), + fakeToReal: make(map[netip.Addr]netip.Addr), + baseIP: netip.AddrFrom4([4]byte{240, 0, 0, 1}), + maxIP: netip.AddrFrom4([4]byte{240, 0, 0, 3}), // Only 3 IPs available + } + + // Allocate all available IPs + realIPs := []netip.Addr{ + netip.MustParseAddr("1.0.0.1"), + netip.MustParseAddr("1.0.0.2"), + netip.MustParseAddr("1.0.0.3"), + } + + for _, realIP := range realIPs { + _, err := manager.AllocateFakeIP(realIP) + if err != nil { + t.Fatalf("Failed to allocate fake IP: %v", err) + } + } + + // Try to allocate one more - should fail + _, err := manager.AllocateFakeIP(netip.MustParseAddr("1.0.0.4")) + if err == nil { + t.Error("Expected exhaustion error") + } +} + +func TestWrapAround(t *testing.T) { + // Create manager starting near the end of range + manager := &Manager{ + nextIP: netip.AddrFrom4([4]byte{240, 0, 0, 254}), + allocated: make(map[netip.Addr]netip.Addr), + fakeToReal: make(map[netip.Addr]netip.Addr), + baseIP: netip.AddrFrom4([4]byte{240, 0, 0, 1}), + maxIP: netip.AddrFrom4([4]byte{240, 0, 0, 254}), + } + + // Allocate the last IP + fakeIP1, err := manager.AllocateFakeIP(netip.MustParseAddr("1.0.0.1")) + if err != nil { + t.Fatalf("Failed to allocate first IP: %v", err) + } + + if fakeIP1.String() != "240.0.0.254" { + t.Errorf("Expected 240.0.0.254, got %s", fakeIP1.String()) + } + + // Next allocation should wrap around to the beginning + fakeIP2, err := manager.AllocateFakeIP(netip.MustParseAddr("1.0.0.2")) + if err != nil { + t.Fatalf("Failed to allocate second IP: %v", err) + } + + if fakeIP2.String() != "240.0.0.1" { + t.Errorf("Expected 240.0.0.1 after wrap, got %s", fakeIP2.String()) + } +} diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 919bf25e3..e0974ab2a 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -8,9 +8,11 @@ import ( "net/netip" "net/url" "runtime" + "slices" "sync" "time" + "github.com/google/uuid" "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" @@ -24,6 +26,8 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peerstore" "github.com/netbirdio/netbird/client/internal/routemanager/client" + "github.com/netbirdio/netbird/client/internal/routemanager/common" + "github.com/netbirdio/netbird/client/internal/routemanager/fakeip" "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" @@ -49,7 +53,7 @@ type Manager interface { GetClientRoutesWithNetID() map[route.NetID][]*route.Route SetRouteChangeListener(listener listener.NetworkChangeListener) InitialRouteRange() []string - EnableServerRouter(firewall firewall.Manager) error + SetFirewall(firewall.Manager) error Stop(stateManager *statemanager.Manager) } @@ -63,6 +67,7 @@ type ManagerConfig struct { InitialRoutes []*route.Route StateManager *statemanager.Manager DNSServer dns.Server + DNSFeatureFlag bool PeerStore *peerstore.Store DisableClientRoutes bool DisableServerRoutes bool @@ -89,11 +94,13 @@ type DefaultManager struct { // clientRoutes is the most recent list of clientRoutes received from the Management Service clientRoutes route.HAMap dnsServer dns.Server + firewall firewall.Manager peerStore *peerstore.Store useNewDNSRoute bool disableClientRoutes bool disableServerRoutes bool activeRoutes map[route.HAUniqueID]client.RouteHandler + fakeIPManager *fakeip.Manager } func NewManager(config ManagerConfig) *DefaultManager { @@ -129,11 +136,31 @@ func NewManager(config ManagerConfig) *DefaultManager { } if runtime.GOOS == "android" { - cr := dm.initialClientRoutes(config.InitialRoutes) - dm.notifier.SetInitialClientRoutes(cr) + dm.setupAndroidRoutes(config) } return dm } +func (m *DefaultManager) setupAndroidRoutes(config ManagerConfig) { + cr := m.initialClientRoutes(config.InitialRoutes) + + routesForComparison := slices.Clone(cr) + + if config.DNSFeatureFlag { + m.fakeIPManager = fakeip.NewManager() + + id := uuid.NewString() + fakeIPRoute := &route.Route{ + ID: route.ID(id), + Network: m.fakeIPManager.GetFakeIPBlock(), + NetID: route.NetID(id), + Peer: m.pubKey, + NetworkType: route.IPv4Network, + } + cr = append(cr, fakeIPRoute) + } + + m.notifier.SetInitialClientRoutes(cr, routesForComparison) +} func (m *DefaultManager) setupRefCounters(useNoop bool) { m.routeRefCounter = refcounter.New( @@ -222,16 +249,16 @@ func (m *DefaultManager) initSelector() *routeselector.RouteSelector { return routeselector.NewRouteSelector() } -func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error { - if m.disableServerRoutes { +// SetFirewall sets the firewall manager for the DefaultManager +// Not thread-safe, should be called before starting the manager +func (m *DefaultManager) SetFirewall(firewall firewall.Manager) error { + m.firewall = firewall + + if m.disableServerRoutes || firewall == nil { log.Info("server routes are disabled") return nil } - if firewall == nil { - return errors.New("firewall manager is not set") - } - var err error m.serverRouter, err = server.NewRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder) if err != nil { @@ -299,17 +326,20 @@ func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error { } for id, route := range toAdd { - handler := client.HandlerFromRoute( - route, - m.routeRefCounter, - m.allowedIPsRefCounter, - m.dnsRouteInterval, - m.statusRecorder, - m.wgInterface, - m.dnsServer, - m.peerStore, - m.useNewDNSRoute, - ) + params := common.HandlerParams{ + Route: route, + RouteRefCounter: m.routeRefCounter, + AllowedIPsRefCounter: m.allowedIPsRefCounter, + DnsRouterInterval: m.dnsRouteInterval, + StatusRecorder: m.statusRecorder, + WgInterface: m.wgInterface, + DnsServer: m.dnsServer, + PeerStore: m.peerStore, + UseNewDNSRoute: m.useNewDNSRoute, + Firewall: m.firewall, + FakeIPManager: m.fakeIPManager, + } + handler := client.HandlerFromRoute(params) if err := handler.AddRoute(m.ctx); err != nil { merr = multierror.Append(merr, fmt.Errorf("add route %s: %w", handler.String(), err)) continue @@ -517,6 +547,7 @@ func (m *DefaultManager) initialClientRoutes(initialRoutes []*route.Route) []*ro for _, routes := range crMap { rs = append(rs, routes...) } + return rs } diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index 742294cdf..4e182f82c 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -87,7 +87,7 @@ func (m *MockManager) SetRouteChangeListener(listener listener.NetworkChangeList } -func (m *MockManager) EnableServerRouter(firewall firewall.Manager) error { +func (m *MockManager) SetFirewall(firewall.Manager) error { panic("implement me") } diff --git a/client/internal/routemanager/notifier/notifier.go b/client/internal/routemanager/notifier/notifier.go deleted file mode 100644 index 3cc7c3308..000000000 --- a/client/internal/routemanager/notifier/notifier.go +++ /dev/null @@ -1,124 +0,0 @@ -package notifier - -import ( - "net/netip" - "runtime" - "sort" - "strings" - "sync" - - "github.com/netbirdio/netbird/client/internal/listener" - "github.com/netbirdio/netbird/route" -) - -type Notifier struct { - initialRouteRanges []string - routeRanges []string - - listener listener.NetworkChangeListener - listenerMux sync.Mutex -} - -func NewNotifier() *Notifier { - return &Notifier{} -} - -func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { - n.listenerMux.Lock() - defer n.listenerMux.Unlock() - n.listener = listener -} - -func (n *Notifier) SetInitialClientRoutes(clientRoutes []*route.Route) { - nets := make([]string, 0) - for _, r := range clientRoutes { - if r.IsDynamic() { - continue - } - nets = append(nets, r.Network.String()) - } - sort.Strings(nets) - n.initialRouteRanges = nets -} - -func (n *Notifier) OnNewRoutes(idMap route.HAMap) { - if runtime.GOOS != "android" { - return - } - - var newNets []string - for _, routes := range idMap { - for _, r := range routes { - if r.IsDynamic() { - continue - } - newNets = append(newNets, r.Network.String()) - } - } - - sort.Strings(newNets) - if !n.hasDiff(n.initialRouteRanges, newNets) { - return - } - - n.routeRanges = newNets - n.notify() -} - -// OnNewPrefixes is called from iOS only -func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) { - newNets := make([]string, 0) - for _, prefix := range prefixes { - newNets = append(newNets, prefix.String()) - } - - sort.Strings(newNets) - if !n.hasDiff(n.routeRanges, newNets) { - return - } - - n.routeRanges = newNets - n.notify() -} - -func (n *Notifier) notify() { - n.listenerMux.Lock() - defer n.listenerMux.Unlock() - if n.listener == nil { - return - } - - go func(l listener.NetworkChangeListener) { - l.OnNetworkChanged(strings.Join(addIPv6RangeIfNeeded(n.routeRanges), ",")) - }(n.listener) -} - -func (n *Notifier) hasDiff(a []string, b []string) bool { - if len(a) != len(b) { - return true - } - for i, v := range a { - if v != b[i] { - return true - } - } - return false -} - -func (n *Notifier) GetInitialRouteRanges() []string { - return addIPv6RangeIfNeeded(n.initialRouteRanges) -} - -// addIPv6RangeIfNeeded returns the input ranges with the default IPv6 range when there is an IPv4 default route. -func addIPv6RangeIfNeeded(inputRanges []string) []string { - ranges := inputRanges - for _, r := range inputRanges { - // we are intentionally adding the ipv6 default range in case of ipv4 default range - // to ensure that all traffic is managed by the tunnel interface on android - if r == "0.0.0.0/0" { - ranges = append(ranges, "::/0") - break - } - } - return ranges -} diff --git a/client/internal/routemanager/notifier/notifier_android.go b/client/internal/routemanager/notifier/notifier_android.go new file mode 100644 index 000000000..dec0af87c --- /dev/null +++ b/client/internal/routemanager/notifier/notifier_android.go @@ -0,0 +1,127 @@ +//go:build android + +package notifier + +import ( + "net/netip" + "slices" + "sort" + "strings" + "sync" + + "github.com/netbirdio/netbird/client/internal/listener" + "github.com/netbirdio/netbird/route" +) + +type Notifier struct { + initialRoutes []*route.Route + currentRoutes []*route.Route + + listener listener.NetworkChangeListener + listenerMux sync.Mutex +} + +func NewNotifier() *Notifier { + return &Notifier{} +} + +func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + n.listener = listener +} + +func (n *Notifier) SetInitialClientRoutes(initialRoutes []*route.Route, routesForComparison []*route.Route) { + // initialRoutes contains fake IP block for interface configuration + filteredInitial := make([]*route.Route, 0) + for _, r := range initialRoutes { + if r.IsDynamic() { + continue + } + filteredInitial = append(filteredInitial, r) + } + n.initialRoutes = filteredInitial + + // routesForComparison excludes fake IP block for comparison with new routes + filteredComparison := make([]*route.Route, 0) + for _, r := range routesForComparison { + if r.IsDynamic() { + continue + } + filteredComparison = append(filteredComparison, r) + } + n.currentRoutes = filteredComparison +} + +func (n *Notifier) OnNewRoutes(idMap route.HAMap) { + var newRoutes []*route.Route + for _, routes := range idMap { + for _, r := range routes { + if r.IsDynamic() { + continue + } + newRoutes = append(newRoutes, r) + } + } + + if !n.hasRouteDiff(n.currentRoutes, newRoutes) { + return + } + + n.currentRoutes = newRoutes + n.notify() +} + +func (n *Notifier) OnNewPrefixes([]netip.Prefix) { + // Not used on Android +} + +func (n *Notifier) notify() { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + if n.listener == nil { + return + } + + routeStrings := n.routesToStrings(n.currentRoutes) + sort.Strings(routeStrings) + go func(l listener.NetworkChangeListener) { + l.OnNetworkChanged(strings.Join(n.addIPv6RangeIfNeeded(routeStrings, n.currentRoutes), ",")) + }(n.listener) +} + +func (n *Notifier) routesToStrings(routes []*route.Route) []string { + nets := make([]string, 0, len(routes)) + for _, r := range routes { + nets = append(nets, r.NetString()) + } + return nets +} + +func (n *Notifier) hasRouteDiff(a []*route.Route, b []*route.Route) bool { + slices.SortFunc(a, func(x, y *route.Route) int { + return strings.Compare(x.NetString(), y.NetString()) + }) + slices.SortFunc(b, func(x, y *route.Route) int { + return strings.Compare(x.NetString(), y.NetString()) + }) + + return !slices.EqualFunc(a, b, func(x, y *route.Route) bool { + return x.NetString() == y.NetString() + }) +} + +func (n *Notifier) GetInitialRouteRanges() []string { + initialStrings := n.routesToStrings(n.initialRoutes) + sort.Strings(initialStrings) + return n.addIPv6RangeIfNeeded(initialStrings, n.initialRoutes) +} + +func (n *Notifier) addIPv6RangeIfNeeded(inputRanges []string, routes []*route.Route) []string { + for _, r := range routes { + if r.Network.Addr().Is4() && r.Network.Bits() == 0 { + return append(slices.Clone(inputRanges), "::/0") + } + } + return inputRanges +} diff --git a/client/internal/routemanager/notifier/notifier_ios.go b/client/internal/routemanager/notifier/notifier_ios.go new file mode 100644 index 000000000..bb125cfa4 --- /dev/null +++ b/client/internal/routemanager/notifier/notifier_ios.go @@ -0,0 +1,80 @@ +//go:build ios + +package notifier + +import ( + "net/netip" + "slices" + "sort" + "strings" + "sync" + + "github.com/netbirdio/netbird/client/internal/listener" + "github.com/netbirdio/netbird/route" +) + +type Notifier struct { + currentPrefixes []string + + listener listener.NetworkChangeListener + listenerMux sync.Mutex +} + +func NewNotifier() *Notifier { + return &Notifier{} +} + +func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + n.listener = listener +} + +func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) { + // iOS doesn't care about initial routes +} + +func (n *Notifier) OnNewRoutes(route.HAMap) { + // Not used on iOS +} + +func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) { + newNets := make([]string, 0) + for _, prefix := range prefixes { + newNets = append(newNets, prefix.String()) + } + + sort.Strings(newNets) + + if slices.Equal(n.currentPrefixes, newNets) { + return + } + + n.currentPrefixes = newNets + n.notify() +} + +func (n *Notifier) notify() { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + if n.listener == nil { + return + } + + go func(l listener.NetworkChangeListener) { + l.OnNetworkChanged(strings.Join(n.addIPv6RangeIfNeeded(n.currentPrefixes), ",")) + }(n.listener) +} + +func (n *Notifier) GetInitialRouteRanges() []string { + return nil +} + +func (n *Notifier) addIPv6RangeIfNeeded(inputRanges []string) []string { + for _, r := range inputRanges { + if r == "0.0.0.0/0" { + return append(slices.Clone(inputRanges), "::/0") + } + } + return inputRanges +} diff --git a/client/internal/routemanager/notifier/notifier_other.go b/client/internal/routemanager/notifier/notifier_other.go new file mode 100644 index 000000000..77045b839 --- /dev/null +++ b/client/internal/routemanager/notifier/notifier_other.go @@ -0,0 +1,36 @@ +//go:build !android && !ios + +package notifier + +import ( + "net/netip" + + "github.com/netbirdio/netbird/client/internal/listener" + "github.com/netbirdio/netbird/route" +) + +type Notifier struct{} + +func NewNotifier() *Notifier { + return &Notifier{} +} + +func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { + // Not used on non-mobile platforms +} + +func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) { + // Not used on non-mobile platforms +} + +func (n *Notifier) OnNewRoutes(idMap route.HAMap) { + // Not used on non-mobile platforms +} + +func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) { + // Not used on non-mobile platforms +} + +func (n *Notifier) GetInitialRouteRanges() []string { + return []string{} +} \ No newline at end of file diff --git a/client/internal/routemanager/static/route.go b/client/internal/routemanager/static/route.go index c8b9338e0..d480fdf00 100644 --- a/client/internal/routemanager/static/route.go +++ b/client/internal/routemanager/static/route.go @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal/routemanager/common" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/route" ) @@ -16,11 +17,11 @@ type Route struct { allowedIPsRefcounter *refcounter.AllowedIPsRefCounter } -func NewRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter) *Route { +func NewRoute(params common.HandlerParams) *Route { return &Route{ - route: rt, - routeRefCounter: routeRefCounter, - allowedIPsRefcounter: allowedIPsRefCounter, + route: params.Route, + routeRefCounter: params.RouteRefCounter, + allowedIPsRefcounter: params.AllowedIPsRefCounter, } } From fbb1b55beb95d3106e7a279931a69bac34f0af1f Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 4 Jul 2025 19:52:27 +0200 Subject: [PATCH 136/213] [client] refactor lazy detection (#4050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces a new inactivity package responsible for monitoring peer activity and notifying when peers become inactive. Introduces a new Signal message type to close the peer connection after the idle timeout is reached. Periodically checks the last activity of registered peers via a Bind interface. Notifies via a channel when peers exceed a configurable inactivity threshold. Default settings DefaultInactivityThreshold is set to 15 minutes, with a minimum allowed threshold of 1 minute. Limitations This inactivity check does not support kernel WireGuard integration. In kernel–user space communication, the user space side will always be responsible for closing the connection. --- client/iface/bind/activity.go | 94 +++++ client/iface/bind/activity_test.go | 27 ++ client/iface/bind/ice_bind.go | 57 ++- client/iface/configurer/kernel_unix.go | 4 + client/iface/configurer/usp.go | 36 +- client/iface/device/device_android.go | 2 +- client/iface/device/device_darwin.go | 2 +- client/iface/device/device_ios.go | 2 +- client/iface/device/device_netstack.go | 2 +- client/iface/device/device_usp_unix.go | 2 +- client/iface/device/device_windows.go | 2 +- client/iface/device/interface.go | 1 + client/iface/iface.go | 8 + client/internal/conn_mgr.go | 66 ++-- client/internal/engine.go | 32 +- client/internal/engine_test.go | 15 +- client/internal/iface_common.go | 1 + client/internal/lazyconn/activity/listener.go | 4 +- client/internal/lazyconn/activity/manager.go | 13 +- .../lazyconn/inactivity/inactivity.go | 75 ---- .../lazyconn/inactivity/inactivity_test.go | 156 --------- .../internal/lazyconn/inactivity/manager.go | 152 ++++++++ .../lazyconn/inactivity/manager_test.go | 113 ++++++ client/internal/lazyconn/inactivity/ticker.go | 24 ++ client/internal/lazyconn/manager/manager.go | 326 ++++++++---------- client/internal/lazyconn/wgiface.go | 2 + client/internal/peer/conn.go | 49 ++- client/internal/peer/signaler.go | 10 + client/internal/peerstore/store.go | 13 +- monotime/time.go | 29 ++ monotime/time_test.go | 20 ++ signal/proto/signalexchange.pb.go | 58 ++-- signal/proto/signalexchange.proto | 3 +- 33 files changed, 857 insertions(+), 543 deletions(-) create mode 100644 client/iface/bind/activity.go create mode 100644 client/iface/bind/activity_test.go delete mode 100644 client/internal/lazyconn/inactivity/inactivity.go delete mode 100644 client/internal/lazyconn/inactivity/inactivity_test.go create mode 100644 client/internal/lazyconn/inactivity/manager.go create mode 100644 client/internal/lazyconn/inactivity/manager_test.go create mode 100644 client/internal/lazyconn/inactivity/ticker.go create mode 100644 monotime/time.go create mode 100644 monotime/time_test.go diff --git a/client/iface/bind/activity.go b/client/iface/bind/activity.go new file mode 100644 index 000000000..d3b406bcd --- /dev/null +++ b/client/iface/bind/activity.go @@ -0,0 +1,94 @@ +package bind + +import ( + "net/netip" + "sync" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/monotime" +) + +const ( + saveFrequency = int64(5 * time.Second) +) + +type PeerRecord struct { + Address netip.AddrPort + LastActivity atomic.Int64 // UnixNano timestamp +} + +type ActivityRecorder struct { + mu sync.RWMutex + peers map[string]*PeerRecord // publicKey to PeerRecord map + addrToPeer map[netip.AddrPort]*PeerRecord // address to PeerRecord map +} + +func NewActivityRecorder() *ActivityRecorder { + return &ActivityRecorder{ + peers: make(map[string]*PeerRecord), + addrToPeer: make(map[netip.AddrPort]*PeerRecord), + } +} + +// GetLastActivities returns a snapshot of peer last activity +func (r *ActivityRecorder) GetLastActivities() map[string]time.Time { + r.mu.RLock() + defer r.mu.RUnlock() + + activities := make(map[string]time.Time, len(r.peers)) + for key, record := range r.peers { + unixNano := record.LastActivity.Load() + activities[key] = time.Unix(0, unixNano) + } + return activities +} + +// UpsertAddress adds or updates the address for a publicKey +func (r *ActivityRecorder) UpsertAddress(publicKey string, address netip.AddrPort) { + r.mu.Lock() + defer r.mu.Unlock() + + if pr, exists := r.peers[publicKey]; exists { + delete(r.addrToPeer, pr.Address) + pr.Address = address + } else { + record := &PeerRecord{ + Address: address, + } + record.LastActivity.Store(monotime.Now()) + r.peers[publicKey] = record + } + + r.addrToPeer[address] = r.peers[publicKey] +} + +func (r *ActivityRecorder) Remove(publicKey string) { + r.mu.Lock() + defer r.mu.Unlock() + if record, exists := r.peers[publicKey]; exists { + delete(r.addrToPeer, record.Address) + delete(r.peers, publicKey) + } +} + +// record updates LastActivity for the given address using atomic store +func (r *ActivityRecorder) record(address netip.AddrPort) { + r.mu.RLock() + record, ok := r.addrToPeer[address] + r.mu.RUnlock() + if !ok { + log.Warnf("could not find record for address %s", address) + return + } + + now := monotime.Now() + last := record.LastActivity.Load() + if now-last < saveFrequency { + return + } + + _ = record.LastActivity.CompareAndSwap(last, now) +} diff --git a/client/iface/bind/activity_test.go b/client/iface/bind/activity_test.go new file mode 100644 index 000000000..598607b95 --- /dev/null +++ b/client/iface/bind/activity_test.go @@ -0,0 +1,27 @@ +package bind + +import ( + "net/netip" + "testing" + "time" +) + +func TestActivityRecorder_GetLastActivities(t *testing.T) { + peer := "peer1" + ar := NewActivityRecorder() + ar.UpsertAddress("peer1", netip.MustParseAddrPort("192.168.0.5:51820")) + activities := ar.GetLastActivities() + + p, ok := activities[peer] + if !ok { + t.Fatalf("Expected activity for peer %s, but got none", peer) + } + + if p.IsZero() { + t.Fatalf("Expected activity for peer %s, but got zero", peer) + } + + if p.Before(time.Now().Add(-2 * time.Minute)) { + t.Fatalf("Expected activity for peer %s to be recent, but got %v", peer, p) + } +} diff --git a/client/iface/bind/ice_bind.go b/client/iface/bind/ice_bind.go index 66ec6a00d..bb7a27279 100644 --- a/client/iface/bind/ice_bind.go +++ b/client/iface/bind/ice_bind.go @@ -1,6 +1,7 @@ package bind import ( + "encoding/binary" "fmt" "net" "net/netip" @@ -51,22 +52,24 @@ type ICEBind struct { closedChanMu sync.RWMutex // protect the closeChan recreation from reading from it. closed bool - muUDPMux sync.Mutex - udpMux *UniversalUDPMuxDefault - address wgaddr.Address + muUDPMux sync.Mutex + udpMux *UniversalUDPMuxDefault + address wgaddr.Address + activityRecorder *ActivityRecorder } func NewICEBind(transportNet transport.Net, filterFn FilterFn, address wgaddr.Address) *ICEBind { b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind) ib := &ICEBind{ - StdNetBind: b, - RecvChan: make(chan RecvMessage, 1), - transportNet: transportNet, - filterFn: filterFn, - endpoints: make(map[netip.Addr]net.Conn), - closedChan: make(chan struct{}), - closed: true, - address: address, + StdNetBind: b, + RecvChan: make(chan RecvMessage, 1), + transportNet: transportNet, + filterFn: filterFn, + endpoints: make(map[netip.Addr]net.Conn), + closedChan: make(chan struct{}), + closed: true, + address: address, + activityRecorder: NewActivityRecorder(), } rc := receiverCreator{ @@ -100,6 +103,10 @@ func (s *ICEBind) Close() error { return s.StdNetBind.Close() } +func (s *ICEBind) ActivityRecorder() *ActivityRecorder { + return s.activityRecorder +} + // GetICEMux returns the ICE UDPMux that was created and used by ICEBind func (s *ICEBind) GetICEMux() (*UniversalUDPMuxDefault, error) { s.muUDPMux.Lock() @@ -199,6 +206,11 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r continue } addrPort := msg.Addr.(*net.UDPAddr).AddrPort() + + if isTransportPkg(msg.Buffers, msg.N) { + s.activityRecorder.record(addrPort) + } + ep := &wgConn.StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation wgConn.GetSrcFromControl(msg.OOB[:msg.NN], ep) eps[i] = ep @@ -257,6 +269,13 @@ func (c *ICEBind) receiveRelayed(buffs [][]byte, sizes []int, eps []wgConn.Endpo copy(buffs[0], msg.Buffer) sizes[0] = len(msg.Buffer) eps[0] = wgConn.Endpoint(msg.Endpoint) + + if isTransportPkg(buffs, sizes[0]) { + if ep, ok := eps[0].(*Endpoint); ok { + c.activityRecorder.record(ep.AddrPort) + } + } + return 1, nil } } @@ -272,3 +291,19 @@ func putMessages(msgs *[]ipv6.Message, msgsPool *sync.Pool) { } msgsPool.Put(msgs) } + +func isTransportPkg(buffers [][]byte, n int) bool { + // The first buffer should contain at least 4 bytes for type + if len(buffers[0]) < 4 { + return true + } + + // WireGuard packet type is a little-endian uint32 at start + packetType := binary.LittleEndian.Uint32(buffers[0][:4]) + + // Check if packetType matches known WireGuard message types + if packetType == 4 && n > 32 { + return true + } + return false +} diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go index 4922a54fc..e2ea19144 100644 --- a/client/iface/configurer/kernel_unix.go +++ b/client/iface/configurer/kernel_unix.go @@ -276,3 +276,7 @@ func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) { } return stats, nil } + +func (c *KernelConfigurer) LastActivities() map[string]time.Time { + return nil +} diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 79ce91eea..6ead716f1 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -16,6 +16,7 @@ import ( "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "github.com/netbirdio/netbird/client/iface/bind" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -36,16 +37,18 @@ const ( var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found") type WGUSPConfigurer struct { - device *device.Device - deviceName string + device *device.Device + deviceName string + activityRecorder *bind.ActivityRecorder uapiListener net.Listener } -func NewUSPConfigurer(device *device.Device, deviceName string) *WGUSPConfigurer { +func NewUSPConfigurer(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer { wgCfg := &WGUSPConfigurer{ - device: device, - deviceName: deviceName, + device: device, + deviceName: deviceName, + activityRecorder: activityRecorder, } wgCfg.startUAPI() return wgCfg @@ -87,7 +90,19 @@ func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix, Peers: []wgtypes.PeerConfig{peer}, } - return c.device.IpcSet(toWgUserspaceString(config)) + if ipcErr := c.device.IpcSet(toWgUserspaceString(config)); ipcErr != nil { + return ipcErr + } + + if endpoint != nil { + addr, err := netip.ParseAddr(endpoint.IP.String()) + if err != nil { + return fmt.Errorf("failed to parse endpoint address: %w", err) + } + addrPort := netip.AddrPortFrom(addr, uint16(endpoint.Port)) + c.activityRecorder.UpsertAddress(peerKey, addrPort) + } + return nil } func (c *WGUSPConfigurer) RemovePeer(peerKey string) error { @@ -104,7 +119,10 @@ func (c *WGUSPConfigurer) RemovePeer(peerKey string) error { config := wgtypes.Config{ Peers: []wgtypes.PeerConfig{peer}, } - return c.device.IpcSet(toWgUserspaceString(config)) + ipcErr := c.device.IpcSet(toWgUserspaceString(config)) + + c.activityRecorder.Remove(peerKey) + return ipcErr } func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { @@ -205,6 +223,10 @@ func (c *WGUSPConfigurer) FullStats() (*Stats, error) { return parseStatus(c.deviceName, ipcStr) } +func (c *WGUSPConfigurer) LastActivities() map[string]time.Time { + return c.activityRecorder.GetLastActivities() +} + // startUAPI starts the UAPI listener for managing the WireGuard interface via external tool func (t *WGUSPConfigurer) startUAPI() { var err error diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go index ae9e29bd1..4fe6e466b 100644 --- a/client/iface/device/device_android.go +++ b/client/iface/device/device_android.go @@ -79,7 +79,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() - t.configurer = configurer.NewUSPConfigurer(t.device, t.name) + t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { t.device.Close() diff --git a/client/iface/device/device_darwin.go b/client/iface/device/device_darwin.go index 01bfbf381..81de0e360 100644 --- a/client/iface/device/device_darwin.go +++ b/client/iface/device/device_darwin.go @@ -61,7 +61,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { return nil, fmt.Errorf("error assigning ip: %s", err) } - t.configurer = configurer.NewUSPConfigurer(t.device, t.name) + t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { t.device.Close() diff --git a/client/iface/device/device_ios.go b/client/iface/device/device_ios.go index 56d44d68e..4613762c3 100644 --- a/client/iface/device/device_ios.go +++ b/client/iface/device/device_ios.go @@ -71,7 +71,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() - t.configurer = configurer.NewUSPConfigurer(t.device, t.name) + t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { t.device.Close() diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go index d2f2c87a1..fc3cb0215 100644 --- a/client/iface/device/device_netstack.go +++ b/client/iface/device/device_netstack.go @@ -72,7 +72,7 @@ func (t *TunNetstackDevice) Create() (WGConfigurer, error) { device.NewLogger(wgLogLevel(), "[netbird] "), ) - t.configurer = configurer.NewUSPConfigurer(t.device, t.name) + t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { _ = tunIface.Close() diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go index c45ae9676..e781f6004 100644 --- a/client/iface/device/device_usp_unix.go +++ b/client/iface/device/device_usp_unix.go @@ -64,7 +64,7 @@ func (t *USPDevice) Create() (WGConfigurer, error) { return nil, fmt.Errorf("error assigning ip: %s", err) } - t.configurer = configurer.NewUSPConfigurer(t.device, t.name) + t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { t.device.Close() diff --git a/client/iface/device/device_windows.go b/client/iface/device/device_windows.go index 41e615bc2..0316c4b8d 100644 --- a/client/iface/device/device_windows.go +++ b/client/iface/device/device_windows.go @@ -94,7 +94,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { return nil, fmt.Errorf("error assigning ip: %s", err) } - t.configurer = configurer.NewUSPConfigurer(t.device, t.name) + t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { t.device.Close() diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go index 296eb7dda..d68e6bf90 100644 --- a/client/iface/device/interface.go +++ b/client/iface/device/interface.go @@ -19,4 +19,5 @@ type WGConfigurer interface { Close() GetStats() (map[string]configurer.WGStats, error) FullStats() (*configurer.Stats, error) + LastActivities() map[string]time.Time } diff --git a/client/iface/iface.go b/client/iface/iface.go index 006dfe4e7..1b9055e6c 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -217,6 +217,14 @@ func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) { return w.configurer.GetStats() } +func (w *WGIface) LastActivities() map[string]time.Time { + w.mu.Lock() + defer w.mu.Unlock() + + return w.configurer.LastActivities() + +} + func (w *WGIface) FullStats() (*configurer.Stats, error) { return w.configurer.FullStats() } diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index c630d3052..c76b0a99f 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -12,7 +12,6 @@ import ( "github.com/netbirdio/netbird/client/internal/lazyconn" "github.com/netbirdio/netbird/client/internal/lazyconn/manager" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peerstore" "github.com/netbirdio/netbird/route" ) @@ -26,11 +25,11 @@ import ( // // The implementation is not thread-safe; it is protected by engine.syncMsgMux. type ConnMgr struct { - peerStore *peerstore.Store - statusRecorder *peer.Status - iface lazyconn.WGIface - dispatcher *dispatcher.ConnectionDispatcher - enabledLocally bool + peerStore *peerstore.Store + statusRecorder *peer.Status + iface lazyconn.WGIface + enabledLocally bool + rosenpassEnabled bool lazyConnMgr *manager.Manager @@ -39,12 +38,12 @@ type ConnMgr struct { lazyCtxCancel context.CancelFunc } -func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface, dispatcher *dispatcher.ConnectionDispatcher) *ConnMgr { +func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface) *ConnMgr { e := &ConnMgr{ - peerStore: peerStore, - statusRecorder: statusRecorder, - iface: iface, - dispatcher: dispatcher, + peerStore: peerStore, + statusRecorder: statusRecorder, + iface: iface, + rosenpassEnabled: engineConfig.RosenpassEnabled, } if engineConfig.LazyConnectionEnabled || lazyconn.IsLazyConnEnabledByEnv() { e.enabledLocally = true @@ -64,6 +63,11 @@ func (e *ConnMgr) Start(ctx context.Context) { return } + if e.rosenpassEnabled { + log.Warnf("rosenpass connection manager is enabled, lazy connection manager will not be started") + return + } + e.initLazyManager(ctx) e.statusRecorder.UpdateLazyConnection(true) } @@ -83,7 +87,12 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er return nil } - log.Infof("lazy connection manager is enabled by management feature flag") + if e.rosenpassEnabled { + log.Infof("rosenpass connection manager is enabled, lazy connection manager will not be started") + return nil + } + + log.Warnf("lazy connection manager is enabled by management feature flag") e.initLazyManager(ctx) e.statusRecorder.UpdateLazyConnection(true) return e.addPeersToLazyConnManager() @@ -133,7 +142,7 @@ func (e *ConnMgr) SetExcludeList(ctx context.Context, peerIDs map[string]bool) { excludedPeers = append(excludedPeers, lazyPeerCfg) } - added := e.lazyConnMgr.ExcludePeer(e.lazyCtx, excludedPeers) + added := e.lazyConnMgr.ExcludePeer(excludedPeers) for _, peerID := range added { var peerConn *peer.Conn var exists bool @@ -175,7 +184,7 @@ func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Co PeerConnID: conn.ConnID(), Log: conn.Log, } - excluded, err := e.lazyConnMgr.AddPeer(e.lazyCtx, lazyPeerCfg) + excluded, err := e.lazyConnMgr.AddPeer(lazyPeerCfg) if err != nil { conn.Log.Errorf("failed to add peer to lazyconn manager: %v", err) if err := conn.Open(ctx); err != nil { @@ -201,7 +210,7 @@ func (e *ConnMgr) RemovePeerConn(peerKey string) { if !ok { return } - defer conn.Close() + defer conn.Close(false) if !e.isStartedWithLazyMgr() { return @@ -211,23 +220,28 @@ func (e *ConnMgr) RemovePeerConn(peerKey string) { conn.Log.Infof("removed peer from lazy conn manager") } -func (e *ConnMgr) OnSignalMsg(ctx context.Context, peerKey string) (*peer.Conn, bool) { - conn, ok := e.peerStore.PeerConn(peerKey) - if !ok { - return nil, false - } - +func (e *ConnMgr) ActivatePeer(ctx context.Context, conn *peer.Conn) { if !e.isStartedWithLazyMgr() { - return conn, true + return } - if found := e.lazyConnMgr.ActivatePeer(e.lazyCtx, peerKey); found { + if found := e.lazyConnMgr.ActivatePeer(conn.GetKey()); found { conn.Log.Infof("activated peer from inactive state") if err := conn.Open(ctx); err != nil { conn.Log.Errorf("failed to open connection: %v", err) } } - return conn, true +} + +// DeactivatePeer deactivates a peer connection in the lazy connection manager. +// If locally the lazy connection is disabled, we force the peer connection open. +func (e *ConnMgr) DeactivatePeer(conn *peer.Conn) { + if !e.isStartedWithLazyMgr() { + return + } + + conn.Log.Infof("closing peer connection: remote peer initiated inactive, idle lazy state and sent GOAWAY") + e.lazyConnMgr.DeactivatePeer(conn.ConnID()) } func (e *ConnMgr) Close() { @@ -244,7 +258,7 @@ func (e *ConnMgr) initLazyManager(engineCtx context.Context) { cfg := manager.Config{ InactivityThreshold: inactivityThresholdEnv(), } - e.lazyConnMgr = manager.NewManager(cfg, engineCtx, e.peerStore, e.iface, e.dispatcher) + e.lazyConnMgr = manager.NewManager(cfg, engineCtx, e.peerStore, e.iface) e.lazyCtx, e.lazyCtxCancel = context.WithCancel(engineCtx) @@ -275,7 +289,7 @@ func (e *ConnMgr) addPeersToLazyConnManager() error { lazyPeerCfgs = append(lazyPeerCfgs, lazyPeerCfg) } - return e.lazyConnMgr.AddActivePeers(e.lazyCtx, lazyPeerCfgs) + return e.lazyConnMgr.AddActivePeers(lazyPeerCfgs) } func (e *ConnMgr) closeManager(ctx context.Context) { diff --git a/client/internal/engine.go b/client/internal/engine.go index 74d84569a..e9772b359 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -38,7 +38,6 @@ import ( nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/networkmonitor" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/peerstore" @@ -175,8 +174,7 @@ type Engine struct { sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error) sshServer nbssh.Server - statusRecorder *peer.Status - peerConnDispatcher *dispatcher.ConnectionDispatcher + statusRecorder *peer.Status firewall firewallManager.Manager routeManager routemanager.Manager @@ -458,9 +456,7 @@ func (e *Engine) Start() error { NATExternalIPs: e.parseNATExternalIPMappings(), } - e.peerConnDispatcher = dispatcher.NewConnectionDispatcher() - - e.connMgr = NewConnMgr(e.config, e.statusRecorder, e.peerStore, wgIface, e.peerConnDispatcher) + e.connMgr = NewConnMgr(e.config, e.statusRecorder, e.peerStore, wgIface) e.connMgr.Start(e.ctx) e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg) @@ -1261,7 +1257,7 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error { } if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn); exists { - conn.Close() + conn.Close(false) return fmt.Errorf("peer already exists: %s", peerKey) } @@ -1308,13 +1304,12 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV } serviceDependencies := peer.ServiceDependencies{ - StatusRecorder: e.statusRecorder, - Signaler: e.signaler, - IFaceDiscover: e.mobileDep.IFaceDiscover, - RelayManager: e.relayManager, - SrWatcher: e.srWatcher, - Semaphore: e.connSemaphore, - PeerConnDispatcher: e.peerConnDispatcher, + StatusRecorder: e.statusRecorder, + Signaler: e.signaler, + IFaceDiscover: e.mobileDep.IFaceDiscover, + RelayManager: e.relayManager, + SrWatcher: e.srWatcher, + Semaphore: e.connSemaphore, } peerConn, err := peer.NewConn(config, serviceDependencies) if err != nil { @@ -1337,11 +1332,16 @@ func (e *Engine) receiveSignalEvents() { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() - conn, ok := e.connMgr.OnSignalMsg(e.ctx, msg.Key) + conn, ok := e.peerStore.PeerConn(msg.Key) if !ok { return fmt.Errorf("wrongly addressed message %s", msg.Key) } + msgType := msg.GetBody().GetType() + if msgType != sProto.Body_GO_IDLE { + e.connMgr.ActivatePeer(e.ctx, conn) + } + switch msg.GetBody().Type { case sProto.Body_OFFER: remoteCred, err := signal.UnMarshalCredential(msg) @@ -1398,6 +1398,8 @@ func (e *Engine) receiveSignalEvents() { go conn.OnRemoteCandidate(candidate, e.routeManager.GetClientRoutes()) case sProto.Body_MODE: + case sProto.Body_GO_IDLE: + e.connMgr.DeactivatePeer(conn) } return nil diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index d9c9881da..f4ed8f1c0 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -36,7 +36,6 @@ import ( "github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/peer/dispatcher" "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/routemanager" @@ -97,6 +96,7 @@ type MockWGIface struct { GetInterfaceGUIDStringFunc func() (string, error) GetProxyFunc func() wgproxy.Proxy GetNetFunc func() *netstack.Net + LastActivitiesFunc func() map[string]time.Time } func (m *MockWGIface) FullStats() (*configurer.Stats, error) { @@ -187,6 +187,13 @@ func (m *MockWGIface) GetNet() *netstack.Net { return m.GetNetFunc() } +func (m *MockWGIface) LastActivities() map[string]time.Time { + if m.LastActivitiesFunc != nil { + return m.LastActivitiesFunc() + } + return nil +} + func TestMain(m *testing.M) { _ = util.InitLog("debug", "console") code := m.Run() @@ -404,7 +411,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { engine.udpMux = bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: conn}) engine.ctx = ctx engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{}) - engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface, dispatcher.NewConnectionDispatcher()) + engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface) engine.connMgr.Start(ctx) type testCase struct { @@ -793,7 +800,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { engine.routeManager = mockRouteManager engine.dnsServer = &dns.MockServer{} - engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface, dispatcher.NewConnectionDispatcher()) + engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface) engine.connMgr.Start(ctx) defer func() { @@ -991,7 +998,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { } engine.dnsServer = mockDNSServer - engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface, dispatcher.NewConnectionDispatcher()) + engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface) engine.connMgr.Start(ctx) defer func() { diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index 999472411..38fb3561e 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -38,4 +38,5 @@ type wgIfaceBase interface { GetStats() (map[string]configurer.WGStats, error) GetNet() *netstack.Net FullStats() (*configurer.Stats, error) + LastActivities() map[string]time.Time } diff --git a/client/internal/lazyconn/activity/listener.go b/client/internal/lazyconn/activity/listener.go index 1ef48416a..81b5da17b 100644 --- a/client/internal/lazyconn/activity/listener.go +++ b/client/internal/lazyconn/activity/listener.go @@ -13,7 +13,7 @@ import ( // Listener it is not a thread safe implementation, do not call Close before ReadPackets. It will cause blocking type Listener struct { - wgIface lazyconn.WGIface + wgIface WgInterface peerCfg lazyconn.PeerConfig conn *net.UDPConn endpoint *net.UDPAddr @@ -22,7 +22,7 @@ type Listener struct { isClosed atomic.Bool // use to avoid error log when closing the listener } -func NewListener(wgIface lazyconn.WGIface, cfg lazyconn.PeerConfig) (*Listener, error) { +func NewListener(wgIface WgInterface, cfg lazyconn.PeerConfig) (*Listener, error) { d := &Listener{ wgIface: wgIface, peerCfg: cfg, diff --git a/client/internal/lazyconn/activity/manager.go b/client/internal/lazyconn/activity/manager.go index e18b96465..915fb9cb8 100644 --- a/client/internal/lazyconn/activity/manager.go +++ b/client/internal/lazyconn/activity/manager.go @@ -1,18 +1,27 @@ package activity import ( + "net" + "net/netip" "sync" + "time" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/internal/lazyconn" peerid "github.com/netbirdio/netbird/client/internal/peer/id" ) +type WgInterface interface { + RemovePeer(peerKey string) error + UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error +} + type Manager struct { OnActivityChan chan peerid.ConnID - wgIface lazyconn.WGIface + wgIface WgInterface peers map[peerid.ConnID]*Listener done chan struct{} @@ -20,7 +29,7 @@ type Manager struct { mu sync.Mutex } -func NewManager(wgIface lazyconn.WGIface) *Manager { +func NewManager(wgIface WgInterface) *Manager { m := &Manager{ OnActivityChan: make(chan peerid.ConnID, 1), wgIface: wgIface, diff --git a/client/internal/lazyconn/inactivity/inactivity.go b/client/internal/lazyconn/inactivity/inactivity.go deleted file mode 100644 index 9b7c8511b..000000000 --- a/client/internal/lazyconn/inactivity/inactivity.go +++ /dev/null @@ -1,75 +0,0 @@ -package inactivity - -import ( - "context" - "time" - - peer "github.com/netbirdio/netbird/client/internal/peer/id" -) - -const ( - DefaultInactivityThreshold = 60 * time.Minute // idle after 1 hour inactivity - MinimumInactivityThreshold = 3 * time.Minute -) - -type Monitor struct { - id peer.ConnID - timer *time.Timer - cancel context.CancelFunc - inactivityThreshold time.Duration -} - -func NewInactivityMonitor(peerID peer.ConnID, threshold time.Duration) *Monitor { - i := &Monitor{ - id: peerID, - timer: time.NewTimer(0), - inactivityThreshold: threshold, - } - i.timer.Stop() - return i -} - -func (i *Monitor) Start(ctx context.Context, timeoutChan chan peer.ConnID) { - i.timer.Reset(i.inactivityThreshold) - defer i.timer.Stop() - - ctx, i.cancel = context.WithCancel(ctx) - defer func() { - defer i.cancel() - select { - case <-i.timer.C: - default: - } - }() - - select { - case <-i.timer.C: - select { - case timeoutChan <- i.id: - case <-ctx.Done(): - return - } - case <-ctx.Done(): - return - } -} - -func (i *Monitor) Stop() { - if i.cancel == nil { - return - } - i.cancel() -} - -func (i *Monitor) PauseTimer() { - i.timer.Stop() -} - -func (i *Monitor) ResetTimer() { - i.timer.Reset(i.inactivityThreshold) -} - -func (i *Monitor) ResetMonitor(ctx context.Context, timeoutChan chan peer.ConnID) { - i.Stop() - go i.Start(ctx, timeoutChan) -} diff --git a/client/internal/lazyconn/inactivity/inactivity_test.go b/client/internal/lazyconn/inactivity/inactivity_test.go deleted file mode 100644 index 944512985..000000000 --- a/client/internal/lazyconn/inactivity/inactivity_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package inactivity - -import ( - "context" - "testing" - "time" - - peerid "github.com/netbirdio/netbird/client/internal/peer/id" -) - -type MocPeer struct { -} - -func (m *MocPeer) ConnID() peerid.ConnID { - return peerid.ConnID(m) -} - -func TestInactivityMonitor(t *testing.T) { - tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5) - defer testTimeoutCancel() - - p := &MocPeer{} - im := NewInactivityMonitor(p.ConnID(), time.Second*2) - - timeoutChan := make(chan peerid.ConnID) - - exitChan := make(chan struct{}) - - go func() { - defer close(exitChan) - im.Start(tCtx, timeoutChan) - }() - - select { - case <-timeoutChan: - case <-tCtx.Done(): - t.Fatal("timeout") - } - - select { - case <-exitChan: - case <-tCtx.Done(): - t.Fatal("timeout") - } -} - -func TestReuseInactivityMonitor(t *testing.T) { - p := &MocPeer{} - im := NewInactivityMonitor(p.ConnID(), time.Second*2) - - timeoutChan := make(chan peerid.ConnID) - - for i := 2; i > 0; i-- { - exitChan := make(chan struct{}) - - testTimeoutCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5) - - go func() { - defer close(exitChan) - im.Start(testTimeoutCtx, timeoutChan) - }() - - select { - case <-timeoutChan: - case <-testTimeoutCtx.Done(): - t.Fatal("timeout") - } - - select { - case <-exitChan: - case <-testTimeoutCtx.Done(): - t.Fatal("timeout") - } - testTimeoutCancel() - } -} - -func TestStopInactivityMonitor(t *testing.T) { - tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5) - defer testTimeoutCancel() - - p := &MocPeer{} - im := NewInactivityMonitor(p.ConnID(), DefaultInactivityThreshold) - - timeoutChan := make(chan peerid.ConnID) - - exitChan := make(chan struct{}) - - go func() { - defer close(exitChan) - im.Start(tCtx, timeoutChan) - }() - - go func() { - time.Sleep(3 * time.Second) - im.Stop() - }() - - select { - case <-timeoutChan: - t.Fatal("unexpected timeout") - case <-exitChan: - case <-tCtx.Done(): - t.Fatal("timeout") - } -} - -func TestPauseInactivityMonitor(t *testing.T) { - tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*10) - defer testTimeoutCancel() - - p := &MocPeer{} - trashHold := time.Second * 3 - im := NewInactivityMonitor(p.ConnID(), trashHold) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - timeoutChan := make(chan peerid.ConnID) - - exitChan := make(chan struct{}) - - go func() { - defer close(exitChan) - im.Start(ctx, timeoutChan) - }() - - time.Sleep(1 * time.Second) // grant time to start the monitor - im.PauseTimer() - - // check to do not receive timeout - thresholdCtx, thresholdCancel := context.WithTimeout(context.Background(), trashHold+time.Second) - defer thresholdCancel() - select { - case <-exitChan: - t.Fatal("unexpected exit") - case <-timeoutChan: - t.Fatal("unexpected timeout") - case <-thresholdCtx.Done(): - // test ok - case <-tCtx.Done(): - t.Fatal("test timed out") - } - - // test reset timer - im.ResetTimer() - - select { - case <-tCtx.Done(): - t.Fatal("test timed out") - case <-exitChan: - t.Fatal("unexpected exit") - case <-timeoutChan: - // expected timeout - } -} diff --git a/client/internal/lazyconn/inactivity/manager.go b/client/internal/lazyconn/inactivity/manager.go new file mode 100644 index 000000000..854951729 --- /dev/null +++ b/client/internal/lazyconn/inactivity/manager.go @@ -0,0 +1,152 @@ +package inactivity + +import ( + "context" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/lazyconn" +) + +const ( + checkInterval = 1 * time.Minute + + DefaultInactivityThreshold = 15 * time.Minute + MinimumInactivityThreshold = 1 * time.Minute +) + +type WgInterface interface { + LastActivities() map[string]time.Time +} + +type Manager struct { + inactivePeersChan chan map[string]struct{} + + iface WgInterface + interestedPeers map[string]*lazyconn.PeerConfig + inactivityThreshold time.Duration +} + +func NewManager(iface WgInterface, configuredThreshold *time.Duration) *Manager { + inactivityThreshold, err := validateInactivityThreshold(configuredThreshold) + if err != nil { + inactivityThreshold = DefaultInactivityThreshold + log.Warnf("invalid inactivity threshold configured: %v, using default: %v", err, DefaultInactivityThreshold) + } + + log.Infof("inactivity threshold configured: %v", inactivityThreshold) + return &Manager{ + inactivePeersChan: make(chan map[string]struct{}, 1), + iface: iface, + interestedPeers: make(map[string]*lazyconn.PeerConfig), + inactivityThreshold: inactivityThreshold, + } +} + +func (m *Manager) InactivePeersChan() chan map[string]struct{} { + if m == nil { + // return a nil channel that blocks forever + return nil + } + + return m.inactivePeersChan +} + +func (m *Manager) AddPeer(peerCfg *lazyconn.PeerConfig) { + if m == nil { + return + } + + if _, exists := m.interestedPeers[peerCfg.PublicKey]; exists { + return + } + + peerCfg.Log.Infof("adding peer to inactivity manager") + m.interestedPeers[peerCfg.PublicKey] = peerCfg +} + +func (m *Manager) RemovePeer(peer string) { + if m == nil { + return + } + + pi, ok := m.interestedPeers[peer] + if !ok { + return + } + + pi.Log.Debugf("remove peer from inactivity manager") + delete(m.interestedPeers, peer) +} + +func (m *Manager) Start(ctx context.Context) { + if m == nil { + return + } + + ticker := newTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C(): + idlePeers, err := m.checkStats() + if err != nil { + log.Errorf("error checking stats: %v", err) + return + } + + if len(idlePeers) == 0 { + continue + } + + m.notifyInactivePeers(ctx, idlePeers) + } + } +} + +func (m *Manager) notifyInactivePeers(ctx context.Context, inactivePeers map[string]struct{}) { + select { + case m.inactivePeersChan <- inactivePeers: + case <-ctx.Done(): + return + default: + return + } +} + +func (m *Manager) checkStats() (map[string]struct{}, error) { + lastActivities := m.iface.LastActivities() + + idlePeers := make(map[string]struct{}) + + for peerID, peerCfg := range m.interestedPeers { + lastActive, ok := lastActivities[peerID] + if !ok { + // when peer is in connecting state + peerCfg.Log.Warnf("peer not found in wg stats") + continue + } + + if time.Since(lastActive) > m.inactivityThreshold { + peerCfg.Log.Infof("peer is inactive since: %v", lastActive) + idlePeers[peerID] = struct{}{} + } + } + + return idlePeers, nil +} + +func validateInactivityThreshold(configuredThreshold *time.Duration) (time.Duration, error) { + if configuredThreshold == nil { + return DefaultInactivityThreshold, nil + } + if *configuredThreshold < MinimumInactivityThreshold { + return 0, fmt.Errorf("configured inactivity threshold %v is too low, using %v", *configuredThreshold, MinimumInactivityThreshold) + } + return *configuredThreshold, nil +} diff --git a/client/internal/lazyconn/inactivity/manager_test.go b/client/internal/lazyconn/inactivity/manager_test.go new file mode 100644 index 000000000..d012b41a2 --- /dev/null +++ b/client/internal/lazyconn/inactivity/manager_test.go @@ -0,0 +1,113 @@ +package inactivity + +import ( + "context" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/client/internal/lazyconn" +) + +type mockWgInterface struct { + lastActivities map[string]time.Time +} + +func (m *mockWgInterface) LastActivities() map[string]time.Time { + return m.lastActivities +} + +func TestPeerTriggersInactivity(t *testing.T) { + peerID := "peer1" + + wgMock := &mockWgInterface{ + lastActivities: map[string]time.Time{ + peerID: time.Now().Add(-20 * time.Minute), + }, + } + + fakeTick := make(chan time.Time, 1) + newTicker = func(d time.Duration) Ticker { + return &fakeTickerMock{CChan: fakeTick} + } + + peerLog := log.WithField("peer", peerID) + peerCfg := &lazyconn.PeerConfig{ + PublicKey: peerID, + Log: peerLog, + } + + manager := NewManager(wgMock, nil) + manager.AddPeer(peerCfg) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start the manager in a goroutine + go manager.Start(ctx) + + // Send a tick to simulate time passage + fakeTick <- time.Now() + + // Check if peer appears on inactivePeersChan + select { + case inactivePeers := <-manager.inactivePeersChan: + assert.Contains(t, inactivePeers, peerID, "expected peer to be marked inactive") + case <-time.After(1 * time.Second): + t.Fatal("expected inactivity event, but none received") + } +} + +func TestPeerTriggersActivity(t *testing.T) { + peerID := "peer1" + + wgMock := &mockWgInterface{ + lastActivities: map[string]time.Time{ + peerID: time.Now().Add(-5 * time.Minute), + }, + } + + fakeTick := make(chan time.Time, 1) + newTicker = func(d time.Duration) Ticker { + return &fakeTickerMock{CChan: fakeTick} + } + + peerLog := log.WithField("peer", peerID) + peerCfg := &lazyconn.PeerConfig{ + PublicKey: peerID, + Log: peerLog, + } + + manager := NewManager(wgMock, nil) + manager.AddPeer(peerCfg) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start the manager in a goroutine + go manager.Start(ctx) + + // Send a tick to simulate time passage + fakeTick <- time.Now() + + // Check if peer appears on inactivePeersChan + select { + case <-manager.inactivePeersChan: + t.Fatal("expected inactive peer to be marked inactive") + case <-time.After(1 * time.Second): + // No inactivity event should be received + } +} + +// fakeTickerMock implements Ticker interface for testing +type fakeTickerMock struct { + CChan chan time.Time +} + +func (f *fakeTickerMock) C() <-chan time.Time { + return f.CChan +} + +func (f *fakeTickerMock) Stop() {} diff --git a/client/internal/lazyconn/inactivity/ticker.go b/client/internal/lazyconn/inactivity/ticker.go new file mode 100644 index 000000000..12b64bd5f --- /dev/null +++ b/client/internal/lazyconn/inactivity/ticker.go @@ -0,0 +1,24 @@ +package inactivity + +import "time" + +var newTicker = func(d time.Duration) Ticker { + return &realTicker{t: time.NewTicker(d)} +} + +type Ticker interface { + C() <-chan time.Time + Stop() +} + +type realTicker struct { + t *time.Ticker +} + +func (r *realTicker) C() <-chan time.Time { + return r.t.C +} + +func (r *realTicker) Stop() { + r.t.Stop() +} diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go index 74ede50a7..b45b39221 100644 --- a/client/internal/lazyconn/manager/manager.go +++ b/client/internal/lazyconn/manager/manager.go @@ -52,51 +52,39 @@ type Manager struct { excludes map[string]lazyconn.PeerConfig managedPeersMu sync.Mutex - activityManager *activity.Manager - inactivityMonitors map[peerid.ConnID]*inactivity.Monitor + activityManager *activity.Manager + inactivityManager *inactivity.Manager // Route HA group management + // If any peer in the same HA group is active, all peers in that group should prevent going idle peerToHAGroups map[string][]route.HAUniqueID // peer ID -> HA groups they belong to haGroupToPeers map[route.HAUniqueID][]string // HA group -> peer IDs in the group routesMu sync.RWMutex - - onInactive chan peerid.ConnID } // NewManager creates a new lazy connection manager // engineCtx is the context for creating peer Connection -func NewManager(config Config, engineCtx context.Context, peerStore *peerstore.Store, wgIface lazyconn.WGIface, connStateDispatcher *dispatcher.ConnectionDispatcher) *Manager { +func NewManager(config Config, engineCtx context.Context, peerStore *peerstore.Store, wgIface lazyconn.WGIface) *Manager { log.Infof("setup lazy connection service") + m := &Manager{ engineCtx: engineCtx, peerStore: peerStore, - connStateDispatcher: connStateDispatcher, inactivityThreshold: inactivity.DefaultInactivityThreshold, managedPeers: make(map[string]*lazyconn.PeerConfig), managedPeersByConnID: make(map[peerid.ConnID]*managedPeer), excludes: make(map[string]lazyconn.PeerConfig), activityManager: activity.NewManager(wgIface), - inactivityMonitors: make(map[peerid.ConnID]*inactivity.Monitor), peerToHAGroups: make(map[string][]route.HAUniqueID), haGroupToPeers: make(map[route.HAUniqueID][]string), - onInactive: make(chan peerid.ConnID), } - if config.InactivityThreshold != nil { - if *config.InactivityThreshold >= inactivity.MinimumInactivityThreshold { - m.inactivityThreshold = *config.InactivityThreshold - } else { - log.Warnf("inactivity threshold is too low, using %v", m.inactivityThreshold) - } + if wgIface.IsUserspaceBind() { + m.inactivityManager = inactivity.NewManager(wgIface, config.InactivityThreshold) + } else { + log.Warnf("inactivity manager not supported for kernel mode, wait for remote peer to close the connection") } - m.connStateListener = &dispatcher.ConnectionListener{ - OnConnected: m.onPeerConnected, - OnDisconnected: m.onPeerDisconnected, - } - - connStateDispatcher.AddListener(m.connStateListener) - return m } @@ -131,24 +119,28 @@ func (m *Manager) UpdateRouteHAMap(haMap route.HAMap) { } } - log.Debugf("updated route HA mappings: %d HA groups, %d peers with routes", - len(m.haGroupToPeers), len(m.peerToHAGroups)) + log.Debugf("updated route HA mappings: %d HA groups, %d peers with routes", len(m.haGroupToPeers), len(m.peerToHAGroups)) } // Start starts the manager and listens for peer activity and inactivity events func (m *Manager) Start(ctx context.Context) { defer m.close() + if m.inactivityManager != nil { + go m.inactivityManager.Start(ctx) + } + for { select { case <-ctx.Done(): return case peerConnID := <-m.activityManager.OnActivityChan: - m.onPeerActivity(ctx, peerConnID) - case peerConnID := <-m.onInactive: - m.onPeerInactivityTimedOut(ctx, peerConnID) + m.onPeerActivity(peerConnID) + case peerIDs := <-m.inactivityManager.InactivePeersChan(): + m.onPeerInactivityTimedOut(peerIDs) } } + } // ExcludePeer marks peers for a permanent connection @@ -156,7 +148,7 @@ func (m *Manager) Start(ctx context.Context) { // Adds them back to the managed list and start the inactivity listener if they are removed from the exclude list. In // this case, we suppose that the connection status is connected or connecting. // If the peer is not exists yet in the managed list then the responsibility is the upper layer to call the AddPeer function -func (m *Manager) ExcludePeer(ctx context.Context, peerConfigs []lazyconn.PeerConfig) []string { +func (m *Manager) ExcludePeer(peerConfigs []lazyconn.PeerConfig) []string { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -187,7 +179,7 @@ func (m *Manager) ExcludePeer(ctx context.Context, peerConfigs []lazyconn.PeerCo peerCfg.Log.Infof("peer removed from lazy connection exclude list") - if err := m.addActivePeer(ctx, peerCfg); err != nil { + if err := m.addActivePeer(&peerCfg); err != nil { log.Errorf("failed to add peer to lazy connection manager: %s", err) continue } @@ -197,7 +189,7 @@ func (m *Manager) ExcludePeer(ctx context.Context, peerConfigs []lazyconn.PeerCo return added } -func (m *Manager) AddPeer(ctx context.Context, peerCfg lazyconn.PeerConfig) (bool, error) { +func (m *Manager) AddPeer(peerCfg lazyconn.PeerConfig) (bool, error) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -217,9 +209,6 @@ func (m *Manager) AddPeer(ctx context.Context, peerCfg lazyconn.PeerConfig) (boo return false, err } - im := inactivity.NewInactivityMonitor(peerCfg.PeerConnID, m.inactivityThreshold) - m.inactivityMonitors[peerCfg.PeerConnID] = im - m.managedPeers[peerCfg.PublicKey] = &peerCfg m.managedPeersByConnID[peerCfg.PeerConnID] = &managedPeer{ peerCfg: &peerCfg, @@ -229,7 +218,7 @@ func (m *Manager) AddPeer(ctx context.Context, peerCfg lazyconn.PeerConfig) (boo // Check if this peer should be activated because its HA group peers are active if group, ok := m.shouldActivateNewPeer(peerCfg.PublicKey); ok { peerCfg.Log.Debugf("peer belongs to active HA group %s, will activate immediately", group) - m.activateNewPeerInActiveGroup(ctx, peerCfg) + m.activateNewPeerInActiveGroup(peerCfg) } return false, nil @@ -237,7 +226,7 @@ func (m *Manager) AddPeer(ctx context.Context, peerCfg lazyconn.PeerConfig) (boo // AddActivePeers adds a list of peers to the lazy connection manager // suppose these peers was in connected or in connecting states -func (m *Manager) AddActivePeers(ctx context.Context, peerCfg []lazyconn.PeerConfig) error { +func (m *Manager) AddActivePeers(peerCfg []lazyconn.PeerConfig) error { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -247,7 +236,7 @@ func (m *Manager) AddActivePeers(ctx context.Context, peerCfg []lazyconn.PeerCon continue } - if err := m.addActivePeer(ctx, cfg); err != nil { + if err := m.addActivePeer(&cfg); err != nil { cfg.Log.Errorf("failed to add peer to lazy connection manager: %v", err) return err } @@ -264,7 +253,7 @@ func (m *Manager) RemovePeer(peerID string) { // ActivatePeer activates a peer connection when a signal message is received // Also activates all peers in the same HA groups as this peer -func (m *Manager) ActivatePeer(ctx context.Context, peerID string) (found bool) { +func (m *Manager) ActivatePeer(peerID string) (found bool) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() cfg, mp := m.getPeerForActivation(peerID) @@ -272,15 +261,42 @@ func (m *Manager) ActivatePeer(ctx context.Context, peerID string) (found bool) return false } - if !m.activateSinglePeer(ctx, cfg, mp) { + if !m.activateSinglePeer(cfg, mp) { return false } - m.activateHAGroupPeers(ctx, peerID) + m.activateHAGroupPeers(cfg) return true } +func (m *Manager) DeactivatePeer(peerID peerid.ConnID) { + m.managedPeersMu.Lock() + defer m.managedPeersMu.Unlock() + + mp, ok := m.managedPeersByConnID[peerID] + if !ok { + return + } + + if mp.expectedWatcher != watcherInactivity { + return + } + + m.peerStore.PeerConnClose(mp.peerCfg.PublicKey) + + mp.peerCfg.Log.Infof("start activity monitor") + + mp.expectedWatcher = watcherActivity + + m.inactivityManager.RemovePeer(mp.peerCfg.PublicKey) + + if err := m.activityManager.MonitorPeerActivity(*mp.peerCfg); err != nil { + mp.peerCfg.Log.Errorf("failed to create activity monitor: %v", err) + return + } +} + // getPeerForActivation checks if a peer can be activated and returns the necessary structs // Returns nil values if the peer should be skipped func (m *Manager) getPeerForActivation(peerID string) (*lazyconn.PeerConfig, *managedPeer) { @@ -302,41 +318,36 @@ func (m *Manager) getPeerForActivation(peerID string) (*lazyconn.PeerConfig, *ma return cfg, mp } -// activateSinglePeer activates a single peer (internal method) -func (m *Manager) activateSinglePeer(ctx context.Context, cfg *lazyconn.PeerConfig, mp *managedPeer) bool { - mp.expectedWatcher = watcherInactivity - - m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID) - - im, ok := m.inactivityMonitors[cfg.PeerConnID] - if !ok { - cfg.Log.Errorf("inactivity monitor not found for peer") +// activateSinglePeer activates a single peer +// return true if the peer was activated, false if it was already active +func (m *Manager) activateSinglePeer(cfg *lazyconn.PeerConfig, mp *managedPeer) bool { + if mp.expectedWatcher == watcherInactivity { return false } - cfg.Log.Infof("starting inactivity monitor") - go im.Start(ctx, m.onInactive) - + mp.expectedWatcher = watcherInactivity + m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID) + m.inactivityManager.AddPeer(cfg) return true } // activateHAGroupPeers activates all peers in HA groups that the given peer belongs to -func (m *Manager) activateHAGroupPeers(ctx context.Context, triggerPeerID string) { +func (m *Manager) activateHAGroupPeers(triggeredPeerCfg *lazyconn.PeerConfig) { var peersToActivate []string m.routesMu.RLock() - haGroups := m.peerToHAGroups[triggerPeerID] + haGroups := m.peerToHAGroups[triggeredPeerCfg.PublicKey] if len(haGroups) == 0 { m.routesMu.RUnlock() - log.Debugf("peer %s is not part of any HA groups", triggerPeerID) + triggeredPeerCfg.Log.Debugf("peer is not part of any HA groups") return } for _, haGroup := range haGroups { peers := m.haGroupToPeers[haGroup] for _, peerID := range peers { - if peerID != triggerPeerID { + if peerID != triggeredPeerCfg.PublicKey { peersToActivate = append(peersToActivate, peerID) } } @@ -350,16 +361,16 @@ func (m *Manager) activateHAGroupPeers(ctx context.Context, triggerPeerID string continue } - if m.activateSinglePeer(ctx, cfg, mp) { + if m.activateSinglePeer(cfg, mp) { activatedCount++ - cfg.Log.Infof("activated peer as part of HA group (triggered by %s)", triggerPeerID) + cfg.Log.Infof("activated peer as part of HA group (triggered by %s)", triggeredPeerCfg.PublicKey) m.peerStore.PeerConnOpen(m.engineCtx, cfg.PublicKey) } } if activatedCount > 0 { log.Infof("activated %d additional peers in HA groups for peer %s (groups: %v)", - activatedCount, triggerPeerID, haGroups) + activatedCount, triggeredPeerCfg.PublicKey, haGroups) } } @@ -394,13 +405,13 @@ func (m *Manager) shouldActivateNewPeer(peerID string) (route.HAUniqueID, bool) } // activateNewPeerInActiveGroup activates a newly added peer that should be active due to HA group -func (m *Manager) activateNewPeerInActiveGroup(ctx context.Context, peerCfg lazyconn.PeerConfig) { +func (m *Manager) activateNewPeerInActiveGroup(peerCfg lazyconn.PeerConfig) { mp, ok := m.managedPeersByConnID[peerCfg.PeerConnID] if !ok { return } - if !m.activateSinglePeer(ctx, &peerCfg, mp) { + if !m.activateSinglePeer(&peerCfg, mp) { return } @@ -408,23 +419,19 @@ func (m *Manager) activateNewPeerInActiveGroup(ctx context.Context, peerCfg lazy m.peerStore.PeerConnOpen(m.engineCtx, peerCfg.PublicKey) } -func (m *Manager) addActivePeer(ctx context.Context, peerCfg lazyconn.PeerConfig) error { +func (m *Manager) addActivePeer(peerCfg *lazyconn.PeerConfig) error { if _, ok := m.managedPeers[peerCfg.PublicKey]; ok { peerCfg.Log.Warnf("peer already managed") return nil } - im := inactivity.NewInactivityMonitor(peerCfg.PeerConnID, m.inactivityThreshold) - m.inactivityMonitors[peerCfg.PeerConnID] = im - - m.managedPeers[peerCfg.PublicKey] = &peerCfg + m.managedPeers[peerCfg.PublicKey] = peerCfg m.managedPeersByConnID[peerCfg.PeerConnID] = &managedPeer{ - peerCfg: &peerCfg, + peerCfg: peerCfg, expectedWatcher: watcherInactivity, } - peerCfg.Log.Infof("starting inactivity monitor on peer that has been removed from exclude list") - go im.Start(ctx, m.onInactive) + m.inactivityManager.AddPeer(peerCfg) return nil } @@ -436,12 +443,7 @@ func (m *Manager) removePeer(peerID string) { cfg.Log.Infof("removing lazy peer") - if im, ok := m.inactivityMonitors[cfg.PeerConnID]; ok { - im.Stop() - delete(m.inactivityMonitors, cfg.PeerConnID) - cfg.Log.Debugf("inactivity monitor stopped") - } - + m.inactivityManager.RemovePeer(cfg.PublicKey) m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID) delete(m.managedPeers, peerID) delete(m.managedPeersByConnID, cfg.PeerConnID) @@ -453,10 +455,7 @@ func (m *Manager) close() { m.connStateDispatcher.RemoveListener(m.connStateListener) m.activityManager.Close() - for _, iw := range m.inactivityMonitors { - iw.Stop() - } - m.inactivityMonitors = make(map[peerid.ConnID]*inactivity.Monitor) + m.managedPeers = make(map[string]*lazyconn.PeerConfig) m.managedPeersByConnID = make(map[peerid.ConnID]*managedPeer) @@ -470,7 +469,7 @@ func (m *Manager) close() { } // shouldDeferIdleForHA checks if peer should stay connected due to HA group requirements -func (m *Manager) shouldDeferIdleForHA(peerID string) bool { +func (m *Manager) shouldDeferIdleForHA(inactivePeers map[string]struct{}, peerID string) bool { m.routesMu.RLock() defer m.routesMu.RUnlock() @@ -480,38 +479,45 @@ func (m *Manager) shouldDeferIdleForHA(peerID string) bool { } for _, haGroup := range haGroups { - groupPeers := m.haGroupToPeers[haGroup] - - for _, groupPeerID := range groupPeers { - if groupPeerID == peerID { - continue - } - - cfg, ok := m.managedPeers[groupPeerID] - if !ok { - continue - } - - groupMp, ok := m.managedPeersByConnID[cfg.PeerConnID] - if !ok { - continue - } - - if groupMp.expectedWatcher != watcherInactivity { - continue - } - - // Other member is still connected, defer idle - if peer, ok := m.peerStore.PeerConn(groupPeerID); ok && peer.IsConnected() { - return true - } + if active := m.checkHaGroupActivity(haGroup, peerID, inactivePeers); active { + return true } } return false } -func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) { +func (m *Manager) checkHaGroupActivity(haGroup route.HAUniqueID, peerID string, inactivePeers map[string]struct{}) bool { + groupPeers := m.haGroupToPeers[haGroup] + for _, groupPeerID := range groupPeers { + + if groupPeerID == peerID { + continue + } + + cfg, ok := m.managedPeers[groupPeerID] + if !ok { + continue + } + + groupMp, ok := m.managedPeersByConnID[cfg.PeerConnID] + if !ok { + continue + } + + if groupMp.expectedWatcher != watcherInactivity { + continue + } + + // If any peer in the group is active, do defer idle + if _, isInactive := inactivePeers[groupPeerID]; !isInactive { + return true + } + } + return false +} + +func (m *Manager) onPeerActivity(peerConnID peerid.ConnID) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() @@ -528,100 +534,56 @@ func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) mp.peerCfg.Log.Infof("detected peer activity") - if !m.activateSinglePeer(ctx, mp.peerCfg, mp) { + if !m.activateSinglePeer(mp.peerCfg, mp) { return } - m.activateHAGroupPeers(ctx, mp.peerCfg.PublicKey) + m.activateHAGroupPeers(mp.peerCfg) m.peerStore.PeerConnOpen(m.engineCtx, mp.peerCfg.PublicKey) } -func (m *Manager) onPeerInactivityTimedOut(ctx context.Context, peerConnID peerid.ConnID) { +func (m *Manager) onPeerInactivityTimedOut(peerIDs map[string]struct{}) { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() - mp, ok := m.managedPeersByConnID[peerConnID] - if !ok { - log.Errorf("peer not found by id: %v", peerConnID) - return - } - - if mp.expectedWatcher != watcherInactivity { - mp.peerCfg.Log.Warnf("ignore inactivity event") - return - } - - if m.shouldDeferIdleForHA(mp.peerCfg.PublicKey) { - iw, ok := m.inactivityMonitors[peerConnID] - if ok { - mp.peerCfg.Log.Debugf("resetting inactivity timer due to HA group requirements") - iw.ResetMonitor(ctx, m.onInactive) - } else { - mp.peerCfg.Log.Errorf("inactivity monitor not found for HA defer reset") + for peerID := range peerIDs { + peerCfg, ok := m.managedPeers[peerID] + if !ok { + log.Errorf("peer not found by peerId: %v", peerID) + continue } - return - } - mp.peerCfg.Log.Infof("connection timed out") + mp, ok := m.managedPeersByConnID[peerCfg.PeerConnID] + if !ok { + log.Errorf("peer not found by conn id: %v", peerCfg.PeerConnID) + continue + } - // this is blocking operation, potentially can be optimized - m.peerStore.PeerConnClose(mp.peerCfg.PublicKey) + if mp.expectedWatcher != watcherInactivity { + mp.peerCfg.Log.Warnf("ignore inactivity event") + continue + } - mp.peerCfg.Log.Infof("start activity monitor") + if m.shouldDeferIdleForHA(peerIDs, mp.peerCfg.PublicKey) { + mp.peerCfg.Log.Infof("defer inactivity due to active HA group peers") + continue + } - mp.expectedWatcher = watcherActivity + mp.peerCfg.Log.Infof("connection timed out") - // just in case free up - m.inactivityMonitors[peerConnID].PauseTimer() + // this is blocking operation, potentially can be optimized + m.peerStore.PeerConnIdle(mp.peerCfg.PublicKey) - if err := m.activityManager.MonitorPeerActivity(*mp.peerCfg); err != nil { - mp.peerCfg.Log.Errorf("failed to create activity monitor: %v", err) - return + mp.peerCfg.Log.Infof("start activity monitor") + + mp.expectedWatcher = watcherActivity + + m.inactivityManager.RemovePeer(mp.peerCfg.PublicKey) + + if err := m.activityManager.MonitorPeerActivity(*mp.peerCfg); err != nil { + mp.peerCfg.Log.Errorf("failed to create activity monitor: %v", err) + continue + } } } - -func (m *Manager) onPeerConnected(peerConnID peerid.ConnID) { - m.managedPeersMu.Lock() - defer m.managedPeersMu.Unlock() - - mp, ok := m.managedPeersByConnID[peerConnID] - if !ok { - return - } - - if mp.expectedWatcher != watcherInactivity { - return - } - - iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID] - if !ok { - mp.peerCfg.Log.Warnf("inactivity monitor not found for peer") - return - } - - mp.peerCfg.Log.Infof("peer connected, pausing inactivity monitor while connection is not disconnected") - iw.PauseTimer() -} - -func (m *Manager) onPeerDisconnected(peerConnID peerid.ConnID) { - m.managedPeersMu.Lock() - defer m.managedPeersMu.Unlock() - - mp, ok := m.managedPeersByConnID[peerConnID] - if !ok { - return - } - - if mp.expectedWatcher != watcherInactivity { - return - } - - iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID] - if !ok { - return - } - - mp.peerCfg.Log.Infof("reset inactivity monitor timer") - iw.ResetTimer() -} diff --git a/client/internal/lazyconn/wgiface.go b/client/internal/lazyconn/wgiface.go index 090a9319c..d55ff9670 100644 --- a/client/internal/lazyconn/wgiface.go +++ b/client/internal/lazyconn/wgiface.go @@ -11,4 +11,6 @@ import ( type WGIface interface { RemovePeer(peerKey string) error UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error + IsUserspaceBind() bool + LastActivities() map[string]time.Time } diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index c3f44cc7f..1f0ec164e 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -117,10 +117,9 @@ type Conn struct { wgProxyRelay wgproxy.Proxy handshaker *Handshaker - guard *guard.Guard - semaphore *semaphoregroup.SemaphoreGroup - peerConnDispatcher *dispatcher.ConnectionDispatcher - wg sync.WaitGroup + guard *guard.Guard + semaphore *semaphoregroup.SemaphoreGroup + wg sync.WaitGroup // debug purpose dumpState *stateDump @@ -136,18 +135,17 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) { connLog := log.WithField("peer", config.Key) var conn = &Conn{ - Log: connLog, - config: config, - statusRecorder: services.StatusRecorder, - signaler: services.Signaler, - iFaceDiscover: services.IFaceDiscover, - relayManager: services.RelayManager, - srWatcher: services.SrWatcher, - semaphore: services.Semaphore, - peerConnDispatcher: services.PeerConnDispatcher, - statusRelay: worker.NewAtomicStatus(), - statusICE: worker.NewAtomicStatus(), - dumpState: newStateDump(config.Key, connLog, services.StatusRecorder), + Log: connLog, + config: config, + statusRecorder: services.StatusRecorder, + signaler: services.Signaler, + iFaceDiscover: services.IFaceDiscover, + relayManager: services.RelayManager, + srWatcher: services.SrWatcher, + semaphore: services.Semaphore, + statusRelay: worker.NewAtomicStatus(), + statusICE: worker.NewAtomicStatus(), + dumpState: newStateDump(config.Key, connLog, services.StatusRecorder), } return conn, nil @@ -226,7 +224,7 @@ func (conn *Conn) Open(engineCtx context.Context) error { } // Close closes this peer Conn issuing a close event to the Conn closeCh -func (conn *Conn) Close() { +func (conn *Conn) Close(signalToRemote bool) { conn.mu.Lock() defer conn.wgWatcherWg.Wait() defer conn.mu.Unlock() @@ -236,6 +234,12 @@ func (conn *Conn) Close() { return } + if signalToRemote { + if err := conn.signaler.SignalIdle(conn.config.Key); err != nil { + conn.Log.Errorf("failed to signal idle state to peer: %v", err) + } + } + conn.Log.Infof("close peer connection") conn.ctxCancel() @@ -404,15 +408,10 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn } wgConfigWorkaround() - oldState := conn.currentConnPriority conn.currentConnPriority = priority conn.statusICE.SetConnected() conn.updateIceState(iceConnInfo) conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr) - - if oldState == conntype.None { - conn.peerConnDispatcher.NotifyConnected(conn.ConnID()) - } } func (conn *Conn) onICEStateDisconnected() { @@ -450,7 +449,6 @@ func (conn *Conn) onICEStateDisconnected() { } else { conn.Log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", conntype.None.String()) conn.currentConnPriority = conntype.None - conn.peerConnDispatcher.NotifyDisconnected(conn.ConnID()) } changed := conn.statusICE.Get() != worker.StatusDisconnected @@ -530,7 +528,6 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey) conn.Log.Infof("start to communicate with peer via relay") conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr) - conn.peerConnDispatcher.NotifyConnected(conn.ConnID()) } func (conn *Conn) onRelayDisconnected() { @@ -545,11 +542,7 @@ func (conn *Conn) onRelayDisconnected() { if conn.currentConnPriority == conntype.Relay { conn.Log.Debugf("clean up WireGuard config") - if err := conn.removeWgPeer(); err != nil { - conn.Log.Errorf("failed to remove wg endpoint: %v", err) - } conn.currentConnPriority = conntype.None - conn.peerConnDispatcher.NotifyDisconnected(conn.ConnID()) } if conn.wgProxyRelay != nil { diff --git a/client/internal/peer/signaler.go b/client/internal/peer/signaler.go index 713123e5d..9022e0299 100644 --- a/client/internal/peer/signaler.go +++ b/client/internal/peer/signaler.go @@ -68,3 +68,13 @@ func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string, return nil } + +func (s *Signaler) SignalIdle(remoteKey string) error { + return s.signal.Send(&sProto.Message{ + Key: s.wgPrivateKey.PublicKey().String(), + RemoteKey: remoteKey, + Body: &sProto.Body{ + Type: sProto.Body_GO_IDLE, + }, + }) +} diff --git a/client/internal/peerstore/store.go b/client/internal/peerstore/store.go index 81ac7a5b6..099fe4528 100644 --- a/client/internal/peerstore/store.go +++ b/client/internal/peerstore/store.go @@ -95,6 +95,17 @@ func (s *Store) PeerConnOpen(ctx context.Context, pubKey string) { } +func (s *Store) PeerConnIdle(pubKey string) { + s.peerConnsMu.RLock() + defer s.peerConnsMu.RUnlock() + + p, ok := s.peerConns[pubKey] + if !ok { + return + } + p.Close(true) +} + func (s *Store) PeerConnClose(pubKey string) { s.peerConnsMu.RLock() defer s.peerConnsMu.RUnlock() @@ -103,7 +114,7 @@ func (s *Store) PeerConnClose(pubKey string) { if !ok { return } - p.Close() + p.Close(false) } func (s *Store) PeersPubKey() []string { diff --git a/monotime/time.go b/monotime/time.go new file mode 100644 index 000000000..6032fb60b --- /dev/null +++ b/monotime/time.go @@ -0,0 +1,29 @@ +package monotime + +import ( + "time" +) + +var ( + baseWallTime time.Time + baseWallNano int64 +) + +func init() { + baseWallTime = time.Now() + baseWallNano = baseWallTime.UnixNano() +} + +// Now returns the current time as Unix nanoseconds (int64). +// It uses monotonic time measurement from the base time to ensure +// the returned value increases monotonically and is not affected +// by system clock adjustments. +// +// Performance optimization: By capturing the base wall time once at startup +// and using time.Since() for elapsed calculation, this avoids repeated +// time.Now() calls and leverages Go's internal monotonic clock for +// efficient duration measurement. +func Now() int64 { + elapsed := time.Since(baseWallTime) + return baseWallNano + int64(elapsed) +} diff --git a/monotime/time_test.go b/monotime/time_test.go new file mode 100644 index 000000000..ac837b226 --- /dev/null +++ b/monotime/time_test.go @@ -0,0 +1,20 @@ +package monotime + +import ( + "testing" + "time" +) + +func BenchmarkMonotimeNow(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = Now() + } +} + +func BenchmarkTimeNow(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = time.Now() + } +} diff --git a/signal/proto/signalexchange.pb.go b/signal/proto/signalexchange.pb.go index 30f704c6f..3d45dea69 100644 --- a/signal/proto/signalexchange.pb.go +++ b/signal/proto/signalexchange.pb.go @@ -29,6 +29,7 @@ const ( Body_ANSWER Body_Type = 1 Body_CANDIDATE Body_Type = 2 Body_MODE Body_Type = 4 + Body_GO_IDLE Body_Type = 5 ) // Enum value maps for Body_Type. @@ -38,12 +39,14 @@ var ( 1: "ANSWER", 2: "CANDIDATE", 4: "MODE", + 5: "GO_IDLE", } Body_Type_value = map[string]int32{ "OFFER": 0, "ANSWER": 1, "CANDIDATE": 2, "MODE": 4, + "GO_IDLE": 5, } ) @@ -225,7 +228,7 @@ type Body struct { FeaturesSupported []uint32 `protobuf:"varint,6,rep,packed,name=featuresSupported,proto3" json:"featuresSupported,omitempty"` // RosenpassConfig is a Rosenpass config of the remote peer our peer tries to connect to RosenpassConfig *RosenpassConfig `protobuf:"bytes,7,opt,name=rosenpassConfig,proto3" json:"rosenpassConfig,omitempty"` - // relayServerAddress is an IP:port of the relay server + // relayServerAddress is url of the relay server RelayServerAddress string `protobuf:"bytes,8,opt,name=relayServerAddress,proto3" json:"relayServerAddress,omitempty"` } @@ -440,7 +443,7 @@ var file_signalexchange_proto_rawDesc = []byte{ 0x52, 0x09, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x42, 0x6f, 0x64, 0x79, 0x52, - 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0xa6, 0x03, 0x0a, 0x04, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x2d, + 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0xb3, 0x03, 0x0a, 0x04, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x42, 0x6f, 0x64, 0x79, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, @@ -463,33 +466,34 @@ var file_signalexchange_proto_rawDesc = []byte{ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x43, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x46, 0x46, 0x45, 0x52, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4e, 0x53, 0x57, 0x45, 0x52, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x41, 0x4e, 0x44, 0x49, 0x44, 0x41, - 0x54, 0x45, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4d, 0x4f, 0x44, 0x45, 0x10, 0x04, 0x22, 0x2e, - 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0x6d, - 0x0a, 0x0f, 0x52, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x72, - 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, - 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, - 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x32, 0xb9, 0x01, - 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x12, 0x4c, 0x0a, 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, - 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x59, - 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, - 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x54, 0x45, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4d, 0x4f, 0x44, 0x45, 0x10, 0x04, 0x12, 0x0b, + 0x0a, 0x07, 0x47, 0x4f, 0x5f, 0x49, 0x44, 0x4c, 0x45, 0x10, 0x05, 0x22, 0x2e, 0x0a, 0x04, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, + 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0x6d, 0x0a, 0x0f, 0x52, + 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, + 0x0a, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x32, 0xb9, 0x01, 0x0a, 0x0e, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4c, 0x0a, + 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, + 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, + 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x0d, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x2e, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, + 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/signal/proto/signalexchange.proto b/signal/proto/signalexchange.proto index 4431edd7c..b04d6ef28 100644 --- a/signal/proto/signalexchange.proto +++ b/signal/proto/signalexchange.proto @@ -47,6 +47,7 @@ message Body { ANSWER = 1; CANDIDATE = 2; MODE = 4; + GO_IDLE = 5; } Type type = 1; string payload = 2; @@ -74,4 +75,4 @@ message RosenpassConfig { bytes rosenpassPubKey = 1; // rosenpassServerAddr is an IP:port of the rosenpass service string rosenpassServerAddr = 2; -} \ No newline at end of file +} From 8942c40fde62421d510361a2558afafc1bcd3fd9 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 6 Jul 2025 15:13:14 +0200 Subject: [PATCH 137/213] [client] Fix nil pointer exception in lazy connection (#4109) Remove unused variable --- client/internal/lazyconn/manager/manager.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go index b45b39221..416e4e7e7 100644 --- a/client/internal/lazyconn/manager/manager.go +++ b/client/internal/lazyconn/manager/manager.go @@ -11,7 +11,6 @@ import ( "github.com/netbirdio/netbird/client/internal/lazyconn" "github.com/netbirdio/netbird/client/internal/lazyconn/activity" "github.com/netbirdio/netbird/client/internal/lazyconn/inactivity" - "github.com/netbirdio/netbird/client/internal/peer/dispatcher" peerid "github.com/netbirdio/netbird/client/internal/peer/id" "github.com/netbirdio/netbird/client/internal/peerstore" "github.com/netbirdio/netbird/route" @@ -43,10 +42,8 @@ type Config struct { type Manager struct { engineCtx context.Context peerStore *peerstore.Store - connStateDispatcher *dispatcher.ConnectionDispatcher inactivityThreshold time.Duration - connStateListener *dispatcher.ConnectionListener managedPeers map[string]*lazyconn.PeerConfig managedPeersByConnID map[peerid.ConnID]*managedPeer excludes map[string]lazyconn.PeerConfig @@ -453,7 +450,6 @@ func (m *Manager) close() { m.managedPeersMu.Lock() defer m.managedPeersMu.Unlock() - m.connStateDispatcher.RemoveListener(m.connStateListener) m.activityManager.Close() m.managedPeers = make(map[string]*lazyconn.PeerConfig) From 768ba24fda97db833b0d44eca6a269ed12cb245f Mon Sep 17 00:00:00 2001 From: "M. Essam" Date: Tue, 8 Jul 2025 19:08:13 +0300 Subject: [PATCH 138/213] [management,rest] Add name/ip filters to peer management rest client (#4112) --- management/client/rest/accounts.go | 6 ++--- management/client/rest/client.go | 10 +++++++- management/client/rest/dns.go | 14 +++++------ management/client/rest/events.go | 2 +- management/client/rest/geo.go | 4 ++-- management/client/rest/groups.go | 10 ++++---- management/client/rest/networks.go | 30 +++++++++++------------ management/client/rest/peers.go | 32 ++++++++++++++++++++----- management/client/rest/peers_test.go | 4 ++++ management/client/rest/policies.go | 10 ++++---- management/client/rest/posturechecks.go | 10 ++++---- management/client/rest/routes.go | 10 ++++---- management/client/rest/setupkeys.go | 10 ++++---- management/client/rest/tokens.go | 8 +++---- management/client/rest/users.go | 12 +++++----- 15 files changed, 102 insertions(+), 70 deletions(-) diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go index 2530e4f72..fbe3010e1 100644 --- a/management/client/rest/accounts.go +++ b/management/client/rest/accounts.go @@ -16,7 +16,7 @@ type AccountsAPI struct { // List list all accounts, only returns one account always // See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/accounts", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/accounts", nil, nil) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api. if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api. // Delete delete account // See more: https://docs.netbird.io/api/resources/accounts#delete-an-account func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/client.go b/management/client/rest/client.go index 8bf11caae..b5945985f 100644 --- a/management/client/rest/client.go +++ b/management/client/rest/client.go @@ -117,7 +117,7 @@ func (c *Client) initialize() { } // NewRequest creates and executes new management API request -func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { +func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader, query map[string]string) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body) if err != nil { return nil, err @@ -129,6 +129,14 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re req.Header.Add("Content-Type", "application/json") } + if len(query) != 0 { + q := req.URL.Query() + for k, v := range query { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + resp, err := c.httpClient.Do(req) if err != nil { return nil, err diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go index 1e35c0226..3fb74d5f5 100644 --- a/management/client/rest/dns.go +++ b/management/client/rest/dns.go @@ -16,7 +16,7 @@ type DNSAPI struct { // ListNameserverGroups list all nameserver groups // See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGrou // GetNameserverGroup get nameserver group info // See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiD if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st // DeleteNameserverGroup delete nameserver group // See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil, nil) if err != nil { return err } @@ -94,7 +94,7 @@ func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID st // GetSettings get DNS settings // See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/settings", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/settings", nil, nil) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettin if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } diff --git a/management/client/rest/events.go b/management/client/rest/events.go index cae813e86..775d3ba2e 100644 --- a/management/client/rest/events.go +++ b/management/client/rest/events.go @@ -14,7 +14,7 @@ type EventsAPI struct { // List list all events // See more: https://docs.netbird.io/api/resources/events#list-all-events func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/events", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/events", nil, nil) if err != nil { return nil, err } diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go index d06d65d80..dfecee09e 100644 --- a/management/client/rest/geo.go +++ b/management/client/rest/geo.go @@ -14,7 +14,7 @@ type GeoLocationAPI struct { // ListCountries list all country codes // See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries", nil, nil) if err != nil { return nil, err } @@ -28,7 +28,7 @@ func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, erro // ListCountryCities Get a list of all English city names for a given country code // See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil, nil) if err != nil { return nil, err } diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go index 7612b7188..7d4bac62c 100644 --- a/management/client/rest/groups.go +++ b/management/client/rest/groups.go @@ -16,7 +16,7 @@ type GroupsAPI struct { // List list all groups // See more: https://docs.netbird.io/api/resources/groups#list-all-groups func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/groups", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/groups", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { // Get get group info // See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/groups/"+groupID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/groups/"+groupID, nil, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONReq if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA // Delete delete group // See more: https://docs.netbird.io/api/resources/groups#delete-a-group func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/groups/"+groupID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/groups/"+groupID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go index b744e3fe7..9441780f3 100644 --- a/management/client/rest/networks.go +++ b/management/client/rest/networks.go @@ -16,7 +16,7 @@ type NetworksAPI struct { // List list all networks // See more: https://docs.netbird.io/api/resources/networks#list-all-networks func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/networks", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { // Get get network info // See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+networkID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+networkID, nil, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSO if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api. if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api. // Delete delete network // See more: https://docs.netbird.io/api/resources/networks#delete-a-network func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+networkID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+networkID, nil, nil) if err != nil { return err } @@ -108,7 +108,7 @@ func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI { // List list all resources in networks // See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil, nil) if err != nil { return nil, err } @@ -122,7 +122,7 @@ func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, // Get get network resource info // See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil, nil) if err != nil { return nil, err } @@ -140,7 +140,7 @@ func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNet if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -172,7 +172,7 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri // Delete delete network resource // See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil, nil) if err != nil { return err } @@ -200,7 +200,7 @@ func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI { // List list all routers in networks // See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil, nil) if err != nil { return nil, err } @@ -214,7 +214,7 @@ func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, erro // Get get network router info // See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil, nil) if err != nil { return nil, err } @@ -232,7 +232,7 @@ func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetwo if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -250,7 +250,7 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -264,7 +264,7 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, // Delete delete network router // See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go index 37679fdb9..f4364bb62 100644 --- a/management/client/rest/peers.go +++ b/management/client/rest/peers.go @@ -13,10 +13,30 @@ type PeersAPI struct { c *Client } +// PeersListOption options for Peers List API +type PeersListOption func() (string, string) + +func PeerNameFilter(name string) PeersListOption { + return func() (string, string) { + return "name", name + } +} + +func PeerIPFilter(ip string) PeersListOption { + return func() (string, string) { + return "ip", ip + } +} + // List list all peers // See more: https://docs.netbird.io/api/resources/peers#list-all-peers -func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/peers", nil) +func (a *PeersAPI) List(ctx context.Context, opts ...PeersListOption) ([]api.Peer, error) { + query := make(map[string]string) + for _, o := range opts { + k, v := o() + query[k] = v + } + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers", nil, query) if err != nil { return nil, err } @@ -30,7 +50,7 @@ func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { // Get retrieve a peer // See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID, nil, nil) if err != nil { return nil, err } @@ -48,7 +68,7 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -62,7 +82,7 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi // Delete delete a peer // See more: https://docs.netbird.io/api/resources/peers#delete-a-peer func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+peerID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+peerID, nil, nil) if err != nil { return err } @@ -76,7 +96,7 @@ func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { // ListAccessiblePeers list all peers that the specified peer can connect to within the network // See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil, nil) if err != nil { return nil, err } diff --git a/management/client/rest/peers_test.go b/management/client/rest/peers_test.go index 4c5cd1e60..f31e44e10 100644 --- a/management/client/rest/peers_test.go +++ b/management/client/rest/peers_test.go @@ -184,6 +184,10 @@ func TestPeers_Integration(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, peers) + filteredPeers, err := c.Peers.List(context.Background(), rest.PeerIPFilter("192.168.10.0")) + require.NoError(t, err) + require.Empty(t, filteredPeers) + peer, err := c.Peers.Get(context.Background(), peers[0].Id) require.NoError(t, err) assert.Equal(t, peers[0].Id, peer.Id) diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go index 2f2df4a78..a6e0e38d3 100644 --- a/management/client/rest/policies.go +++ b/management/client/rest/policies.go @@ -18,7 +18,7 @@ type PoliciesAPI struct { func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { path := "/api/policies" - resp, err := a.c.NewRequest(ctx, "GET", path, nil) + resp, err := a.c.NewRequest(ctx, "GET", path, nil, nil) if err != nil { return nil, err } @@ -32,7 +32,7 @@ func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { // Get get policy info // See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/policies/"+policyID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/policies/"+policyID, nil, nil) if err != nil { return nil, err } @@ -50,7 +50,7 @@ func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSO if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -70,7 +70,7 @@ func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.P if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", path, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", path, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.P // Delete delete policy // See more: https://docs.netbird.io/api/resources/policies#delete-a-policy func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/policies/"+policyID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/policies/"+policyID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go index 622eeeb64..2ab8f4549 100644 --- a/management/client/rest/posturechecks.go +++ b/management/client/rest/posturechecks.go @@ -16,7 +16,7 @@ type PostureChecksAPI struct { // List list all posture checks // See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) // Get get posture check info // See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostur if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re // Delete delete posture check // See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go index 671c3bfc9..183c363cf 100644 --- a/management/client/rest/routes.go +++ b/management/client/rest/routes.go @@ -16,7 +16,7 @@ type RoutesAPI struct { // List list all routes // See more: https://docs.netbird.io/api/resources/routes#list-all-routes func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/routes", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/routes", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { // Get get route info // See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/routes/"+routeID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/routes/"+routeID, nil, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONReq if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA // Delete delete route // See more: https://docs.netbird.io/api/resources/routes#delete-a-route func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/routes/"+routeID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/routes/"+routeID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go index 5625b6acc..6f26cd0b7 100644 --- a/management/client/rest/setupkeys.go +++ b/management/client/rest/setupkeys.go @@ -16,7 +16,7 @@ type SetupKeysAPI struct { // List list all setup keys // See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { // Get get setup key info // See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil, nil) if err != nil { return nil, err } @@ -50,7 +50,7 @@ func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJ if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", path, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", path, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -82,7 +82,7 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap // Delete delete setup key // See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go index 278a0d159..7a63d0c9d 100644 --- a/management/client/rest/tokens.go +++ b/management/client/rest/tokens.go @@ -16,7 +16,7 @@ type TokensAPI struct { // List list user tokens // See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil, nil) if err != nil { return nil, err } @@ -30,7 +30,7 @@ func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAcce // Get get user token info // See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil, nil) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA // Delete delete user token // See more: https://docs.netbird.io/api/resources/tokens#delete-a-token func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil, nil) if err != nil { return err } diff --git a/management/client/rest/users.go b/management/client/rest/users.go index 107b0581e..f0ef54be2 100644 --- a/management/client/rest/users.go +++ b/management/client/rest/users.go @@ -16,7 +16,7 @@ type UsersAPI struct { // List list all users, only returns one user always // See more: https://docs.netbird.io/api/resources/users#list-all-users func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/users", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users", nil, nil) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONReque if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi if err != nil { return nil, err } - resp, err := a.c.NewRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes)) + resp, err := a.c.NewRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes), nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi // Delete delete user // See more: https://docs.netbird.io/api/resources/users#delete-a-user func (a *UsersAPI) Delete(ctx context.Context, userID string) error { - resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID, nil) + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID, nil, nil) if err != nil { return err } @@ -80,7 +80,7 @@ func (a *UsersAPI) Delete(ctx context.Context, userID string) error { // ResendInvitation resend user invitation // See more: https://docs.netbird.io/api/resources/users#resend-user-invitation func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { - resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil) + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil, nil) if err != nil { return err } @@ -94,7 +94,7 @@ func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { // Current gets the current user info // See more: https://docs.netbird.io/api/resources/users#retrieve-current-user func (a *UsersAPI) Current(ctx context.Context) (*api.User, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/users/current", nil) + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/current", nil, nil) if err != nil { return nil, err } From 969f1ed59a9645eaee4fd76b3529b4b829accc35 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 9 Jul 2025 10:14:10 +0300 Subject: [PATCH 139/213] [management] Remove deleted user peers from groups on user deletion (#4121) Refactors peer deletion to centralize group cleanup logic, ensuring deleted peers are consistently removed from all groups in one place. - Removed redundant group removal code from DefaultAccountManager.DeletePeer - Added group removal logic inside deletePeers to handle both single and multiple peer deletions --- management/server/peer.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index 1dd390dd9..44156e534 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -364,19 +364,6 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return err } - groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthUpdate, accountID, peerID) - if err != nil { - return fmt.Errorf("failed to get peer groups: %w", err) - } - - for _, group := range groups { - group.RemovePeer(peerID) - err = transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) - if err != nil { - return fmt.Errorf("failed to save group: %w", err) - } - } - eventsToStore, err = deletePeers(ctx, am, transaction, accountID, userID, []*nbpeer.Peer{peer}) return err }) @@ -1517,13 +1504,26 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto } dnsDomain := am.GetDNSDomain(settings) + network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + for _, peer := range peers { - if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil { - return nil, err + groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthUpdate, accountID, peer.ID) + if err != nil { + return nil, fmt.Errorf("failed to get peer groups: %w", err) } - network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID) - if err != nil { + for _, group := range groups { + group.RemovePeer(peer.ID) + err = transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + if err != nil { + return nil, fmt.Errorf("failed to save group: %w", err) + } + } + + if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil { return nil, err } From f17dd3619cf6c121fa4ea76d3f1e8192b15497d8 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Wed, 9 Jul 2025 15:49:09 +0200 Subject: [PATCH 140/213] [misc] update image in README.md (#4122) --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c3b365694..d5469c28b 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,9 @@ **Secure.** NetBird enables secure remote access by applying granular access policies while allowing you to manage them intuitively from a single place. Works universally on any infrastructure. -### Open-Source Network Security in a Single Platform +### Open Source Network Security in a Single Platform - -![netbird_2](https://github.com/netbirdio/netbird/assets/700848/46bc3b73-508d-4a0e-bb9a-f465d68646ab) +centralized-network-management 1 ### NetBird on Lawrence Systems (Video) [![Watch the video](https://img.youtube.com/vi/Kwrff6h0rEw/0.jpg)](https://www.youtube.com/watch?v=Kwrff6h0rEw) From 408f423adcf7a51f1ae56cdb6016c0b0158643fb Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 9 Jul 2025 22:16:08 +0200 Subject: [PATCH 141/213] [client] Disable pidfd check on Android 11 and below (#4127) Disable pidfd check on Android 11 and below On Android 11 (SDK <= 30) and earlier, pidfd-related system calls are blocked by seccomp policies, causing SIGSYS crashes. This change overrides `checkPidfdOnce` to return an error on affected versions, preventing the use of unsupported pidfd features. --- .github/workflows/mobile-build-validation.yml | 2 +- client/android/client.go | 4 ++- client/android/exec.go | 26 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 client/android/exec.go diff --git a/.github/workflows/mobile-build-validation.yml b/.github/workflows/mobile-build-validation.yml index 569956a54..c7d43695b 100644 --- a/.github/workflows/mobile-build-validation.yml +++ b/.github/workflows/mobile-build-validation.yml @@ -43,7 +43,7 @@ jobs: - name: gomobile init run: gomobile init - name: build android netbird lib - run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android + run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-checklinkname=0 -X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android env: CGO_ENABLED: 0 ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620 diff --git a/client/android/client.go b/client/android/client.go index a17439696..0d0c76549 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -64,7 +64,9 @@ type Client struct { } // NewClient instantiate a new Client -func NewClient(cfgFile, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client { +func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client { + execWorkaround(androidSDKVersion) + net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket) return &Client{ cfgFile: cfgFile, diff --git a/client/android/exec.go b/client/android/exec.go new file mode 100644 index 000000000..805d3129b --- /dev/null +++ b/client/android/exec.go @@ -0,0 +1,26 @@ +//go:build android + +package android + +import ( + "fmt" + _ "unsafe" +) + +// https://github.com/golang/go/pull/69543/commits/aad6b3b32c81795f86bc4a9e81aad94899daf520 +// In Android version 11 and earlier, pidfd-related system calls +// are not allowed by the seccomp policy, which causes crashes due +// to SIGSYS signals. + +//go:linkname checkPidfdOnce os.checkPidfdOnce +var checkPidfdOnce func() error + +func execWorkaround(androidSDKVersion int) { + if androidSDKVersion > 30 { // above Android 11 + return + } + + checkPidfdOnce = func() error { + return fmt.Errorf("unsupported Android version") + } +} From e59d75d56ab047c5374177f16976988aac486405 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 10 Jul 2025 14:24:20 +0200 Subject: [PATCH 142/213] Nil check in iface configurer (#4132) --- client/iface/iface.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/client/iface/iface.go b/client/iface/iface.go index 1b9055e6c..e90c3536b 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -29,6 +29,11 @@ const ( WgInterfaceDefault = configurer.WgInterfaceDefault ) +var ( + // ErrIfaceNotFound is returned when the WireGuard interface is not found + ErrIfaceNotFound = fmt.Errorf("wireguard interface not found") +) + type wgProxyFactory interface { GetProxy() wgproxy.Proxy Free() error @@ -117,6 +122,9 @@ func (w *WGIface) UpdateAddr(newAddr string) error { func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { w.mu.Lock() defer w.mu.Unlock() + if w.configurer == nil { + return ErrIfaceNotFound + } log.Debugf("updating interface %s peer %s, endpoint %s, allowedIPs %v", w.tun.DeviceName(), peerKey, endpoint, allowedIps) return w.configurer.UpdatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey) @@ -126,6 +134,9 @@ func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAliv func (w *WGIface) RemovePeer(peerKey string) error { w.mu.Lock() defer w.mu.Unlock() + if w.configurer == nil { + return ErrIfaceNotFound + } log.Debugf("Removing peer %s from interface %s ", peerKey, w.tun.DeviceName()) return w.configurer.RemovePeer(peerKey) @@ -135,6 +146,9 @@ func (w *WGIface) RemovePeer(peerKey string) error { func (w *WGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { w.mu.Lock() defer w.mu.Unlock() + if w.configurer == nil { + return ErrIfaceNotFound + } log.Debugf("Adding allowed IP to interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) return w.configurer.AddAllowedIP(peerKey, allowedIP) @@ -144,6 +158,9 @@ func (w *WGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { func (w *WGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error { w.mu.Lock() defer w.mu.Unlock() + if w.configurer == nil { + return ErrIfaceNotFound + } log.Debugf("Removing allowed IP from interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) return w.configurer.RemoveAllowedIP(peerKey, allowedIP) @@ -214,6 +231,9 @@ func (w *WGIface) GetWGDevice() *wgdevice.Device { // GetStats returns the last handshake time, rx and tx bytes func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) { + if w.configurer == nil { + return nil, ErrIfaceNotFound + } return w.configurer.GetStats() } @@ -221,11 +241,19 @@ func (w *WGIface) LastActivities() map[string]time.Time { w.mu.Lock() defer w.mu.Unlock() + if w.configurer == nil { + return nil + } + return w.configurer.LastActivities() } func (w *WGIface) FullStats() (*configurer.Stats, error) { + if w.configurer == nil { + return nil, ErrIfaceNotFound + } + return w.configurer.FullStats() } From e3b40ba694a5f3b3396b09ebac86b224848901d9 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 10 Jul 2025 15:00:58 +0200 Subject: [PATCH 143/213] Update cli description of lazy connection (#4133) --- client/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 16e445f4d..e00a9b073 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -184,7 +184,7 @@ func init() { upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.") upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted") upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.") - upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand.") + upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand. Note: this setting may be overridden by management configuration.") debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle") debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL)) From 8632dd15f13a6954d71f09af667089bb07571a26 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:21:01 +0200 Subject: [PATCH 144/213] [management] added cleanupWindow for collecting several ephemeral peers to delete (#4130) --------- Co-authored-by: Maycon Santos Co-authored-by: Pedro Costa <550684+pnmcosta@users.noreply.github.com> --- management/client/client_test.go | 6 ++ management/server/account/manager.go | 1 + management/server/dns_test.go | 2 + management/server/ephemeral.go | 37 +++++-- management/server/ephemeral_test.go | 98 ++++++++++++++++--- management/server/management_proto_test.go | 6 +- management/server/mock_server/account_mock.go | 4 + management/server/nameserver_test.go | 6 ++ management/server/peer.go | 27 +++-- management/server/peer_test.go | 10 ++ 10 files changed, 171 insertions(+), 26 deletions(-) diff --git a/management/client/client_test.go b/management/client/client_test.go index c163d1833..1847af73e 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -87,6 +87,12 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { ). Return(&types.Settings{}, nil). AnyTimes() + settingsMockManager. + EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() + permissionsManagerMock := permissions.NewMockManager(ctrl) permissionsManagerMock. EXPECT(). diff --git a/management/server/account/manager.go b/management/server/account/manager.go index ed17fa5ec..f8aa2756a 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -112,6 +112,7 @@ type Manager interface { GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error UpdateAccountPeers(ctx context.Context, accountID string) + BufferUpdateAccountPeers(ctx context.Context, accountID string) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error GetStore() store.Store diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 02bb042d7..31c944a25 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -216,6 +216,8 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { t.Cleanup(ctrl.Finish) settingsMockManager := settings.NewMockManager(ctrl) + // return empty extra settings for expected calls to UpdateAccountPeers + settingsMockManager.EXPECT().GetExtraSettings(gomock.Any(), gomock.Any()).Return(&types.ExtraSettings{}, nil).AnyTimes() permissionsManager := permissions.NewManager(store) return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } diff --git a/management/server/ephemeral.go b/management/server/ephemeral.go index 3cb9b7536..9f4348ebb 100644 --- a/management/server/ephemeral.go +++ b/management/server/ephemeral.go @@ -15,6 +15,8 @@ import ( const ( ephemeralLifeTime = 10 * time.Minute + // cleanupWindow is the time window to wait after nearest peer deadline to start the cleanup procedure. + cleanupWindow = 1 * time.Minute ) var ( @@ -41,6 +43,9 @@ type EphemeralManager struct { tailPeer *ephemeralPeer peersLock sync.Mutex timer *time.Timer + + lifeTime time.Duration + cleanupWindow time.Duration } // NewEphemeralManager instantiate new EphemeralManager @@ -48,6 +53,9 @@ func NewEphemeralManager(store store.Store, accountManager nbAccount.Manager) *E return &EphemeralManager{ store: store, accountManager: accountManager, + + lifeTime: ephemeralLifeTime, + cleanupWindow: cleanupWindow, } } @@ -60,7 +68,7 @@ func (e *EphemeralManager) LoadInitialPeers(ctx context.Context) { e.loadEphemeralPeers(ctx) if e.headPeer != nil { - e.timer = time.AfterFunc(ephemeralLifeTime, func() { + e.timer = time.AfterFunc(e.lifeTime, func() { e.cleanup(ctx) }) } @@ -113,9 +121,13 @@ func (e *EphemeralManager) OnPeerDisconnected(ctx context.Context, peer *nbpeer. return } - e.addPeer(peer.AccountID, peer.ID, newDeadLine()) + e.addPeer(peer.AccountID, peer.ID, e.newDeadLine()) if e.timer == nil { - e.timer = time.AfterFunc(e.headPeer.deadline.Sub(timeNow()), func() { + delay := e.headPeer.deadline.Sub(timeNow()) + e.cleanupWindow + if delay < 0 { + delay = 0 + } + e.timer = time.AfterFunc(delay, func() { e.cleanup(ctx) }) } @@ -128,7 +140,7 @@ func (e *EphemeralManager) loadEphemeralPeers(ctx context.Context) { return } - t := newDeadLine() + t := e.newDeadLine() for _, p := range peers { e.addPeer(p.AccountID, p.ID, t) } @@ -155,7 +167,11 @@ func (e *EphemeralManager) cleanup(ctx context.Context) { } if e.headPeer != nil { - e.timer = time.AfterFunc(e.headPeer.deadline.Sub(timeNow()), func() { + delay := e.headPeer.deadline.Sub(timeNow()) + e.cleanupWindow + if delay < 0 { + delay = 0 + } + e.timer = time.AfterFunc(delay, func() { e.cleanup(ctx) }) } else { @@ -164,13 +180,20 @@ func (e *EphemeralManager) cleanup(ctx context.Context) { e.peersLock.Unlock() + bufferAccountCall := make(map[string]struct{}) + for id, p := range deletePeers { log.WithContext(ctx).Debugf("delete ephemeral peer: %s", id) err := e.accountManager.DeletePeer(ctx, p.accountID, id, activity.SystemInitiator) if err != nil { log.WithContext(ctx).Errorf("failed to delete ephemeral peer: %s", err) + } else { + bufferAccountCall[p.accountID] = struct{}{} } } + for accountID := range bufferAccountCall { + e.accountManager.BufferUpdateAccountPeers(ctx, accountID) + } } func (e *EphemeralManager) addPeer(accountID string, peerID string, deadline time.Time) { @@ -223,6 +246,6 @@ func (e *EphemeralManager) isPeerOnList(id string) bool { return false } -func newDeadLine() time.Time { - return timeNow().Add(ephemeralLifeTime) +func (e *EphemeralManager) newDeadLine() time.Time { + return timeNow().Add(e.lifeTime) } diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go index 3cf6ae7f3..f71d48c58 100644 --- a/management/server/ephemeral_test.go +++ b/management/server/ephemeral_test.go @@ -3,9 +3,12 @@ package server import ( "context" "fmt" + "sync" "testing" "time" + "github.com/stretchr/testify/assert" + nbAccount "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" @@ -27,28 +30,65 @@ func (s *MockStore) GetAllEphemeralPeers(_ context.Context, _ store.LockingStren return peers, nil } -type MocAccountManager struct { +type MockAccountManager struct { + mu sync.Mutex nbAccount.Manager - store *MockStore + store *MockStore + deletePeerCalls int + bufferUpdateCalls map[string]int + wg *sync.WaitGroup } -func (a MocAccountManager) DeletePeer(_ context.Context, accountID, peerID, userID string) error { +func (a *MockAccountManager) DeletePeer(_ context.Context, accountID, peerID, userID string) error { + a.mu.Lock() + defer a.mu.Unlock() + a.deletePeerCalls++ + if a.wg != nil { + a.wg.Done() + } delete(a.store.account.Peers, peerID) - return nil //nolint:nil + return nil } -func (a MocAccountManager) GetStore() store.Store { +func (a *MockAccountManager) GetDeletePeerCalls() int { + a.mu.Lock() + defer a.mu.Unlock() + return a.deletePeerCalls +} + +func (a *MockAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) { + a.mu.Lock() + defer a.mu.Unlock() + if a.bufferUpdateCalls == nil { + a.bufferUpdateCalls = make(map[string]int) + } + a.bufferUpdateCalls[accountID]++ +} + +func (a *MockAccountManager) GetBufferUpdateCalls(accountID string) int { + a.mu.Lock() + defer a.mu.Unlock() + if a.bufferUpdateCalls == nil { + return 0 + } + return a.bufferUpdateCalls[accountID] +} + +func (a *MockAccountManager) GetStore() store.Store { return a.store } func TestNewManager(t *testing.T) { + t.Cleanup(func() { + timeNow = time.Now + }) startTime := time.Now() timeNow = func() time.Time { return startTime } store := &MockStore{} - am := MocAccountManager{ + am := MockAccountManager{ store: store, } @@ -56,7 +96,7 @@ func TestNewManager(t *testing.T) { numberOfEphemeralPeers := 3 seedPeers(store, numberOfPeers, numberOfEphemeralPeers) - mgr := NewEphemeralManager(store, am) + mgr := NewEphemeralManager(store, &am) mgr.loadEphemeralPeers(context.Background()) startTime = startTime.Add(ephemeralLifeTime + 1) mgr.cleanup(context.Background()) @@ -67,13 +107,16 @@ func TestNewManager(t *testing.T) { } func TestNewManagerPeerConnected(t *testing.T) { + t.Cleanup(func() { + timeNow = time.Now + }) startTime := time.Now() timeNow = func() time.Time { return startTime } store := &MockStore{} - am := MocAccountManager{ + am := MockAccountManager{ store: store, } @@ -81,7 +124,7 @@ func TestNewManagerPeerConnected(t *testing.T) { numberOfEphemeralPeers := 3 seedPeers(store, numberOfPeers, numberOfEphemeralPeers) - mgr := NewEphemeralManager(store, am) + mgr := NewEphemeralManager(store, &am) mgr.loadEphemeralPeers(context.Background()) mgr.OnPeerConnected(context.Background(), store.account.Peers["ephemeral_peer_0"]) @@ -95,13 +138,16 @@ func TestNewManagerPeerConnected(t *testing.T) { } func TestNewManagerPeerDisconnected(t *testing.T) { + t.Cleanup(func() { + timeNow = time.Now + }) startTime := time.Now() timeNow = func() time.Time { return startTime } store := &MockStore{} - am := MocAccountManager{ + am := MockAccountManager{ store: store, } @@ -109,7 +155,7 @@ func TestNewManagerPeerDisconnected(t *testing.T) { numberOfEphemeralPeers := 3 seedPeers(store, numberOfPeers, numberOfEphemeralPeers) - mgr := NewEphemeralManager(store, am) + mgr := NewEphemeralManager(store, &am) mgr.loadEphemeralPeers(context.Background()) for _, v := range store.account.Peers { mgr.OnPeerConnected(context.Background(), v) @@ -126,6 +172,36 @@ func TestNewManagerPeerDisconnected(t *testing.T) { } } +func TestCleanupSchedulingBehaviorIsBatched(t *testing.T) { + const ( + ephemeralPeers = 10 + testLifeTime = 1 * time.Second + testCleanupWindow = 100 * time.Millisecond + ) + mockStore := &MockStore{} + mockAM := &MockAccountManager{ + store: mockStore, + } + mockAM.wg = &sync.WaitGroup{} + mockAM.wg.Add(ephemeralPeers) + mgr := NewEphemeralManager(mockStore, mockAM) + mgr.lifeTime = testLifeTime + mgr.cleanupWindow = testCleanupWindow + + account := newAccountWithId(context.Background(), "account", "", "", false) + mockStore.account = account + for i := range ephemeralPeers { + p := &nbpeer.Peer{ID: fmt.Sprintf("peer-%d", i), AccountID: account.Id, Ephemeral: true} + mockStore.account.Peers[p.ID] = p + time.Sleep(testCleanupWindow / ephemeralPeers) + mgr.OnPeerDisconnected(context.Background(), p) + } + mockAM.wg.Wait() + assert.Len(t, mockStore.account.Peers, 0, "all ephemeral peers should be cleaned up after the lifetime") + assert.Equal(t, 1, mockAM.GetBufferUpdateCalls(account.Id), "buffer update should be called once") + assert.Equal(t, ephemeralPeers, mockAM.GetDeletePeerCalls(), "should have deleted all peers") +} + func seedPeers(store *MockStore, numberOfPeers int, numberOfEphemeralPeers int) { store.account = newAccountWithId(context.Background(), "my account", "", "", false) diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 337890ef9..57c00ed9f 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -440,7 +440,11 @@ func startManagementForTest(t *testing.T, testFile string, config *types.Config) GetSettings(gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes(). Return(&types.Settings{}, nil) - + settingsMockManager. + EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() permissionsManager := permissions.NewManager(store) accountManager, err := BuildManager(ctx, store, peersUpdateManager, nil, "", "netbird.selfhosted", diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 8837f9f50..4004f1b57 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -126,6 +126,10 @@ func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID // do nothing } +func (am *MockAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) { + // do nothing +} + func (am *MockAccountManager) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error { if am.DeleteSetupKeyFunc != nil { return am.DeleteSetupKeyFunc(ctx, accountID, userID, keyID) diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 75d1e7972..8fada742c 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -778,6 +778,12 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() + permissionsManager := permissions.NewManager(store) return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } diff --git a/management/server/peer.go b/management/server/peer.go index 44156e534..a60513b38 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -375,7 +375,7 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer storeEvent() } - if updateAccountPeers { + if updateAccountPeers && userID != activity.SystemInitiator { am.BufferUpdateAccountPeers(ctx, accountID) } @@ -1177,6 +1177,19 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account globalStart := time.Now() + hasPeersConnected := false + for _, peer := range account.Peers { + if am.peersUpdateManager.HasChannel(peer.ID) { + hasPeersConnected = true + break + } + + } + + if !hasPeersConnected { + return + } + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { log.WithContext(ctx).Errorf("failed to send out updates to peers, failed to get validate peers: %v", err) @@ -1198,6 +1211,12 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account return } + extraSetting, err := am.settingsManager.GetExtraSettings(ctx, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get flow enabled status: %v", err) + return + } + for _, peer := range account.Peers { if !am.peersUpdateManager.HasChannel(peer.ID) { log.WithContext(ctx).Tracef("peer %s doesn't have a channel, skipping network map update", peer.ID) @@ -1232,12 +1251,6 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } am.metrics.UpdateChannelMetrics().CountMergeNetworkMapDuration(time.Since(start)) - extraSetting, err := am.settingsManager.GetExtraSettings(ctx, accountID) - if err != nil { - log.WithContext(ctx).Errorf("failed to get flow enabled status: %v", err) - return - } - start = time.Now() update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting) am.metrics.UpdateChannelMetrics().CountToSyncResponseDuration(time.Since(start)) diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 31439d670..07ec5037b 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1344,6 +1344,11 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() permissionsManager := permissions.NewManager(s) am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) @@ -1556,6 +1561,11 @@ func Test_LoginPeer(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() permissionsManager := permissions.NewManager(s) am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) From a7ea881900b53bdafc80ccb7ad42e96eda308ef7 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:13:53 +0200 Subject: [PATCH 145/213] [client] Add rotated logs flag for debug bundle generation (#4100) --- client/cmd/debug.go | 46 +++++--- client/cmd/root.go | 10 -- client/internal/debug/debug.go | 83 +++++++++----- client/proto/daemon.pb.go | 13 ++- client/proto/daemon.proto | 1 + client/proto/daemon_grpc.pb.go | 192 ++++++++++++++------------------- client/proto/generate.sh | 2 +- client/server/debug.go | 1 + go.mod | 2 +- go.sum | 4 +- util/log.go | 2 +- 11 files changed, 188 insertions(+), 168 deletions(-) diff --git a/client/cmd/debug.go b/client/cmd/debug.go index 385bd95f5..4036bb8f6 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -17,10 +17,18 @@ import ( "github.com/netbirdio/netbird/client/server" nbstatus "github.com/netbirdio/netbird/client/status" mgmProto "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/upload-server/types" ) const errCloseConnection = "Failed to close connection: %v" +var ( + logFileCount uint32 + systemInfoFlag bool + uploadBundleFlag bool + uploadBundleURLFlag string +) + var debugCmd = &cobra.Command{ Use: "debug", Short: "Debugging commands", @@ -88,12 +96,13 @@ func debugBundle(cmd *cobra.Command, _ []string) error { client := proto.NewDaemonServiceClient(conn) request := &proto.DebugBundleRequest{ - Anonymize: anonymizeFlag, - Status: getStatusOutput(cmd, anonymizeFlag), - SystemInfo: debugSystemInfoFlag, + Anonymize: anonymizeFlag, + Status: getStatusOutput(cmd, anonymizeFlag), + SystemInfo: systemInfoFlag, + LogFileCount: logFileCount, } - if debugUploadBundle { - request.UploadURL = debugUploadBundleURL + if uploadBundleFlag { + request.UploadURL = uploadBundleURLFlag } resp, err := client.DebugBundle(cmd.Context(), request) if err != nil { @@ -105,7 +114,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error { return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason()) } - if debugUploadBundle { + if uploadBundleFlag { cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey()) } @@ -223,12 +232,13 @@ func runForDuration(cmd *cobra.Command, args []string) error { headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag)) request := &proto.DebugBundleRequest{ - Anonymize: anonymizeFlag, - Status: statusOutput, - SystemInfo: debugSystemInfoFlag, + Anonymize: anonymizeFlag, + Status: statusOutput, + SystemInfo: systemInfoFlag, + LogFileCount: logFileCount, } - if debugUploadBundle { - request.UploadURL = debugUploadBundleURL + if uploadBundleFlag { + request.UploadURL = uploadBundleURLFlag } resp, err := client.DebugBundle(cmd.Context(), request) if err != nil { @@ -255,7 +265,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason()) } - if debugUploadBundle { + if uploadBundleFlag { cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey()) } @@ -375,3 +385,15 @@ func generateDebugBundle(config *internal.Config, recorder *peer.Status, connect } log.Infof("Generated debug bundle from SIGUSR1 at: %s", path) } + +func init() { + debugBundleCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle") + debugBundleCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle") + debugBundleCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server") + debugBundleCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle") + + forCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle") + forCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle") + forCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server") + forCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle") +} diff --git a/client/cmd/root.go b/client/cmd/root.go index e00a9b073..fa4bd4d42 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -22,7 +22,6 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/upload-server/types" ) const ( @@ -38,10 +37,7 @@ const ( serverSSHAllowedFlag = "allow-server-ssh" extraIFaceBlackListFlag = "extra-iface-blacklist" dnsRouteIntervalFlag = "dns-router-interval" - systemInfoFlag = "system-info" enableLazyConnectionFlag = "enable-lazy-connection" - uploadBundle = "upload-bundle" - uploadBundleURL = "upload-bundle-url" ) var ( @@ -75,10 +71,7 @@ var ( autoConnectDisabled bool extraIFaceBlackList []string anonymizeFlag bool - debugSystemInfoFlag bool dnsRouteInterval time.Duration - debugUploadBundle bool - debugUploadBundleURL string lazyConnEnabled bool rootCmd = &cobra.Command{ @@ -186,9 +179,6 @@ func init() { upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.") upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand. Note: this setting may be overridden by management configuration.") - debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle") - debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL)) - debugCmd.PersistentFlags().StringVar(&debugUploadBundleURL, uploadBundleURL, types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle") } // SetupCloseHandler handles SIGTERM signal and exits with success diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index dfed47f05..6455b3aaf 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -167,6 +167,7 @@ type BundleGenerator struct { anonymize bool clientStatus string includeSystemInfo bool + logFileCount uint32 archive *zip.Writer } @@ -175,6 +176,7 @@ type BundleConfig struct { Anonymize bool ClientStatus string IncludeSystemInfo bool + LogFileCount uint32 } type GeneratorDependencies struct { @@ -185,6 +187,12 @@ type GeneratorDependencies struct { } func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator { + // Default to 1 log file for backward compatibility when 0 is provided + logFileCount := cfg.LogFileCount + if logFileCount == 0 { + logFileCount = 1 + } + return &BundleGenerator{ anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()), @@ -196,6 +204,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen anonymize: cfg.Anonymize, clientStatus: cfg.ClientStatus, includeSystemInfo: cfg.IncludeSystemInfo, + logFileCount: logFileCount, } } @@ -561,32 +570,8 @@ func (g *BundleGenerator) addLogfile() error { return fmt.Errorf("add client log file to zip: %w", err) } - // add latest rotated log file - pattern := filepath.Join(logDir, "client-*.log.gz") - files, err := filepath.Glob(pattern) - if err != nil { - log.Warnf("failed to glob rotated logs: %v", err) - } else if len(files) > 0 { - // pick the file with the latest ModTime - sort.Slice(files, func(i, j int) bool { - fi, err := os.Stat(files[i]) - if err != nil { - log.Warnf("failed to stat rotated log %s: %v", files[i], err) - return false - } - fj, err := os.Stat(files[j]) - if err != nil { - log.Warnf("failed to stat rotated log %s: %v", files[j], err) - return false - } - return fi.ModTime().Before(fj.ModTime()) - }) - latest := files[len(files)-1] - name := filepath.Base(latest) - if err := g.addSingleLogFileGz(latest, name); err != nil { - log.Warnf("failed to add rotated log %s: %v", name, err) - } - } + // add rotated log files based on logFileCount + g.addRotatedLogFiles(logDir) stdErrLogPath := filepath.Join(logDir, errorLogFile) stdoutLogPath := filepath.Join(logDir, stdoutLogFile) @@ -670,6 +655,52 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error { return nil } +// addRotatedLogFiles adds rotated log files to the bundle based on logFileCount +func (g *BundleGenerator) addRotatedLogFiles(logDir string) { + if g.logFileCount == 0 { + return + } + + pattern := filepath.Join(logDir, "client-*.log.gz") + files, err := filepath.Glob(pattern) + if err != nil { + log.Warnf("failed to glob rotated logs: %v", err) + return + } + + if len(files) == 0 { + return + } + + // sort files by modification time (newest first) + sort.Slice(files, func(i, j int) bool { + fi, err := os.Stat(files[i]) + if err != nil { + log.Warnf("failed to stat rotated log %s: %v", files[i], err) + return false + } + fj, err := os.Stat(files[j]) + if err != nil { + log.Warnf("failed to stat rotated log %s: %v", files[j], err) + return false + } + return fi.ModTime().After(fj.ModTime()) + }) + + // include up to logFileCount rotated files + maxFiles := int(g.logFileCount) + if maxFiles > len(files) { + maxFiles = len(files) + } + + for i := 0; i < maxFiles; i++ { + name := filepath.Base(files[i]) + if err := g.addSingleLogFileGz(files[i], name); err != nil { + log.Warnf("failed to add rotated log %s: %v", name, err) + } + } +} + func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error { header := &zip.FileHeader{ Name: filename, diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 202dc6f89..26e58d183 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -2290,6 +2290,7 @@ type DebugBundleRequest struct { Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` SystemInfo bool `protobuf:"varint,3,opt,name=systemInfo,proto3" json:"systemInfo,omitempty"` UploadURL string `protobuf:"bytes,4,opt,name=uploadURL,proto3" json:"uploadURL,omitempty"` + LogFileCount uint32 `protobuf:"varint,5,opt,name=logFileCount,proto3" json:"logFileCount,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2352,6 +2353,13 @@ func (x *DebugBundleRequest) GetUploadURL() string { return "" } +func (x *DebugBundleRequest) GetLogFileCount() uint32 { + if x != nil { + return x.LogFileCount + } + return 0 +} + type DebugBundleResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` @@ -3746,14 +3754,15 @@ const file_daemon_proto_rawDesc = "" + "\x12translatedHostname\x18\x04 \x01(\tR\x12translatedHostname\x128\n" + "\x0etranslatedPort\x18\x05 \x01(\v2\x10.daemon.PortInfoR\x0etranslatedPort\"G\n" + "\x17ForwardingRulesResponse\x12,\n" + - "\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\x88\x01\n" + + "\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\xac\x01\n" + "\x12DebugBundleRequest\x12\x1c\n" + "\tanonymize\x18\x01 \x01(\bR\tanonymize\x12\x16\n" + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1e\n" + "\n" + "systemInfo\x18\x03 \x01(\bR\n" + "systemInfo\x12\x1c\n" + - "\tuploadURL\x18\x04 \x01(\tR\tuploadURL\"}\n" + + "\tuploadURL\x18\x04 \x01(\tR\tuploadURL\x12\"\n" + + "\flogFileCount\x18\x05 \x01(\rR\flogFileCount\"}\n" + "\x13DebugBundleResponse\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12 \n" + "\vuploadedKey\x18\x02 \x01(\tR\vuploadedKey\x120\n" + diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index f488e69e7..462555c82 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -356,6 +356,7 @@ message DebugBundleRequest { string status = 2; bool systemInfo = 3; string uploadURL = 4; + uint32 logFileCount = 5; } message DebugBundleResponse { diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index e0612a6d1..6251f7c52 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -1,8 +1,4 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 -// source: daemon.proto package proto @@ -15,31 +11,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - DaemonService_Login_FullMethodName = "/daemon.DaemonService/Login" - DaemonService_WaitSSOLogin_FullMethodName = "/daemon.DaemonService/WaitSSOLogin" - DaemonService_Up_FullMethodName = "/daemon.DaemonService/Up" - DaemonService_Status_FullMethodName = "/daemon.DaemonService/Status" - DaemonService_Down_FullMethodName = "/daemon.DaemonService/Down" - DaemonService_GetConfig_FullMethodName = "/daemon.DaemonService/GetConfig" - DaemonService_ListNetworks_FullMethodName = "/daemon.DaemonService/ListNetworks" - DaemonService_SelectNetworks_FullMethodName = "/daemon.DaemonService/SelectNetworks" - DaemonService_DeselectNetworks_FullMethodName = "/daemon.DaemonService/DeselectNetworks" - DaemonService_ForwardingRules_FullMethodName = "/daemon.DaemonService/ForwardingRules" - DaemonService_DebugBundle_FullMethodName = "/daemon.DaemonService/DebugBundle" - DaemonService_GetLogLevel_FullMethodName = "/daemon.DaemonService/GetLogLevel" - DaemonService_SetLogLevel_FullMethodName = "/daemon.DaemonService/SetLogLevel" - DaemonService_ListStates_FullMethodName = "/daemon.DaemonService/ListStates" - DaemonService_CleanState_FullMethodName = "/daemon.DaemonService/CleanState" - DaemonService_DeleteState_FullMethodName = "/daemon.DaemonService/DeleteState" - DaemonService_SetNetworkMapPersistence_FullMethodName = "/daemon.DaemonService/SetNetworkMapPersistence" - DaemonService_TracePacket_FullMethodName = "/daemon.DaemonService/TracePacket" - DaemonService_SubscribeEvents_FullMethodName = "/daemon.DaemonService/SubscribeEvents" - DaemonService_GetEvents_FullMethodName = "/daemon.DaemonService/GetEvents" -) +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 // DaemonServiceClient is the client API for DaemonService service. // @@ -80,7 +53,7 @@ type DaemonServiceClient interface { // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) - SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SystemEvent], error) + SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) } @@ -93,9 +66,8 @@ func NewDaemonServiceClient(cc grpc.ClientConnInterface) DaemonServiceClient { } func (c *daemonServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(LoginResponse) - err := c.cc.Invoke(ctx, DaemonService_Login_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/Login", in, out, opts...) if err != nil { return nil, err } @@ -103,9 +75,8 @@ func (c *daemonServiceClient) Login(ctx context.Context, in *LoginRequest, opts } func (c *daemonServiceClient) WaitSSOLogin(ctx context.Context, in *WaitSSOLoginRequest, opts ...grpc.CallOption) (*WaitSSOLoginResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(WaitSSOLoginResponse) - err := c.cc.Invoke(ctx, DaemonService_WaitSSOLogin_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/WaitSSOLogin", in, out, opts...) if err != nil { return nil, err } @@ -113,9 +84,8 @@ func (c *daemonServiceClient) WaitSSOLogin(ctx context.Context, in *WaitSSOLogin } func (c *daemonServiceClient) Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UpResponse) - err := c.cc.Invoke(ctx, DaemonService_Up_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/Up", in, out, opts...) if err != nil { return nil, err } @@ -123,9 +93,8 @@ func (c *daemonServiceClient) Up(ctx context.Context, in *UpRequest, opts ...grp } func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StatusResponse) - err := c.cc.Invoke(ctx, DaemonService_Status_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/Status", in, out, opts...) if err != nil { return nil, err } @@ -133,9 +102,8 @@ func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opt } func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DownResponse) - err := c.cc.Invoke(ctx, DaemonService_Down_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/Down", in, out, opts...) if err != nil { return nil, err } @@ -143,9 +111,8 @@ func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts .. } func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetConfigResponse) - err := c.cc.Invoke(ctx, DaemonService_GetConfig_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetConfig", in, out, opts...) if err != nil { return nil, err } @@ -153,9 +120,8 @@ func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigReques } func (c *daemonServiceClient) ListNetworks(ctx context.Context, in *ListNetworksRequest, opts ...grpc.CallOption) (*ListNetworksResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListNetworksResponse) - err := c.cc.Invoke(ctx, DaemonService_ListNetworks_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListNetworks", in, out, opts...) if err != nil { return nil, err } @@ -163,9 +129,8 @@ func (c *daemonServiceClient) ListNetworks(ctx context.Context, in *ListNetworks } func (c *daemonServiceClient) SelectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SelectNetworksResponse) - err := c.cc.Invoke(ctx, DaemonService_SelectNetworks_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SelectNetworks", in, out, opts...) if err != nil { return nil, err } @@ -173,9 +138,8 @@ func (c *daemonServiceClient) SelectNetworks(ctx context.Context, in *SelectNetw } func (c *daemonServiceClient) DeselectNetworks(ctx context.Context, in *SelectNetworksRequest, opts ...grpc.CallOption) (*SelectNetworksResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SelectNetworksResponse) - err := c.cc.Invoke(ctx, DaemonService_DeselectNetworks_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeselectNetworks", in, out, opts...) if err != nil { return nil, err } @@ -183,9 +147,8 @@ func (c *daemonServiceClient) DeselectNetworks(ctx context.Context, in *SelectNe } func (c *daemonServiceClient) ForwardingRules(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*ForwardingRulesResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ForwardingRulesResponse) - err := c.cc.Invoke(ctx, DaemonService_ForwardingRules_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/ForwardingRules", in, out, opts...) if err != nil { return nil, err } @@ -193,9 +156,8 @@ func (c *daemonServiceClient) ForwardingRules(ctx context.Context, in *EmptyRequ } func (c *daemonServiceClient) DebugBundle(ctx context.Context, in *DebugBundleRequest, opts ...grpc.CallOption) (*DebugBundleResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DebugBundleResponse) - err := c.cc.Invoke(ctx, DaemonService_DebugBundle_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/DebugBundle", in, out, opts...) if err != nil { return nil, err } @@ -203,9 +165,8 @@ func (c *daemonServiceClient) DebugBundle(ctx context.Context, in *DebugBundleRe } func (c *daemonServiceClient) GetLogLevel(ctx context.Context, in *GetLogLevelRequest, opts ...grpc.CallOption) (*GetLogLevelResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetLogLevelResponse) - err := c.cc.Invoke(ctx, DaemonService_GetLogLevel_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetLogLevel", in, out, opts...) if err != nil { return nil, err } @@ -213,9 +174,8 @@ func (c *daemonServiceClient) GetLogLevel(ctx context.Context, in *GetLogLevelRe } func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRequest, opts ...grpc.CallOption) (*SetLogLevelResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SetLogLevelResponse) - err := c.cc.Invoke(ctx, DaemonService_SetLogLevel_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetLogLevel", in, out, opts...) if err != nil { return nil, err } @@ -223,9 +183,8 @@ func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRe } func (c *daemonServiceClient) ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListStatesResponse) - err := c.cc.Invoke(ctx, DaemonService_ListStates_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListStates", in, out, opts...) if err != nil { return nil, err } @@ -233,9 +192,8 @@ func (c *daemonServiceClient) ListStates(ctx context.Context, in *ListStatesRequ } func (c *daemonServiceClient) CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CleanStateResponse) - err := c.cc.Invoke(ctx, DaemonService_CleanState_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/CleanState", in, out, opts...) if err != nil { return nil, err } @@ -243,9 +201,8 @@ func (c *daemonServiceClient) CleanState(ctx context.Context, in *CleanStateRequ } func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteStateResponse) - err := c.cc.Invoke(ctx, DaemonService_DeleteState_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeleteState", in, out, opts...) if err != nil { return nil, err } @@ -253,9 +210,8 @@ func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRe } func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SetNetworkMapPersistenceResponse) - err := c.cc.Invoke(ctx, DaemonService_SetNetworkMapPersistence_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetNetworkMapPersistence", in, out, opts...) if err != nil { return nil, err } @@ -263,22 +219,20 @@ func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in * } func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(TracePacketResponse) - err := c.cc.Invoke(ctx, DaemonService_TracePacket_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/TracePacket", in, out, opts...) if err != nil { return nil, err } return out, nil } -func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SystemEvent], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], DaemonService_SubscribeEvents_FullMethodName, cOpts...) +func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) { + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeEvents", opts...) if err != nil { return nil, err } - x := &grpc.GenericClientStream[SubscribeRequest, SystemEvent]{ClientStream: stream} + x := &daemonServiceSubscribeEventsClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -288,13 +242,26 @@ func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *Subscribe return x, nil } -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type DaemonService_SubscribeEventsClient = grpc.ServerStreamingClient[SystemEvent] +type DaemonService_SubscribeEventsClient interface { + Recv() (*SystemEvent, error) + grpc.ClientStream +} + +type daemonServiceSubscribeEventsClient struct { + grpc.ClientStream +} + +func (x *daemonServiceSubscribeEventsClient) Recv() (*SystemEvent, error) { + m := new(SystemEvent) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetEventsResponse) - err := c.cc.Invoke(ctx, DaemonService_GetEvents_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetEvents", in, out, opts...) if err != nil { return nil, err } @@ -303,7 +270,7 @@ func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsReques // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer -// for forward compatibility. +// for forward compatibility type DaemonServiceServer interface { // Login uses setup key to prepare configuration for the daemon. Login(context.Context, *LoginRequest) (*LoginResponse, error) @@ -340,17 +307,14 @@ type DaemonServiceServer interface { // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) - SubscribeEvents(*SubscribeRequest, grpc.ServerStreamingServer[SystemEvent]) error + SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) mustEmbedUnimplementedDaemonServiceServer() } -// UnimplementedDaemonServiceServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedDaemonServiceServer struct{} +// UnimplementedDaemonServiceServer must be embedded to have forward compatible implementations. +type UnimplementedDaemonServiceServer struct { +} func (UnimplementedDaemonServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") @@ -406,14 +370,13 @@ func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented") } -func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, grpc.ServerStreamingServer[SystemEvent]) error { +func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error { return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented") } func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented") } func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} -func (UnimplementedDaemonServiceServer) testEmbeddedByValue() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to DaemonServiceServer will @@ -423,13 +386,6 @@ type UnsafeDaemonServiceServer interface { } func RegisterDaemonServiceServer(s grpc.ServiceRegistrar, srv DaemonServiceServer) { - // If the following call pancis, it indicates UnimplementedDaemonServiceServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } s.RegisterService(&DaemonService_ServiceDesc, srv) } @@ -443,7 +399,7 @@ func _DaemonService_Login_Handler(srv interface{}, ctx context.Context, dec func } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_Login_FullMethodName, + FullMethod: "/daemon.DaemonService/Login", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Login(ctx, req.(*LoginRequest)) @@ -461,7 +417,7 @@ func _DaemonService_WaitSSOLogin_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_WaitSSOLogin_FullMethodName, + FullMethod: "/daemon.DaemonService/WaitSSOLogin", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).WaitSSOLogin(ctx, req.(*WaitSSOLoginRequest)) @@ -479,7 +435,7 @@ func _DaemonService_Up_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_Up_FullMethodName, + FullMethod: "/daemon.DaemonService/Up", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Up(ctx, req.(*UpRequest)) @@ -497,7 +453,7 @@ func _DaemonService_Status_Handler(srv interface{}, ctx context.Context, dec fun } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_Status_FullMethodName, + FullMethod: "/daemon.DaemonService/Status", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Status(ctx, req.(*StatusRequest)) @@ -515,7 +471,7 @@ func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_Down_FullMethodName, + FullMethod: "/daemon.DaemonService/Down", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).Down(ctx, req.(*DownRequest)) @@ -533,7 +489,7 @@ func _DaemonService_GetConfig_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_GetConfig_FullMethodName, + FullMethod: "/daemon.DaemonService/GetConfig", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).GetConfig(ctx, req.(*GetConfigRequest)) @@ -551,7 +507,7 @@ func _DaemonService_ListNetworks_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_ListNetworks_FullMethodName, + FullMethod: "/daemon.DaemonService/ListNetworks", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).ListNetworks(ctx, req.(*ListNetworksRequest)) @@ -569,7 +525,7 @@ func _DaemonService_SelectNetworks_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_SelectNetworks_FullMethodName, + FullMethod: "/daemon.DaemonService/SelectNetworks", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).SelectNetworks(ctx, req.(*SelectNetworksRequest)) @@ -587,7 +543,7 @@ func _DaemonService_DeselectNetworks_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_DeselectNetworks_FullMethodName, + FullMethod: "/daemon.DaemonService/DeselectNetworks", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).DeselectNetworks(ctx, req.(*SelectNetworksRequest)) @@ -605,7 +561,7 @@ func _DaemonService_ForwardingRules_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_ForwardingRules_FullMethodName, + FullMethod: "/daemon.DaemonService/ForwardingRules", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).ForwardingRules(ctx, req.(*EmptyRequest)) @@ -623,7 +579,7 @@ func _DaemonService_DebugBundle_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_DebugBundle_FullMethodName, + FullMethod: "/daemon.DaemonService/DebugBundle", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).DebugBundle(ctx, req.(*DebugBundleRequest)) @@ -641,7 +597,7 @@ func _DaemonService_GetLogLevel_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_GetLogLevel_FullMethodName, + FullMethod: "/daemon.DaemonService/GetLogLevel", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).GetLogLevel(ctx, req.(*GetLogLevelRequest)) @@ -659,7 +615,7 @@ func _DaemonService_SetLogLevel_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_SetLogLevel_FullMethodName, + FullMethod: "/daemon.DaemonService/SetLogLevel", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).SetLogLevel(ctx, req.(*SetLogLevelRequest)) @@ -677,7 +633,7 @@ func _DaemonService_ListStates_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_ListStates_FullMethodName, + FullMethod: "/daemon.DaemonService/ListStates", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).ListStates(ctx, req.(*ListStatesRequest)) @@ -695,7 +651,7 @@ func _DaemonService_CleanState_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_CleanState_FullMethodName, + FullMethod: "/daemon.DaemonService/CleanState", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).CleanState(ctx, req.(*CleanStateRequest)) @@ -713,7 +669,7 @@ func _DaemonService_DeleteState_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_DeleteState_FullMethodName, + FullMethod: "/daemon.DaemonService/DeleteState", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).DeleteState(ctx, req.(*DeleteStateRequest)) @@ -731,7 +687,7 @@ func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_SetNetworkMapPersistence_FullMethodName, + FullMethod: "/daemon.DaemonService/SetNetworkMapPersistence", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, req.(*SetNetworkMapPersistenceRequest)) @@ -749,7 +705,7 @@ func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_TracePacket_FullMethodName, + FullMethod: "/daemon.DaemonService/TracePacket", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).TracePacket(ctx, req.(*TracePacketRequest)) @@ -762,11 +718,21 @@ func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerS if err := stream.RecvMsg(m); err != nil { return err } - return srv.(DaemonServiceServer).SubscribeEvents(m, &grpc.GenericServerStream[SubscribeRequest, SystemEvent]{ServerStream: stream}) + return srv.(DaemonServiceServer).SubscribeEvents(m, &daemonServiceSubscribeEventsServer{stream}) } -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type DaemonService_SubscribeEventsServer = grpc.ServerStreamingServer[SystemEvent] +type DaemonService_SubscribeEventsServer interface { + Send(*SystemEvent) error + grpc.ServerStream +} + +type daemonServiceSubscribeEventsServer struct { + grpc.ServerStream +} + +func (x *daemonServiceSubscribeEventsServer) Send(m *SystemEvent) error { + return x.ServerStream.SendMsg(m) +} func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetEventsRequest) @@ -778,7 +744,7 @@ func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: DaemonService_GetEvents_FullMethodName, + FullMethod: "/daemon.DaemonService/GetEvents", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServiceServer).GetEvents(ctx, req.(*GetEventsRequest)) diff --git a/client/proto/generate.sh b/client/proto/generate.sh index 52fe23d7f..f9a2c3750 100755 --- a/client/proto/generate.sh +++ b/client/proto/generate.sh @@ -11,7 +11,7 @@ fi old_pwd=$(pwd) script_path=$(dirname $(realpath "$0")) cd "$script_path" -go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.6 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 protoc -I ./ ./daemon.proto --go_out=../ --go-grpc_out=../ --experimental_allow_proto3_optional cd "$old_pwd" \ No newline at end of file diff --git a/client/server/debug.go b/client/server/debug.go index 7de3e8609..412602b00 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -42,6 +42,7 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( Anonymize: req.GetAnonymize(), ClientStatus: req.GetStatus(), IncludeSystemInfo: req.GetSystemInfo(), + LogFileCount: req.GetLogFileCount(), }, ) diff --git a/go.mod b/go.mod index a12058278..4a9727373 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.36.5 + google.golang.org/protobuf v1.36.6 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) diff --git a/go.sum b/go.sum index 6ce503dd1..a622f203f 100644 --- a/go.sum +++ b/go.sum @@ -1164,8 +1164,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/util/log.go b/util/log.go index 59a064366..53d2b0684 100644 --- a/util/log.go +++ b/util/log.go @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/formatter" ) -const defaultLogSize = 5 +const defaultLogSize = 15 // InitLog parses and sets log-level input func InitLog(logLevel string, logPath string) error { From 2b9f3319803e74b81f16b5216ab99e70acad24ea Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:29:10 +0100 Subject: [PATCH 146/213] always suffix ephemeral peer name (#4138) --- management/server/peer.go | 43 +++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index a60513b38..21a9579fc 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -236,11 +236,23 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user if peer.Name != update.Name { var newLabel string - newLabel, err = getPeerIPDNSLabel(ctx, transaction, peer.IP, accountID, update.Name) + + newLabel, err = nbdns.GetParsedDomainLabel(update.Name) if err != nil { - return fmt.Errorf("failed to get free DNS label: %w", err) + newLabel = "" + } else { + _, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, update.Name) + if err == nil { + newLabel = "" + } } + if newLabel == "" { + newLabel, err = getPeerIPDNSLabel(peer.IP, update.Name) + if err != nil { + return fmt.Errorf("failed to get free DNS label: %w", err) + } + } peer.Name = update.Name peer.DNSLabel = newLabel peerLabelChanged = true @@ -472,6 +484,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s var groupsToAdd []string var allowExtraDNSLabels bool var accountID string + var isEphemeral bool if addedByUser { user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { @@ -501,7 +514,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s setupKeyName = sk.Name allowExtraDNSLabels = sk.AllowExtraDNSLabels accountID = sk.AccountID - + isEphemeral = sk.Ephemeral if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 { return nil, nil, nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels") } @@ -573,11 +586,17 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s } var freeLabel string - freeLabel, err = getPeerIPDNSLabel(ctx, am.Store, freeIP, accountID, peer.Meta.Hostname) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) + if isEphemeral || attempt > 1 { + freeLabel, err = getPeerIPDNSLabel(freeIP, peer.Meta.Hostname) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) + } + } else { + freeLabel, err = nbdns.GetParsedDomainLabel(peer.Meta.Hostname) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) + } } - newPeer.DNSLabel = freeLabel newPeer.IP = freeIP @@ -647,7 +666,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s if isUniqueConstraintError(err) { unlock() unlock = nil - log.WithContext(ctx).Debugf("Failed to add peer in attempt %d, retrying: %v", attempt, err) + log.WithContext(ctx).WithFields(log.Fields{"dns_label": freeLabel, "ip": freeIP}).Tracef("Failed to add peer in attempt %d, retrying: %v", attempt, err) continue } @@ -681,7 +700,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s return am.getValidatedPeerWithMap(ctx, false, accountID, newPeer) } -func getPeerIPDNSLabel(ctx context.Context, tx store.Store, ip net.IP, accountID, peerHostName string) (string, error) { +func getPeerIPDNSLabel(ip net.IP, peerHostName string) (string, error) { ip = ip.To4() dnsName, err := nbdns.GetParsedDomainLabel(peerHostName) @@ -689,12 +708,6 @@ func getPeerIPDNSLabel(ctx context.Context, tx store.Store, ip net.IP, accountID return "", fmt.Errorf("failed to parse peer host name %s: %w", peerHostName, err) } - _, err = tx.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, dnsName) - if err != nil { - //nolint:nilerr - return dnsName, nil - } - return fmt.Sprintf("%s-%d-%d", dnsName, ip[2], ip[3]), nil } From a76c8eafb46cda5e44de0f3784160a34e8cdb4a2 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:37:14 +0200 Subject: [PATCH 147/213] [management] sync calls to UpdateAccountPeers from BufferUpdateAccountPeers (#4137) --------- Co-authored-by: Maycon Santos Co-authored-by: Pedro Costa <550684+pnmcosta@users.noreply.github.com> --- management/server/mock_server/account_mock.go | 10 +- management/server/peer.go | 34 ++++- management/server/peer_test.go | 130 ++++++++++++++++++ 3 files changed, 166 insertions(+), 8 deletions(-) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 4004f1b57..b1ec66286 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -120,14 +120,20 @@ type MockAccountManager struct { GetAccountOnboardingFunc func(ctx context.Context, accountID, userID string) (*types.AccountOnboarding, error) UpdateAccountOnboardingFunc func(ctx context.Context, accountID, userID string, onboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) GetOrCreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) + UpdateAccountPeersFunc func(ctx context.Context, accountID string) + BufferUpdateAccountPeersFunc func(ctx context.Context, accountID string) } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { - // do nothing + if am.UpdateAccountPeersFunc != nil { + am.UpdateAccountPeersFunc(ctx, accountID) + } } func (am *MockAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) { - // do nothing + if am.BufferUpdateAccountPeersFunc != nil { + am.BufferUpdateAccountPeersFunc(ctx, accountID) + } } func (am *MockAccountManager) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error { diff --git a/management/server/peer.go b/management/server/peer.go index 21a9579fc..c6ade83c0 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -9,6 +9,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "time" "github.com/rs/xid" @@ -1280,18 +1281,39 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } } -func (am *DefaultAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) { - mu, _ := am.accountUpdateLocks.LoadOrStore(accountID, &sync.Mutex{}) - lock := mu.(*sync.Mutex) +type bufferUpdate struct { + mu sync.Mutex + next *time.Timer + update atomic.Bool +} - if !lock.TryLock() { +func (am *DefaultAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) { + bufUpd, _ := am.accountUpdateLocks.LoadOrStore(accountID, &bufferUpdate{}) + b := bufUpd.(*bufferUpdate) + + if !b.mu.TryLock() { + b.update.Store(true) return } + if b.next != nil { + b.next.Stop() + } + go func() { - time.Sleep(time.Duration(am.updateAccountPeersBufferInterval.Load())) - lock.Unlock() + defer b.mu.Unlock() am.UpdateAccountPeers(ctx, accountID) + if !b.update.Load() { + return + } + b.update.Store(false) + if b.next == nil { + b.next = time.AfterFunc(time.Duration(am.updateAccountPeersBufferInterval.Load()), func() { + am.UpdateAccountPeers(ctx, accountID) + }) + return + } + b.next.Reset(time.Duration(am.updateAccountPeersBufferInterval.Load())) }() } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 07ec5037b..d41020514 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "testing" "time" @@ -25,6 +26,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" + "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/status" @@ -2251,3 +2253,131 @@ func Test_AddPeer(t *testing.T) { assert.Equal(t, totalPeers, maps.Values(account.SetupKeys)[0].UsedTimes) assert.Equal(t, uint64(totalPeers), account.Network.Serial) } + +func TestBufferUpdateAccountPeers(t *testing.T) { + const ( + peersCount = 1000 + updateAccountInterval = 50 * time.Millisecond + ) + + var ( + deletedPeers, updatePeersDeleted, updatePeersRuns atomic.Int32 + uapLastRun, dpLastRun atomic.Int64 + + totalNewRuns, totalOldRuns int + ) + + uap := func(ctx context.Context, accountID string) { + updatePeersDeleted.Store(deletedPeers.Load()) + updatePeersRuns.Add(1) + uapLastRun.Store(time.Now().UnixMilli()) + time.Sleep(100 * time.Millisecond) + } + + t.Run("new approach", func(t *testing.T) { + updatePeersRuns.Store(0) + updatePeersDeleted.Store(0) + deletedPeers.Store(0) + + var mustore sync.Map + bufupd := func(ctx context.Context, accountID string) { + mu, _ := mustore.LoadOrStore(accountID, &bufferUpdate{}) + b := mu.(*bufferUpdate) + + if !b.mu.TryLock() { + b.update.Store(true) + return + } + + if b.next != nil { + b.next.Stop() + } + + go func() { + defer b.mu.Unlock() + uap(ctx, accountID) + if !b.update.Load() { + return + } + b.update.Store(false) + b.next = time.AfterFunc(updateAccountInterval, func() { + uap(ctx, accountID) + }) + }() + } + dp := func(ctx context.Context, accountID, peerID, userID string) error { + deletedPeers.Add(1) + dpLastRun.Store(time.Now().UnixMilli()) + time.Sleep(10 * time.Millisecond) + bufupd(ctx, accountID) + return nil + } + + am := mock_server.MockAccountManager{ + UpdateAccountPeersFunc: uap, + BufferUpdateAccountPeersFunc: bufupd, + DeletePeerFunc: dp, + } + empty := "" + for range peersCount { + //nolint + am.DeletePeer(context.Background(), empty, empty, empty) + } + time.Sleep(100 * time.Millisecond) + + assert.Equal(t, peersCount, int(deletedPeers.Load()), "Expected all peers to be deleted") + assert.Equal(t, peersCount, int(updatePeersDeleted.Load()), "Expected all peers to be updated in the buffer") + assert.GreaterOrEqual(t, uapLastRun.Load(), dpLastRun.Load(), "Expected update account peers to run after delete peer") + + totalNewRuns = int(updatePeersRuns.Load()) + }) + + t.Run("old approach", func(t *testing.T) { + updatePeersRuns.Store(0) + updatePeersDeleted.Store(0) + deletedPeers.Store(0) + + var mustore sync.Map + bufupd := func(ctx context.Context, accountID string) { + mu, _ := mustore.LoadOrStore(accountID, &sync.Mutex{}) + b := mu.(*sync.Mutex) + + if !b.TryLock() { + return + } + + go func() { + time.Sleep(updateAccountInterval) + b.Unlock() + uap(ctx, accountID) + }() + } + dp := func(ctx context.Context, accountID, peerID, userID string) error { + deletedPeers.Add(1) + dpLastRun.Store(time.Now().UnixMilli()) + time.Sleep(10 * time.Millisecond) + bufupd(ctx, accountID) + return nil + } + + am := mock_server.MockAccountManager{ + UpdateAccountPeersFunc: uap, + BufferUpdateAccountPeersFunc: bufupd, + DeletePeerFunc: dp, + } + empty := "" + for range peersCount { + //nolint + am.DeletePeer(context.Background(), empty, empty, empty) + } + time.Sleep(100 * time.Millisecond) + + assert.Equal(t, peersCount, int(deletedPeers.Load()), "Expected all peers to be deleted") + assert.Equal(t, peersCount, int(updatePeersDeleted.Load()), "Expected all peers to be updated in the buffer") + assert.GreaterOrEqual(t, uapLastRun.Load(), dpLastRun.Load(), "Expected update account peers to run after delete peer") + + totalOldRuns = int(updatePeersRuns.Load()) + }) + assert.Less(t, totalNewRuns, totalOldRuns, "Expected new approach to run less than old approach. New runs: %d, Old runs: %d", totalNewRuns, totalOldRuns) + t.Logf("New runs: %d, Old runs: %d", totalNewRuns, totalOldRuns) +} From 3e6eede1523f5073cd3d35f068c771b6d88ba397 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 12 Jul 2025 11:10:45 +0200 Subject: [PATCH 148/213] [client] Fix elapsed time calculation when machine is in sleep mode (#4140) --- client/iface/bind/activity.go | 24 ++++++++++--------- client/iface/bind/activity_test.go | 8 +++---- client/iface/configurer/kernel_unix.go | 4 +++- client/iface/configurer/usp.go | 3 ++- client/iface/device/interface.go | 3 ++- client/iface/iface.go | 3 ++- client/internal/conn_mgr.go | 1 - client/internal/engine_test.go | 5 ++-- client/internal/iface_common.go | 3 ++- client/internal/lazyconn/activity/listener.go | 7 +++--- .../internal/lazyconn/inactivity/manager.go | 9 ++++--- .../lazyconn/inactivity/manager_test.go | 13 +++++----- client/internal/lazyconn/manager/manager.go | 7 +++--- client/internal/lazyconn/wgiface.go | 4 +++- monotime/time.go | 10 ++++++-- 15 files changed, 62 insertions(+), 42 deletions(-) diff --git a/client/iface/bind/activity.go b/client/iface/bind/activity.go index d3b406bcd..57862e3d1 100644 --- a/client/iface/bind/activity.go +++ b/client/iface/bind/activity.go @@ -34,14 +34,14 @@ func NewActivityRecorder() *ActivityRecorder { } // GetLastActivities returns a snapshot of peer last activity -func (r *ActivityRecorder) GetLastActivities() map[string]time.Time { +func (r *ActivityRecorder) GetLastActivities() map[string]monotime.Time { r.mu.RLock() defer r.mu.RUnlock() - activities := make(map[string]time.Time, len(r.peers)) + activities := make(map[string]monotime.Time, len(r.peers)) for key, record := range r.peers { - unixNano := record.LastActivity.Load() - activities[key] = time.Unix(0, unixNano) + monoTime := record.LastActivity.Load() + activities[key] = monotime.Time(monoTime) } return activities } @@ -51,18 +51,20 @@ func (r *ActivityRecorder) UpsertAddress(publicKey string, address netip.AddrPor r.mu.Lock() defer r.mu.Unlock() - if pr, exists := r.peers[publicKey]; exists { - delete(r.addrToPeer, pr.Address) - pr.Address = address + var record *PeerRecord + record, exists := r.peers[publicKey] + if exists { + delete(r.addrToPeer, record.Address) + record.Address = address } else { - record := &PeerRecord{ + record = &PeerRecord{ Address: address, } - record.LastActivity.Store(monotime.Now()) + record.LastActivity.Store(int64(monotime.Now())) r.peers[publicKey] = record } - r.addrToPeer[address] = r.peers[publicKey] + r.addrToPeer[address] = record } func (r *ActivityRecorder) Remove(publicKey string) { @@ -84,7 +86,7 @@ func (r *ActivityRecorder) record(address netip.AddrPort) { return } - now := monotime.Now() + now := int64(monotime.Now()) last := record.LastActivity.Load() if now-last < saveFrequency { return diff --git a/client/iface/bind/activity_test.go b/client/iface/bind/activity_test.go index 598607b95..bdd0dca29 100644 --- a/client/iface/bind/activity_test.go +++ b/client/iface/bind/activity_test.go @@ -4,6 +4,8 @@ import ( "net/netip" "testing" "time" + + "github.com/netbirdio/netbird/monotime" ) func TestActivityRecorder_GetLastActivities(t *testing.T) { @@ -17,11 +19,7 @@ func TestActivityRecorder_GetLastActivities(t *testing.T) { t.Fatalf("Expected activity for peer %s, but got none", peer) } - if p.IsZero() { - t.Fatalf("Expected activity for peer %s, but got zero", peer) - } - - if p.Before(time.Now().Add(-2 * time.Minute)) { + if monotime.Since(p) > 5*time.Second { t.Fatalf("Expected activity for peer %s to be recent, but got %v", peer, p) } } diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go index e2ea19144..84afc38f5 100644 --- a/client/iface/configurer/kernel_unix.go +++ b/client/iface/configurer/kernel_unix.go @@ -11,6 +11,8 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/netbirdio/netbird/monotime" ) var zeroKey wgtypes.Key @@ -277,6 +279,6 @@ func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) { return stats, nil } -func (c *KernelConfigurer) LastActivities() map[string]time.Time { +func (c *KernelConfigurer) LastActivities() map[string]monotime.Time { return nil } diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 6ead716f1..1ff4d839c 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -17,6 +17,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" + "github.com/netbirdio/netbird/monotime" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -223,7 +224,7 @@ func (c *WGUSPConfigurer) FullStats() (*Stats, error) { return parseStatus(c.deviceName, ipcStr) } -func (c *WGUSPConfigurer) LastActivities() map[string]time.Time { +func (c *WGUSPConfigurer) LastActivities() map[string]monotime.Time { return c.activityRecorder.GetLastActivities() } diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go index d68e6bf90..1f40b0d46 100644 --- a/client/iface/device/interface.go +++ b/client/iface/device/interface.go @@ -8,6 +8,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/configurer" + "github.com/netbirdio/netbird/monotime" ) type WGConfigurer interface { @@ -19,5 +20,5 @@ type WGConfigurer interface { Close() GetStats() (map[string]configurer.WGStats, error) FullStats() (*configurer.Stats, error) - LastActivities() map[string]time.Time + LastActivities() map[string]monotime.Time } diff --git a/client/iface/iface.go b/client/iface/iface.go index e90c3536b..0e41f8e64 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -21,6 +21,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/iface/wgproxy" + "github.com/netbirdio/netbird/monotime" ) const ( @@ -237,7 +238,7 @@ func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) { return w.configurer.GetStats() } -func (w *WGIface) LastActivities() map[string]time.Time { +func (w *WGIface) LastActivities() map[string]monotime.Time { w.mu.Lock() defer w.mu.Unlock() diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go index c76b0a99f..112559132 100644 --- a/client/internal/conn_mgr.go +++ b/client/internal/conn_mgr.go @@ -226,7 +226,6 @@ func (e *ConnMgr) ActivatePeer(ctx context.Context, conn *peer.Conn) { } if found := e.lazyConnMgr.ActivatePeer(conn.GetKey()); found { - conn.Log.Infof("activated peer from inactive state") if err := conn.Open(ctx); err != nil { conn.Log.Errorf("failed to open connection: %v", err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index f4ed8f1c0..4b7a2d600 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -52,6 +52,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/monotime" relayClient "github.com/netbirdio/netbird/relay/client" "github.com/netbirdio/netbird/route" signal "github.com/netbirdio/netbird/signal/client" @@ -96,7 +97,7 @@ type MockWGIface struct { GetInterfaceGUIDStringFunc func() (string, error) GetProxyFunc func() wgproxy.Proxy GetNetFunc func() *netstack.Net - LastActivitiesFunc func() map[string]time.Time + LastActivitiesFunc func() map[string]monotime.Time } func (m *MockWGIface) FullStats() (*configurer.Stats, error) { @@ -187,7 +188,7 @@ func (m *MockWGIface) GetNet() *netstack.Net { return m.GetNetFunc() } -func (m *MockWGIface) LastActivities() map[string]time.Time { +func (m *MockWGIface) LastActivities() map[string]monotime.Time { if m.LastActivitiesFunc != nil { return m.LastActivitiesFunc() } diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index 38fb3561e..bf96153ea 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -14,6 +14,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/iface/wgproxy" + "github.com/netbirdio/netbird/monotime" ) type wgIfaceBase interface { @@ -38,5 +39,5 @@ type wgIfaceBase interface { GetStats() (map[string]configurer.WGStats, error) GetNet() *netstack.Net FullStats() (*configurer.Stats, error) - LastActivities() map[string]time.Time + LastActivities() map[string]monotime.Time } diff --git a/client/internal/lazyconn/activity/listener.go b/client/internal/lazyconn/activity/listener.go index 81b5da17b..817ff00c3 100644 --- a/client/internal/lazyconn/activity/listener.go +++ b/client/internal/lazyconn/activity/listener.go @@ -48,7 +48,7 @@ func (d *Listener) ReadPackets() { n, remoteAddr, err := d.conn.ReadFromUDP(make([]byte, 1)) if err != nil { if d.isClosed.Load() { - d.peerCfg.Log.Debugf("exit from activity listener") + d.peerCfg.Log.Infof("exit from activity listener") } else { d.peerCfg.Log.Errorf("failed to read from activity listener: %s", err) } @@ -59,9 +59,11 @@ func (d *Listener) ReadPackets() { d.peerCfg.Log.Warnf("received %d bytes from %s, too short", n, remoteAddr) continue } + d.peerCfg.Log.Infof("activity detected") break } + d.peerCfg.Log.Debugf("removing lazy endpoint: %s", d.endpoint.String()) if err := d.removeEndpoint(); err != nil { d.peerCfg.Log.Errorf("failed to remove endpoint: %s", err) } @@ -71,7 +73,7 @@ func (d *Listener) ReadPackets() { } func (d *Listener) Close() { - d.peerCfg.Log.Infof("closing listener: %s", d.conn.LocalAddr().String()) + d.peerCfg.Log.Infof("closing activity listener: %s", d.conn.LocalAddr().String()) d.isClosed.Store(true) if err := d.conn.Close(); err != nil { @@ -81,7 +83,6 @@ func (d *Listener) Close() { } func (d *Listener) removeEndpoint() error { - d.peerCfg.Log.Debugf("removing lazy endpoint: %s", d.endpoint.String()) return d.wgIface.RemovePeer(d.peerCfg.PublicKey) } diff --git a/client/internal/lazyconn/inactivity/manager.go b/client/internal/lazyconn/inactivity/manager.go index 854951729..0120f4430 100644 --- a/client/internal/lazyconn/inactivity/manager.go +++ b/client/internal/lazyconn/inactivity/manager.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/lazyconn" + "github.com/netbirdio/netbird/monotime" ) const ( @@ -18,7 +19,7 @@ const ( ) type WgInterface interface { - LastActivities() map[string]time.Time + LastActivities() map[string]monotime.Time } type Manager struct { @@ -124,6 +125,7 @@ func (m *Manager) checkStats() (map[string]struct{}, error) { idlePeers := make(map[string]struct{}) + checkTime := time.Now() for peerID, peerCfg := range m.interestedPeers { lastActive, ok := lastActivities[peerID] if !ok { @@ -132,8 +134,9 @@ func (m *Manager) checkStats() (map[string]struct{}, error) { continue } - if time.Since(lastActive) > m.inactivityThreshold { - peerCfg.Log.Infof("peer is inactive since: %v", lastActive) + since := monotime.Since(lastActive) + if since > m.inactivityThreshold { + peerCfg.Log.Infof("peer is inactive since time: %s", checkTime.Add(-since).String()) idlePeers[peerID] = struct{}{} } } diff --git a/client/internal/lazyconn/inactivity/manager_test.go b/client/internal/lazyconn/inactivity/manager_test.go index d012b41a2..10b4ef1eb 100644 --- a/client/internal/lazyconn/inactivity/manager_test.go +++ b/client/internal/lazyconn/inactivity/manager_test.go @@ -9,13 +9,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/client/internal/lazyconn" + "github.com/netbirdio/netbird/monotime" ) type mockWgInterface struct { - lastActivities map[string]time.Time + lastActivities map[string]monotime.Time } -func (m *mockWgInterface) LastActivities() map[string]time.Time { +func (m *mockWgInterface) LastActivities() map[string]monotime.Time { return m.lastActivities } @@ -23,8 +24,8 @@ func TestPeerTriggersInactivity(t *testing.T) { peerID := "peer1" wgMock := &mockWgInterface{ - lastActivities: map[string]time.Time{ - peerID: time.Now().Add(-20 * time.Minute), + lastActivities: map[string]monotime.Time{ + peerID: monotime.Time(int64(monotime.Now()) - int64(20*time.Minute)), }, } @@ -64,8 +65,8 @@ func TestPeerTriggersActivity(t *testing.T) { peerID := "peer1" wgMock := &mockWgInterface{ - lastActivities: map[string]time.Time{ - peerID: time.Now().Add(-5 * time.Minute), + lastActivities: map[string]monotime.Time{ + peerID: monotime.Time(int64(monotime.Now()) - int64(5*time.Minute)), }, } diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go index 416e4e7e7..b6b3c6091 100644 --- a/client/internal/lazyconn/manager/manager.go +++ b/client/internal/lazyconn/manager/manager.go @@ -258,12 +258,13 @@ func (m *Manager) ActivatePeer(peerID string) (found bool) { return false } + cfg.Log.Infof("activate peer from inactive state by remote signal message") + if !m.activateSinglePeer(cfg, mp) { return false } m.activateHAGroupPeers(cfg) - return true } @@ -571,12 +572,12 @@ func (m *Manager) onPeerInactivityTimedOut(peerIDs map[string]struct{}) { // this is blocking operation, potentially can be optimized m.peerStore.PeerConnIdle(mp.peerCfg.PublicKey) - mp.peerCfg.Log.Infof("start activity monitor") - mp.expectedWatcher = watcherActivity m.inactivityManager.RemovePeer(mp.peerCfg.PublicKey) + mp.peerCfg.Log.Infof("start activity monitor") + if err := m.activityManager.MonitorPeerActivity(*mp.peerCfg); err != nil { mp.peerCfg.Log.Errorf("failed to create activity monitor: %v", err) continue diff --git a/client/internal/lazyconn/wgiface.go b/client/internal/lazyconn/wgiface.go index d55ff9670..0351904f7 100644 --- a/client/internal/lazyconn/wgiface.go +++ b/client/internal/lazyconn/wgiface.go @@ -6,11 +6,13 @@ import ( "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/netbirdio/netbird/monotime" ) type WGIface interface { RemovePeer(peerKey string) error UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error IsUserspaceBind() bool - LastActivities() map[string]time.Time + LastActivities() map[string]monotime.Time } diff --git a/monotime/time.go b/monotime/time.go index 6032fb60b..ba45b6659 100644 --- a/monotime/time.go +++ b/monotime/time.go @@ -9,6 +9,8 @@ var ( baseWallNano int64 ) +type Time int64 + func init() { baseWallTime = time.Now() baseWallNano = baseWallTime.UnixNano() @@ -23,7 +25,11 @@ func init() { // and using time.Since() for elapsed calculation, this avoids repeated // time.Now() calls and leverages Go's internal monotonic clock for // efficient duration measurement. -func Now() int64 { +func Now() Time { elapsed := time.Since(baseWallTime) - return baseWallNano + int64(elapsed) + return Time(baseWallNano + int64(elapsed)) +} + +func Since(t Time) time.Duration { + return time.Duration(Now() - t) } From e49bcc343d30ef6c491782c45bea81c372bcf0e4 Mon Sep 17 00:00:00 2001 From: iisteev Date: Sun, 13 Jul 2025 15:42:48 +0200 Subject: [PATCH 149/213] [client] Avoid parsing NB_NETSTACK_SKIP_PROXY if empty (#4145) Signed-off-by: iisteev --- client/iface/netstack/tun.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go index aec9d4faa..b2506b50d 100644 --- a/client/iface/netstack/tun.go +++ b/client/iface/netstack/tun.go @@ -41,9 +41,12 @@ func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) { } t.tundev = nsTunDev - skipProxy, err := strconv.ParseBool(os.Getenv(EnvSkipProxy)) - if err != nil { - log.Errorf("failed to parse %s: %s", EnvSkipProxy, err) + var skipProxy bool + if val := os.Getenv(EnvSkipProxy); val != "" { + skipProxy, err = strconv.ParseBool(val) + if err != nil { + log.Errorf("failed to parse %s: %s", EnvSkipProxy, err) + } } if skipProxy { return nsTunDev, tunNet, nil From 0dab03252c38e547b346dad5be91ac983b3504a1 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 15 Jul 2025 10:43:42 +0200 Subject: [PATCH 150/213] [client, relay-server] Feature/relay notification (#4083) - Clients now subscribe to peer status changes. - The server manages and maintains these subscriptions. - Replaced raw string peer IDs with a custom peer ID type for better type safety and clarity. --- client/iface/wgproxy/bind/proxy.go | 20 +++ client/iface/wgproxy/ebpf/wrapper.go | 18 +++ client/iface/wgproxy/factory_kernel.go | 5 +- client/iface/wgproxy/factory_usp.go | 4 +- client/iface/wgproxy/listener/listener.go | 19 +++ client/iface/wgproxy/proxy.go | 1 + client/iface/wgproxy/proxy_test.go | 4 +- client/iface/wgproxy/udp/proxy.go | 11 ++ client/internal/peer/conn.go | 4 +- client/internal/peer/worker_relay.go | 6 +- relay/auth/validator.go | 7 - relay/client/client.go | 155 ++++++++++++++------ relay/client/client_test.go | 163 ++++++++++++--------- relay/client/conn.go | 13 +- relay/client/guard.go | 2 +- relay/client/manager.go | 20 +-- relay/client/manager_test.go | 102 ++++++++----- relay/client/peer_subscription.go | 168 ++++++++++++++++++++++ relay/client/peer_subscription_test.go | 99 +++++++++++++ relay/client/picker.go | 4 +- relay/cmd/root.go | 9 +- relay/messages/id.go | 24 ++-- relay/messages/id_test.go | 13 -- relay/messages/message.go | 115 ++++++++------- relay/messages/message_test.go | 16 +-- relay/messages/peer_state.go | 92 ++++++++++++ relay/messages/peer_state_test.go | 144 +++++++++++++++++++ relay/server/handshake.go | 47 +++--- relay/server/peer.go | 113 ++++++++++++--- relay/server/relay.go | 116 ++++++++------- relay/server/server.go | 28 ++-- relay/server/store/listener.go | 121 ++++++++++++++++ relay/server/store/notifier.go | 64 +++++++++ relay/server/{ => store}/store.go | 31 ++-- relay/server/store/store_test.go | 49 +++++++ relay/server/store_test.go | 85 ----------- relay/server/url.go | 33 +++++ relay/test/benchmark_test.go | 22 +-- relay/testec2/relay.go | 12 +- 39 files changed, 1464 insertions(+), 495 deletions(-) create mode 100644 client/iface/wgproxy/listener/listener.go create mode 100644 relay/client/peer_subscription.go create mode 100644 relay/client/peer_subscription_test.go delete mode 100644 relay/messages/id_test.go create mode 100644 relay/messages/peer_state.go create mode 100644 relay/messages/peer_state_test.go create mode 100644 relay/server/store/listener.go create mode 100644 relay/server/store/notifier.go rename relay/server/{ => store}/store.go (61%) create mode 100644 relay/server/store/store_test.go delete mode 100644 relay/server/store_test.go create mode 100644 relay/server/url.go diff --git a/client/iface/wgproxy/bind/proxy.go b/client/iface/wgproxy/bind/proxy.go index 614787e17..179ac0b75 100644 --- a/client/iface/wgproxy/bind/proxy.go +++ b/client/iface/wgproxy/bind/proxy.go @@ -12,6 +12,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/iface/bind" + "github.com/netbirdio/netbird/client/iface/wgproxy/listener" ) type ProxyBind struct { @@ -28,6 +29,17 @@ type ProxyBind struct { pausedMu sync.Mutex paused bool isStarted bool + + closeListener *listener.CloseListener +} + +func NewProxyBind(bind *bind.ICEBind) *ProxyBind { + p := &ProxyBind{ + Bind: bind, + closeListener: listener.NewCloseListener(), + } + + return p } // AddTurnConn adds a new connection to the bind. @@ -54,6 +66,10 @@ func (p *ProxyBind) EndpointAddr() *net.UDPAddr { } } +func (p *ProxyBind) SetDisconnectListener(disconnected func()) { + p.closeListener.SetCloseListener(disconnected) +} + func (p *ProxyBind) Work() { if p.remoteConn == nil { return @@ -96,6 +112,9 @@ func (p *ProxyBind) close() error { if p.closed { return nil } + + p.closeListener.SetCloseListener(nil) + p.closed = true p.cancel() @@ -122,6 +141,7 @@ func (p *ProxyBind) proxyToLocal(ctx context.Context) { if ctx.Err() != nil { return } + p.closeListener.Notify() log.Errorf("failed to read from remote conn: %s, %s", p.remoteConn.RemoteAddr(), err) return } diff --git a/client/iface/wgproxy/ebpf/wrapper.go b/client/iface/wgproxy/ebpf/wrapper.go index 54cab4e1b..dbf9128a8 100644 --- a/client/iface/wgproxy/ebpf/wrapper.go +++ b/client/iface/wgproxy/ebpf/wrapper.go @@ -11,6 +11,8 @@ import ( "sync" log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/iface/wgproxy/listener" ) // ProxyWrapper help to keep the remoteConn instance for net.Conn.Close function call @@ -26,6 +28,15 @@ type ProxyWrapper struct { pausedMu sync.Mutex paused bool isStarted bool + + closeListener *listener.CloseListener +} + +func NewProxyWrapper(WgeBPFProxy *WGEBPFProxy) *ProxyWrapper { + return &ProxyWrapper{ + WgeBPFProxy: WgeBPFProxy, + closeListener: listener.NewCloseListener(), + } } func (p *ProxyWrapper) AddTurnConn(ctx context.Context, endpoint *net.UDPAddr, remoteConn net.Conn) error { @@ -43,6 +54,10 @@ func (p *ProxyWrapper) EndpointAddr() *net.UDPAddr { return p.wgEndpointAddr } +func (p *ProxyWrapper) SetDisconnectListener(disconnected func()) { + p.closeListener.SetCloseListener(disconnected) +} + func (p *ProxyWrapper) Work() { if p.remoteConn == nil { return @@ -77,6 +92,8 @@ func (e *ProxyWrapper) CloseConn() error { e.cancel() + e.closeListener.SetCloseListener(nil) + if err := e.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { return fmt.Errorf("failed to close remote conn: %w", err) } @@ -117,6 +134,7 @@ func (p *ProxyWrapper) readFromRemote(ctx context.Context, buf []byte) (int, err if ctx.Err() != nil { return 0, ctx.Err() } + p.closeListener.Notify() if !errors.Is(err, io.EOF) { log.Errorf("failed to read from turn conn (endpoint: :%d): %s", p.wgEndpointAddr.Port, err) } diff --git a/client/iface/wgproxy/factory_kernel.go b/client/iface/wgproxy/factory_kernel.go index 3ad7dc59d..e62cd97be 100644 --- a/client/iface/wgproxy/factory_kernel.go +++ b/client/iface/wgproxy/factory_kernel.go @@ -36,9 +36,8 @@ func (w *KernelFactory) GetProxy() Proxy { return udpProxy.NewWGUDPProxy(w.wgPort) } - return &ebpf.ProxyWrapper{ - WgeBPFProxy: w.ebpfProxy, - } + return ebpf.NewProxyWrapper(w.ebpfProxy) + } func (w *KernelFactory) Free() error { diff --git a/client/iface/wgproxy/factory_usp.go b/client/iface/wgproxy/factory_usp.go index e2d479331..141b4c1f9 100644 --- a/client/iface/wgproxy/factory_usp.go +++ b/client/iface/wgproxy/factory_usp.go @@ -20,9 +20,7 @@ func NewUSPFactory(iceBind *bind.ICEBind) *USPFactory { } func (w *USPFactory) GetProxy() Proxy { - return &proxyBind.ProxyBind{ - Bind: w.bind, - } + return proxyBind.NewProxyBind(w.bind) } func (w *USPFactory) Free() error { diff --git a/client/iface/wgproxy/listener/listener.go b/client/iface/wgproxy/listener/listener.go new file mode 100644 index 000000000..bfd651548 --- /dev/null +++ b/client/iface/wgproxy/listener/listener.go @@ -0,0 +1,19 @@ +package listener + +type CloseListener struct { + listener func() +} + +func NewCloseListener() *CloseListener { + return &CloseListener{} +} + +func (c *CloseListener) SetCloseListener(listener func()) { + c.listener = listener +} + +func (c *CloseListener) Notify() { + if c.listener != nil { + c.listener() + } +} diff --git a/client/iface/wgproxy/proxy.go b/client/iface/wgproxy/proxy.go index 243aa2bd2..c2879877e 100644 --- a/client/iface/wgproxy/proxy.go +++ b/client/iface/wgproxy/proxy.go @@ -12,4 +12,5 @@ type Proxy interface { Work() // Work start or resume the proxy Pause() // Pause to forward the packages from remote connection to WireGuard. The opposite way still works. CloseConn() error + SetDisconnectListener(disconnected func()) } diff --git a/client/iface/wgproxy/proxy_test.go b/client/iface/wgproxy/proxy_test.go index 64b617621..2165b8aba 100644 --- a/client/iface/wgproxy/proxy_test.go +++ b/client/iface/wgproxy/proxy_test.go @@ -98,9 +98,7 @@ func TestProxyCloseByRemoteConn(t *testing.T) { t.Errorf("failed to free ebpf proxy: %s", err) } }() - proxyWrapper := &ebpf.ProxyWrapper{ - WgeBPFProxy: ebpfProxy, - } + proxyWrapper := ebpf.NewProxyWrapper(ebpfProxy) tests = append(tests, struct { name string diff --git a/client/iface/wgproxy/udp/proxy.go b/client/iface/wgproxy/udp/proxy.go index ba0004b8a..df45d8ca5 100644 --- a/client/iface/wgproxy/udp/proxy.go +++ b/client/iface/wgproxy/udp/proxy.go @@ -12,6 +12,7 @@ import ( log "github.com/sirupsen/logrus" cerrors "github.com/netbirdio/netbird/client/errors" + "github.com/netbirdio/netbird/client/iface/wgproxy/listener" ) // WGUDPProxy proxies @@ -28,6 +29,8 @@ type WGUDPProxy struct { pausedMu sync.Mutex paused bool isStarted bool + + closeListener *listener.CloseListener } // NewWGUDPProxy instantiate a UDP based WireGuard proxy. This is not a thread safe implementation @@ -35,6 +38,7 @@ func NewWGUDPProxy(wgPort int) *WGUDPProxy { log.Debugf("Initializing new user space proxy with port %d", wgPort) p := &WGUDPProxy{ localWGListenPort: wgPort, + closeListener: listener.NewCloseListener(), } return p } @@ -67,6 +71,10 @@ func (p *WGUDPProxy) EndpointAddr() *net.UDPAddr { return endpointUdpAddr } +func (p *WGUDPProxy) SetDisconnectListener(disconnected func()) { + p.closeListener.SetCloseListener(disconnected) +} + // Work starts the proxy or resumes it if it was paused func (p *WGUDPProxy) Work() { if p.remoteConn == nil { @@ -111,6 +119,8 @@ func (p *WGUDPProxy) close() error { if p.closed { return nil } + + p.closeListener.SetCloseListener(nil) p.closed = true p.cancel() @@ -141,6 +151,7 @@ func (p *WGUDPProxy) proxyToRemote(ctx context.Context) { if ctx.Err() != nil { return } + p.closeListener.Notify() log.Debugf("failed to read from wg interface conn: %s", err) return } diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 1f0ec164e..7765bb51c 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -167,7 +167,7 @@ func (conn *Conn) Open(engineCtx context.Context) error { conn.ctx, conn.ctxCancel = context.WithCancel(engineCtx) - conn.workerRelay = NewWorkerRelay(conn.Log, isController(conn.config), conn.config, conn, conn.relayManager, conn.dumpState) + conn.workerRelay = NewWorkerRelay(conn.ctx, conn.Log, isController(conn.config), conn.config, conn, conn.relayManager, conn.dumpState) relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally() workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally) @@ -489,6 +489,8 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.Log.Errorf("failed to add relayed net.Conn to local proxy: %v", err) return } + wgProxy.SetDisconnectListener(conn.onRelayDisconnected) + conn.dumpState.NewLocalProxy() conn.Log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String()) diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go index aa8f7d635..5e2900609 100644 --- a/client/internal/peer/worker_relay.go +++ b/client/internal/peer/worker_relay.go @@ -19,6 +19,7 @@ type RelayConnInfo struct { } type WorkerRelay struct { + peerCtx context.Context log *log.Entry isController bool config ConnConfig @@ -33,8 +34,9 @@ type WorkerRelay struct { wgWatcher *WGWatcher } -func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService, stateDump *stateDump) *WorkerRelay { +func NewWorkerRelay(ctx context.Context, log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService, stateDump *stateDump) *WorkerRelay { r := &WorkerRelay{ + peerCtx: ctx, log: log, isController: ctrl, config: config, @@ -62,7 +64,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { srv := w.preferredRelayServer(currentRelayAddress, remoteOfferAnswer.RelaySrvAddress) - relayedConn, err := w.relayManager.OpenConn(srv, w.config.Key) + relayedConn, err := w.relayManager.OpenConn(w.peerCtx, srv, w.config.Key) if err != nil { if errors.Is(err, relayClient.ErrConnAlreadyExists) { w.log.Debugf("handled offer by reusing existing relay connection") diff --git a/relay/auth/validator.go b/relay/auth/validator.go index 854efd5bb..56a20fbfe 100644 --- a/relay/auth/validator.go +++ b/relay/auth/validator.go @@ -7,13 +7,6 @@ import ( authv2 "github.com/netbirdio/netbird/relay/auth/hmac/v2" ) -// Validator is an interface that defines the Validate method. -type Validator interface { - Validate(any) error - // Deprecated: Use Validate instead. - ValidateHelloMsgType(any) error -} - type TimedHMACValidator struct { authenticatorV2 *authv2.Validator authenticator *auth.TimedHMACValidator diff --git a/relay/client/client.go b/relay/client/client.go index 9e7e54393..2bf679ecb 100644 --- a/relay/client/client.go +++ b/relay/client/client.go @@ -124,15 +124,14 @@ func (cc *connContainer) close() { // While the Connect is in progress, the OpenConn function will block until the connection is established with relay server. type Client struct { log *log.Entry - parentCtx context.Context connectionURL string authTokenStore *auth.TokenStore - hashedID []byte + hashedID messages.PeerID bufPool *sync.Pool relayConn net.Conn - conns map[string]*connContainer + conns map[messages.PeerID]*connContainer serviceIsRunning bool mu sync.Mutex // protect serviceIsRunning and conns readLoopMutex sync.Mutex @@ -142,14 +141,17 @@ type Client struct { onDisconnectListener func(string) listenerMutex sync.Mutex + + stateSubscription *PeersStateSubscription } // NewClient creates a new client for the relay server. The client is not connected to the server until the Connect -func NewClient(ctx context.Context, serverURL string, authTokenStore *auth.TokenStore, peerID string) *Client { - hashedID, hashedStringId := messages.HashID(peerID) +func NewClient(serverURL string, authTokenStore *auth.TokenStore, peerID string) *Client { + hashedID := messages.HashID(peerID) + relayLog := log.WithFields(log.Fields{"relay": serverURL}) + c := &Client{ - log: log.WithFields(log.Fields{"relay": serverURL}), - parentCtx: ctx, + log: relayLog, connectionURL: serverURL, authTokenStore: authTokenStore, hashedID: hashedID, @@ -159,14 +161,15 @@ func NewClient(ctx context.Context, serverURL string, authTokenStore *auth.Token return &buf }, }, - conns: make(map[string]*connContainer), + conns: make(map[messages.PeerID]*connContainer), } - c.log.Infof("create new relay connection: local peerID: %s, local peer hashedID: %s", peerID, hashedStringId) + + c.log.Infof("create new relay connection: local peerID: %s, local peer hashedID: %s", peerID, hashedID) return c } // Connect establishes a connection to the relay server. It blocks until the connection is established or an error occurs. -func (c *Client) Connect() error { +func (c *Client) Connect(ctx context.Context) error { c.log.Infof("connecting to relay server") c.readLoopMutex.Lock() defer c.readLoopMutex.Unlock() @@ -178,17 +181,23 @@ func (c *Client) Connect() error { return nil } - if err := c.connect(); err != nil { + if err := c.connect(ctx); err != nil { return err } + c.stateSubscription = NewPeersStateSubscription(c.log, c.relayConn, c.closeConnsByPeerID) + c.log = c.log.WithField("relay", c.instanceURL.String()) c.log.Infof("relay connection established") c.serviceIsRunning = true + internallyStoppedFlag := newInternalStopFlag() + hc := healthcheck.NewReceiver(c.log) + go c.listenForStopEvents(ctx, hc, c.relayConn, internallyStoppedFlag) + c.wgReadLoop.Add(1) - go c.readLoop(c.relayConn) + go c.readLoop(hc, c.relayConn, internallyStoppedFlag) return nil } @@ -196,26 +205,41 @@ func (c *Client) Connect() error { // OpenConn create a new net.Conn for the destination peer ID. In case if the connection is in progress // to the relay server, the function will block until the connection is established or timed out. Otherwise, // it will return immediately. +// It block until the server confirm the peer is online. // todo: what should happen if call with the same peerID with multiple times? -func (c *Client) OpenConn(dstPeerID string) (net.Conn, error) { - c.mu.Lock() - defer c.mu.Unlock() +func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, error) { + peerID := messages.HashID(dstPeerID) + c.mu.Lock() if !c.serviceIsRunning { + c.mu.Unlock() return nil, fmt.Errorf("relay connection is not established") } - - hashedID, hashedStringID := messages.HashID(dstPeerID) - _, ok := c.conns[hashedStringID] + _, ok := c.conns[peerID] if ok { + c.mu.Unlock() return nil, ErrConnAlreadyExists } + c.mu.Unlock() - c.log.Infof("open connection to peer: %s", hashedStringID) + if err := c.stateSubscription.WaitToBeOnlineAndSubscribe(ctx, peerID); err != nil { + c.log.Errorf("peer not available: %s, %s", peerID, err) + return nil, err + } + + c.log.Infof("remote peer is available, prepare the relayed connection: %s", peerID) msgChannel := make(chan Msg, 100) - conn := NewConn(c, hashedID, hashedStringID, msgChannel, c.instanceURL) + conn := NewConn(c, peerID, msgChannel, c.instanceURL) - c.conns[hashedStringID] = newConnContainer(c.log, conn, msgChannel) + c.mu.Lock() + _, ok = c.conns[peerID] + if ok { + c.mu.Unlock() + _ = conn.Close() + return nil, ErrConnAlreadyExists + } + c.conns[peerID] = newConnContainer(c.log, conn, msgChannel) + c.mu.Unlock() return conn, nil } @@ -254,7 +278,7 @@ func (c *Client) Close() error { return c.close(true) } -func (c *Client) connect() error { +func (c *Client) connect(ctx context.Context) error { rd := dialer.NewRaceDial(c.log, c.connectionURL, quic.Dialer{}, ws.Dialer{}) conn, err := rd.Dial() if err != nil { @@ -262,7 +286,7 @@ func (c *Client) connect() error { } c.relayConn = conn - if err = c.handShake(); err != nil { + if err = c.handShake(ctx); err != nil { cErr := conn.Close() if cErr != nil { c.log.Errorf("failed to close connection: %s", cErr) @@ -273,7 +297,7 @@ func (c *Client) connect() error { return nil } -func (c *Client) handShake() error { +func (c *Client) handShake(ctx context.Context) error { msg, err := messages.MarshalAuthMsg(c.hashedID, c.authTokenStore.TokenBinary()) if err != nil { c.log.Errorf("failed to marshal auth message: %s", err) @@ -286,7 +310,7 @@ func (c *Client) handShake() error { return err } buf := make([]byte, messages.MaxHandshakeRespSize) - n, err := c.readWithTimeout(buf) + n, err := c.readWithTimeout(ctx, buf) if err != nil { c.log.Errorf("failed to read auth response: %s", err) return err @@ -319,11 +343,7 @@ func (c *Client) handShake() error { return nil } -func (c *Client) readLoop(relayConn net.Conn) { - internallyStoppedFlag := newInternalStopFlag() - hc := healthcheck.NewReceiver(c.log) - go c.listenForStopEvents(hc, relayConn, internallyStoppedFlag) - +func (c *Client) readLoop(hc *healthcheck.Receiver, relayConn net.Conn, internallyStoppedFlag *internalStopFlag) { var ( errExit error n int @@ -370,6 +390,7 @@ func (c *Client) readLoop(relayConn net.Conn) { c.instanceURL = nil c.muInstanceURL.Unlock() + c.stateSubscription.Cleanup() c.wgReadLoop.Done() _ = c.close(false) c.notifyDisconnected() @@ -382,6 +403,14 @@ func (c *Client) handleMsg(msgType messages.MsgType, buf []byte, bufPtr *[]byte, c.bufPool.Put(bufPtr) case messages.MsgTypeTransport: return c.handleTransportMsg(buf, bufPtr, internallyStoppedFlag) + case messages.MsgTypePeersOnline: + c.handlePeersOnlineMsg(buf) + c.bufPool.Put(bufPtr) + return true + case messages.MsgTypePeersWentOffline: + c.handlePeersWentOfflineMsg(buf) + c.bufPool.Put(bufPtr) + return true case messages.MsgTypeClose: c.log.Debugf("relay connection close by server") c.bufPool.Put(bufPtr) @@ -413,18 +442,16 @@ func (c *Client) handleTransportMsg(buf []byte, bufPtr *[]byte, internallyStoppe return true } - stringID := messages.HashIDToString(peerID) - c.mu.Lock() if !c.serviceIsRunning { c.mu.Unlock() c.bufPool.Put(bufPtr) return false } - container, ok := c.conns[stringID] + container, ok := c.conns[*peerID] c.mu.Unlock() if !ok { - c.log.Errorf("peer not found: %s", stringID) + c.log.Errorf("peer not found: %s", peerID.String()) c.bufPool.Put(bufPtr) return true } @@ -437,9 +464,9 @@ func (c *Client) handleTransportMsg(buf []byte, bufPtr *[]byte, internallyStoppe return true } -func (c *Client) writeTo(connReference *Conn, id string, dstID []byte, payload []byte) (int, error) { +func (c *Client) writeTo(connReference *Conn, dstID messages.PeerID, payload []byte) (int, error) { c.mu.Lock() - conn, ok := c.conns[id] + conn, ok := c.conns[dstID] c.mu.Unlock() if !ok { return 0, net.ErrClosed @@ -464,7 +491,7 @@ func (c *Client) writeTo(connReference *Conn, id string, dstID []byte, payload [ return len(payload), err } -func (c *Client) listenForStopEvents(hc *healthcheck.Receiver, conn net.Conn, internalStopFlag *internalStopFlag) { +func (c *Client) listenForStopEvents(ctx context.Context, hc *healthcheck.Receiver, conn net.Conn, internalStopFlag *internalStopFlag) { for { select { case _, ok := <-hc.OnTimeout: @@ -478,7 +505,7 @@ func (c *Client) listenForStopEvents(hc *healthcheck.Receiver, conn net.Conn, in c.log.Warnf("failed to close connection: %s", err) } return - case <-c.parentCtx.Done(): + case <-ctx.Done(): err := c.close(true) if err != nil { c.log.Errorf("failed to teardown connection: %s", err) @@ -492,10 +519,31 @@ func (c *Client) closeAllConns() { for _, container := range c.conns { container.close() } - c.conns = make(map[string]*connContainer) + c.conns = make(map[messages.PeerID]*connContainer) } -func (c *Client) closeConn(connReference *Conn, id string) error { +func (c *Client) closeConnsByPeerID(peerIDs []messages.PeerID) { + c.mu.Lock() + defer c.mu.Unlock() + + for _, peerID := range peerIDs { + container, ok := c.conns[peerID] + if !ok { + c.log.Warnf("can not close connection, peer not found: %s", peerID) + continue + } + + container.log.Infof("remote peer has been disconnected, free up connection: %s", peerID) + container.close() + delete(c.conns, peerID) + } + + if err := c.stateSubscription.UnsubscribeStateChange(peerIDs); err != nil { + c.log.Errorf("failed to unsubscribe from peer state change: %s, %s", peerIDs, err) + } +} + +func (c *Client) closeConn(connReference *Conn, id messages.PeerID) error { c.mu.Lock() defer c.mu.Unlock() @@ -507,6 +555,11 @@ func (c *Client) closeConn(connReference *Conn, id string) error { if container.conn != connReference { return fmt.Errorf("conn reference mismatch") } + + if err := c.stateSubscription.UnsubscribeStateChange([]messages.PeerID{id}); err != nil { + container.log.Errorf("failed to unsubscribe from peer state change: %s", err) + } + c.log.Infof("free up connection to peer: %s", id) delete(c.conns, id) container.close() @@ -559,8 +612,8 @@ func (c *Client) writeCloseMsg() { } } -func (c *Client) readWithTimeout(buf []byte) (int, error) { - ctx, cancel := context.WithTimeout(c.parentCtx, serverResponseTimeout) +func (c *Client) readWithTimeout(ctx context.Context, buf []byte) (int, error) { + ctx, cancel := context.WithTimeout(ctx, serverResponseTimeout) defer cancel() readDone := make(chan struct{}) @@ -581,3 +634,21 @@ func (c *Client) readWithTimeout(buf []byte) (int, error) { return n, err } } + +func (c *Client) handlePeersOnlineMsg(buf []byte) { + peersID, err := messages.UnmarshalPeersOnlineMsg(buf) + if err != nil { + c.log.Errorf("failed to unmarshal peers online msg: %s", err) + return + } + c.stateSubscription.OnPeersOnline(peersID) +} + +func (c *Client) handlePeersWentOfflineMsg(buf []byte) { + peersID, err := messages.UnMarshalPeersWentOffline(buf) + if err != nil { + c.log.Errorf("failed to unmarshal peers went offline msg: %s", err) + return + } + c.stateSubscription.OnPeersWentOffline(peersID) +} diff --git a/relay/client/client_test.go b/relay/client/client_test.go index 7ddfba4c6..dd5f5fe1e 100644 --- a/relay/client/client_test.go +++ b/relay/client/client_test.go @@ -18,14 +18,19 @@ import ( ) var ( - av = &allow.Auth{} hmacTokenStore = &hmac.TokenStore{} serverListenAddr = "127.0.0.1:1234" serverURL = "rel://127.0.0.1:1234" + serverCfg = server.Config{ + Meter: otel.Meter(""), + ExposedAddress: serverURL, + TLSSupport: false, + AuthValidator: &allow.Auth{}, + } ) func TestMain(m *testing.M) { - _ = util.InitLog("error", "console") + _ = util.InitLog("debug", "console") code := m.Run() os.Exit(code) } @@ -33,7 +38,7 @@ func TestMain(m *testing.M) { func TestClient(t *testing.T) { ctx := context.Background() - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -58,37 +63,37 @@ func TestClient(t *testing.T) { t.Fatalf("failed to start server: %s", err) } t.Log("alice connecting to server") - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + clientAlice := NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } defer clientAlice.Close() t.Log("placeholder connecting to server") - clientPlaceHolder := NewClient(ctx, serverURL, hmacTokenStore, "clientPlaceHolder") - err = clientPlaceHolder.Connect() + clientPlaceHolder := NewClient(serverURL, hmacTokenStore, "clientPlaceHolder") + err = clientPlaceHolder.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } defer clientPlaceHolder.Close() t.Log("Bob connecting to server") - clientBob := NewClient(ctx, serverURL, hmacTokenStore, "bob") - err = clientBob.Connect() + clientBob := NewClient(serverURL, hmacTokenStore, "bob") + err = clientBob.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } defer clientBob.Close() t.Log("Alice open connection to Bob") - connAliceToBob, err := clientAlice.OpenConn("bob") + connAliceToBob, err := clientAlice.OpenConn(ctx, "bob") if err != nil { t.Fatalf("failed to bind channel: %s", err) } t.Log("Bob open connection to Alice") - connBobToAlice, err := clientBob.OpenConn("alice") + connBobToAlice, err := clientBob.OpenConn(ctx, "alice") if err != nil { t.Fatalf("failed to bind channel: %s", err) } @@ -115,7 +120,7 @@ func TestClient(t *testing.T) { func TestRegistration(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -132,8 +137,8 @@ func TestRegistration(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + clientAlice := NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) if err != nil { _ = srv.Shutdown(ctx) t.Fatalf("failed to connect to server: %s", err) @@ -172,8 +177,8 @@ func TestRegistrationTimeout(t *testing.T) { _ = fakeTCPListener.Close() }(fakeTCPListener) - clientAlice := NewClient(ctx, "127.0.0.1:1234", hmacTokenStore, "alice") - err = clientAlice.Connect() + clientAlice := NewClient("127.0.0.1:1234", hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) if err == nil { t.Errorf("failed to connect to server: %s", err) } @@ -189,7 +194,7 @@ func TestEcho(t *testing.T) { idAlice := "alice" idBob := "bob" srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -213,8 +218,8 @@ func TestEcho(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, idAlice) - err = clientAlice.Connect() + clientAlice := NewClient(serverURL, hmacTokenStore, idAlice) + err = clientAlice.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } @@ -225,8 +230,8 @@ func TestEcho(t *testing.T) { } }() - clientBob := NewClient(ctx, serverURL, hmacTokenStore, idBob) - err = clientBob.Connect() + clientBob := NewClient(serverURL, hmacTokenStore, idBob) + err = clientBob.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } @@ -237,12 +242,12 @@ func TestEcho(t *testing.T) { } }() - connAliceToBob, err := clientAlice.OpenConn(idBob) + connAliceToBob, err := clientAlice.OpenConn(ctx, idBob) if err != nil { t.Fatalf("failed to bind channel: %s", err) } - connBobToAlice, err := clientBob.OpenConn(idAlice) + connBobToAlice, err := clientBob.OpenConn(ctx, idAlice) if err != nil { t.Fatalf("failed to bind channel: %s", err) } @@ -278,7 +283,7 @@ func TestBindToUnavailabePeer(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -303,14 +308,14 @@ func TestBindToUnavailabePeer(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + clientAlice := NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) if err != nil { t.Errorf("failed to connect to server: %s", err) } - _, err = clientAlice.OpenConn("bob") - if err != nil { - t.Errorf("failed to bind channel: %s", err) + _, err = clientAlice.OpenConn(ctx, "bob") + if err == nil { + t.Errorf("expected error when binding to unavailable peer, got nil") } log.Infof("closing client") @@ -324,7 +329,7 @@ func TestBindReconnect(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -349,24 +354,24 @@ func TestBindReconnect(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + clientAlice := NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) + if err != nil { + t.Fatalf("failed to connect to server: %s", err) + } + + clientBob := NewClient(serverURL, hmacTokenStore, "bob") + err = clientBob.Connect(ctx) if err != nil { t.Errorf("failed to connect to server: %s", err) } - _, err = clientAlice.OpenConn("bob") + _, err = clientAlice.OpenConn(ctx, "bob") if err != nil { - t.Errorf("failed to bind channel: %s", err) + t.Fatalf("failed to bind channel: %s", err) } - clientBob := NewClient(ctx, serverURL, hmacTokenStore, "bob") - err = clientBob.Connect() - if err != nil { - t.Errorf("failed to connect to server: %s", err) - } - - chBob, err := clientBob.OpenConn("alice") + chBob, err := clientBob.OpenConn(ctx, "alice") if err != nil { t.Errorf("failed to bind channel: %s", err) } @@ -377,18 +382,28 @@ func TestBindReconnect(t *testing.T) { t.Errorf("failed to close client: %s", err) } - clientAlice = NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + clientAlice = NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) if err != nil { t.Errorf("failed to connect to server: %s", err) } - chAlice, err := clientAlice.OpenConn("bob") + chAlice, err := clientAlice.OpenConn(ctx, "bob") if err != nil { t.Errorf("failed to bind channel: %s", err) } testString := "hello alice, I am bob" + _, err = chBob.Write([]byte(testString)) + if err == nil { + t.Errorf("expected error when writing to channel, got nil") + } + + chBob, err = clientBob.OpenConn(ctx, "alice") + if err != nil { + t.Errorf("failed to bind channel: %s", err) + } + _, err = chBob.Write([]byte(testString)) if err != nil { t.Errorf("failed to write to channel: %s", err) @@ -415,7 +430,7 @@ func TestCloseConn(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -440,13 +455,19 @@ func TestCloseConn(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + bob := NewClient(serverURL, hmacTokenStore, "bob") + err = bob.Connect(ctx) if err != nil { t.Errorf("failed to connect to server: %s", err) } - conn, err := clientAlice.OpenConn("bob") + clientAlice := NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) + if err != nil { + t.Errorf("failed to connect to server: %s", err) + } + + conn, err := clientAlice.OpenConn(ctx, "bob") if err != nil { t.Errorf("failed to bind channel: %s", err) } @@ -472,7 +493,7 @@ func TestCloseRelayConn(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -496,13 +517,19 @@ func TestCloseRelayConn(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, "alice") - err = clientAlice.Connect() + bob := NewClient(serverURL, hmacTokenStore, "bob") + err = bob.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } - conn, err := clientAlice.OpenConn("bob") + clientAlice := NewClient(serverURL, hmacTokenStore, "alice") + err = clientAlice.Connect(ctx) + if err != nil { + t.Fatalf("failed to connect to server: %s", err) + } + + conn, err := clientAlice.OpenConn(ctx, "bob") if err != nil { t.Errorf("failed to bind channel: %s", err) } @@ -514,7 +541,7 @@ func TestCloseRelayConn(t *testing.T) { t.Errorf("unexpected reading from closed connection") } - _, err = clientAlice.OpenConn("bob") + _, err = clientAlice.OpenConn(ctx, "bob") if err == nil { t.Errorf("unexpected opening connection to closed server") } @@ -524,7 +551,7 @@ func TestCloseByServer(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv1, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv1, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -544,8 +571,8 @@ func TestCloseByServer(t *testing.T) { idAlice := "alice" log.Debugf("connect by alice") - relayClient := NewClient(ctx, serverURL, hmacTokenStore, idAlice) - err = relayClient.Connect() + relayClient := NewClient(serverURL, hmacTokenStore, idAlice) + err = relayClient.Connect(ctx) if err != nil { log.Fatalf("failed to connect to server: %s", err) } @@ -567,7 +594,7 @@ func TestCloseByServer(t *testing.T) { log.Fatalf("timeout waiting for client to disconnect") } - _, err = relayClient.OpenConn("bob") + _, err = relayClient.OpenConn(ctx, "bob") if err == nil { t.Errorf("unexpected opening connection to closed server") } @@ -577,7 +604,7 @@ func TestCloseByClient(t *testing.T) { ctx := context.Background() srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -596,8 +623,8 @@ func TestCloseByClient(t *testing.T) { idAlice := "alice" log.Debugf("connect by alice") - relayClient := NewClient(ctx, serverURL, hmacTokenStore, idAlice) - err = relayClient.Connect() + relayClient := NewClient(serverURL, hmacTokenStore, idAlice) + err = relayClient.Connect(ctx) if err != nil { log.Fatalf("failed to connect to server: %s", err) } @@ -607,7 +634,7 @@ func TestCloseByClient(t *testing.T) { t.Errorf("failed to close client: %s", err) } - _, err = relayClient.OpenConn("bob") + _, err = relayClient.OpenConn(ctx, "bob") if err == nil { t.Errorf("unexpected opening connection to closed server") } @@ -623,7 +650,7 @@ func TestCloseNotDrainedChannel(t *testing.T) { idAlice := "alice" idBob := "bob" srvCfg := server.ListenerConfig{Address: serverListenAddr} - srv, err := server.NewServer(otel.Meter(""), serverURL, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -647,8 +674,8 @@ func TestCloseNotDrainedChannel(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - clientAlice := NewClient(ctx, serverURL, hmacTokenStore, idAlice) - err = clientAlice.Connect() + clientAlice := NewClient(serverURL, hmacTokenStore, idAlice) + err = clientAlice.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } @@ -659,8 +686,8 @@ func TestCloseNotDrainedChannel(t *testing.T) { } }() - clientBob := NewClient(ctx, serverURL, hmacTokenStore, idBob) - err = clientBob.Connect() + clientBob := NewClient(serverURL, hmacTokenStore, idBob) + err = clientBob.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } @@ -671,12 +698,12 @@ func TestCloseNotDrainedChannel(t *testing.T) { } }() - connAliceToBob, err := clientAlice.OpenConn(idBob) + connAliceToBob, err := clientAlice.OpenConn(ctx, idBob) if err != nil { t.Fatalf("failed to bind channel: %s", err) } - connBobToAlice, err := clientBob.OpenConn(idAlice) + connBobToAlice, err := clientBob.OpenConn(ctx, idAlice) if err != nil { t.Fatalf("failed to bind channel: %s", err) } diff --git a/relay/client/conn.go b/relay/client/conn.go index fe1b6fb52..d8cffa695 100644 --- a/relay/client/conn.go +++ b/relay/client/conn.go @@ -3,13 +3,14 @@ package client import ( "net" "time" + + "github.com/netbirdio/netbird/relay/messages" ) // Conn represent a connection to a relayed remote peer. type Conn struct { client *Client - dstID []byte - dstStringID string + dstID messages.PeerID messageChan chan Msg instanceURL *RelayAddr } @@ -17,14 +18,12 @@ type Conn struct { // NewConn creates a new connection to a relayed remote peer. // client: the client instance, it used to send messages to the destination peer // dstID: the destination peer ID -// dstStringID: the destination peer ID in string format // messageChan: the channel where the messages will be received // instanceURL: the relay instance URL, it used to get the proper server instance address for the remote peer -func NewConn(client *Client, dstID []byte, dstStringID string, messageChan chan Msg, instanceURL *RelayAddr) *Conn { +func NewConn(client *Client, dstID messages.PeerID, messageChan chan Msg, instanceURL *RelayAddr) *Conn { c := &Conn{ client: client, dstID: dstID, - dstStringID: dstStringID, messageChan: messageChan, instanceURL: instanceURL, } @@ -33,7 +32,7 @@ func NewConn(client *Client, dstID []byte, dstStringID string, messageChan chan } func (c *Conn) Write(p []byte) (n int, err error) { - return c.client.writeTo(c, c.dstStringID, c.dstID, p) + return c.client.writeTo(c, c.dstID, p) } func (c *Conn) Read(b []byte) (n int, err error) { @@ -48,7 +47,7 @@ func (c *Conn) Read(b []byte) (n int, err error) { } func (c *Conn) Close() error { - return c.client.closeConn(c, c.dstStringID) + return c.client.closeConn(c, c.dstID) } func (c *Conn) LocalAddr() net.Addr { diff --git a/relay/client/guard.go b/relay/client/guard.go index 554330ea3..100892d81 100644 --- a/relay/client/guard.go +++ b/relay/client/guard.go @@ -80,7 +80,7 @@ func (g *Guard) tryToQuickReconnect(parentCtx context.Context, rc *Client) bool log.Infof("try to reconnect to Relay server: %s", rc.connectionURL) - if err := rc.Connect(); err != nil { + if err := rc.Connect(parentCtx); err != nil { log.Errorf("failed to reconnect to relay server: %s", err) return false } diff --git a/relay/client/manager.go b/relay/client/manager.go index 26b113050..0fb682d95 100644 --- a/relay/client/manager.go +++ b/relay/client/manager.go @@ -42,7 +42,7 @@ type OnServerCloseListener func() // ManagerService is the interface for the relay manager. type ManagerService interface { Serve() error - OpenConn(serverAddress, peerKey string) (net.Conn, error) + OpenConn(ctx context.Context, serverAddress, peerKey string) (net.Conn, error) AddCloseListener(serverAddress string, onClosedListener OnServerCloseListener) error RelayInstanceAddress() (string, error) ServerURLs() []string @@ -123,7 +123,7 @@ func (m *Manager) Serve() error { // OpenConn opens a connection to the given peer key. If the peer is on the same relay server, the connection will be // established via the relay server. If the peer is on a different relay server, the manager will establish a new // connection to the relay server. It returns back with a net.Conn what represent the remote peer connection. -func (m *Manager) OpenConn(serverAddress, peerKey string) (net.Conn, error) { +func (m *Manager) OpenConn(ctx context.Context, serverAddress, peerKey string) (net.Conn, error) { m.relayClientMu.Lock() defer m.relayClientMu.Unlock() @@ -141,10 +141,10 @@ func (m *Manager) OpenConn(serverAddress, peerKey string) (net.Conn, error) { ) if !foreign { log.Debugf("open peer connection via permanent server: %s", peerKey) - netConn, err = m.relayClient.OpenConn(peerKey) + netConn, err = m.relayClient.OpenConn(ctx, peerKey) } else { log.Debugf("open peer connection via foreign server: %s", serverAddress) - netConn, err = m.openConnVia(serverAddress, peerKey) + netConn, err = m.openConnVia(ctx, serverAddress, peerKey) } if err != nil { return nil, err @@ -229,7 +229,7 @@ func (m *Manager) UpdateToken(token *relayAuth.Token) error { return m.tokenStore.UpdateToken(token) } -func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) { +func (m *Manager) openConnVia(ctx context.Context, serverAddress, peerKey string) (net.Conn, error) { // check if already has a connection to the desired relay server m.relayClientsMutex.RLock() rt, ok := m.relayClients[serverAddress] @@ -240,7 +240,7 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) { if rt.err != nil { return nil, rt.err } - return rt.relayClient.OpenConn(peerKey) + return rt.relayClient.OpenConn(ctx, peerKey) } m.relayClientsMutex.RUnlock() @@ -255,7 +255,7 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) { if rt.err != nil { return nil, rt.err } - return rt.relayClient.OpenConn(peerKey) + return rt.relayClient.OpenConn(ctx, peerKey) } // create a new relay client and store it in the relayClients map @@ -264,8 +264,8 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) { m.relayClients[serverAddress] = rt m.relayClientsMutex.Unlock() - relayClient := NewClient(m.ctx, serverAddress, m.tokenStore, m.peerID) - err := relayClient.Connect() + relayClient := NewClient(serverAddress, m.tokenStore, m.peerID) + err := relayClient.Connect(m.ctx) if err != nil { rt.err = err rt.Unlock() @@ -279,7 +279,7 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) { rt.relayClient = relayClient rt.Unlock() - conn, err := relayClient.OpenConn(peerKey) + conn, err := relayClient.OpenConn(ctx, peerKey) if err != nil { return nil, err } diff --git a/relay/client/manager_test.go b/relay/client/manager_test.go index bfc342f25..d20cdaac0 100644 --- a/relay/client/manager_test.go +++ b/relay/client/manager_test.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" + "github.com/netbirdio/netbird/relay/auth/allow" "github.com/netbirdio/netbird/relay/server" ) @@ -22,16 +23,22 @@ func TestEmptyURL(t *testing.T) { func TestForeignConn(t *testing.T) { ctx := context.Background() - srvCfg1 := server.ListenerConfig{ + lstCfg1 := server.ListenerConfig{ Address: "localhost:1234", } - srv1, err := server.NewServer(otel.Meter(""), srvCfg1.Address, false, av) + + srv1, err := server.NewServer(server.Config{ + Meter: otel.Meter(""), + ExposedAddress: lstCfg1.Address, + TLSSupport: false, + AuthValidator: &allow.Auth{}, + }) if err != nil { t.Fatalf("failed to create server: %s", err) } errChan := make(chan error, 1) go func() { - err := srv1.Listen(srvCfg1) + err := srv1.Listen(lstCfg1) if err != nil { errChan <- err } @@ -51,7 +58,12 @@ func TestForeignConn(t *testing.T) { srvCfg2 := server.ListenerConfig{ Address: "localhost:2234", } - srv2, err := server.NewServer(otel.Meter(""), srvCfg2.Address, false, av) + srv2, err := server.NewServer(server.Config{ + Meter: otel.Meter(""), + ExposedAddress: srvCfg2.Address, + TLSSupport: false, + AuthValidator: &allow.Auth{}, + }) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -74,32 +86,26 @@ func TestForeignConn(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - idAlice := "alice" - log.Debugf("connect by alice") mCtx, cancel := context.WithCancel(ctx) defer cancel() - clientAlice := NewManager(mCtx, toURL(srvCfg1), idAlice) - err = clientAlice.Serve() - if err != nil { + clientAlice := NewManager(mCtx, toURL(lstCfg1), "alice") + if err := clientAlice.Serve(); err != nil { t.Fatalf("failed to serve manager: %s", err) } - idBob := "bob" - log.Debugf("connect by bob") - clientBob := NewManager(mCtx, toURL(srvCfg2), idBob) - err = clientBob.Serve() - if err != nil { + clientBob := NewManager(mCtx, toURL(srvCfg2), "bob") + if err := clientBob.Serve(); err != nil { t.Fatalf("failed to serve manager: %s", err) } bobsSrvAddr, err := clientBob.RelayInstanceAddress() if err != nil { t.Fatalf("failed to get relay address: %s", err) } - connAliceToBob, err := clientAlice.OpenConn(bobsSrvAddr, idBob) + connAliceToBob, err := clientAlice.OpenConn(ctx, bobsSrvAddr, "bob") if err != nil { t.Fatalf("failed to bind channel: %s", err) } - connBobToAlice, err := clientBob.OpenConn(bobsSrvAddr, idAlice) + connBobToAlice, err := clientBob.OpenConn(ctx, bobsSrvAddr, "alice") if err != nil { t.Fatalf("failed to bind channel: %s", err) } @@ -137,7 +143,7 @@ func TestForeginConnClose(t *testing.T) { srvCfg1 := server.ListenerConfig{ Address: "localhost:1234", } - srv1, err := server.NewServer(otel.Meter(""), srvCfg1.Address, false, av) + srv1, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -163,7 +169,7 @@ func TestForeginConnClose(t *testing.T) { srvCfg2 := server.ListenerConfig{ Address: "localhost:2234", } - srv2, err := server.NewServer(otel.Meter(""), srvCfg2.Address, false, av) + srv2, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -186,16 +192,20 @@ func TestForeginConnClose(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - idAlice := "alice" - log.Debugf("connect by alice") mCtx, cancel := context.WithCancel(ctx) defer cancel() - mgr := NewManager(mCtx, toURL(srvCfg1), idAlice) + + mgrBob := NewManager(mCtx, toURL(srvCfg2), "bob") + if err := mgrBob.Serve(); err != nil { + t.Fatalf("failed to serve manager: %s", err) + } + + mgr := NewManager(mCtx, toURL(srvCfg1), "alice") err = mgr.Serve() if err != nil { t.Fatalf("failed to serve manager: %s", err) } - conn, err := mgr.OpenConn(toURL(srvCfg2)[0], "anotherpeer") + conn, err := mgr.OpenConn(ctx, toURL(srvCfg2)[0], "bob") if err != nil { t.Fatalf("failed to bind channel: %s", err) } @@ -212,7 +222,7 @@ func TestForeginAutoClose(t *testing.T) { srvCfg1 := server.ListenerConfig{ Address: "localhost:1234", } - srv1, err := server.NewServer(otel.Meter(""), srvCfg1.Address, false, av) + srv1, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -241,7 +251,7 @@ func TestForeginAutoClose(t *testing.T) { srvCfg2 := server.ListenerConfig{ Address: "localhost:2234", } - srv2, err := server.NewServer(otel.Meter(""), srvCfg2.Address, false, av) + srv2, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -277,7 +287,7 @@ func TestForeginAutoClose(t *testing.T) { } t.Log("open connection to another peer") - conn, err := mgr.OpenConn(toURL(srvCfg2)[0], "anotherpeer") + conn, err := mgr.OpenConn(ctx, toURL(srvCfg2)[0], "anotherpeer") if err != nil { t.Fatalf("failed to bind channel: %s", err) } @@ -305,7 +315,7 @@ func TestAutoReconnect(t *testing.T) { srvCfg := server.ListenerConfig{ Address: "localhost:1234", } - srv, err := server.NewServer(otel.Meter(""), srvCfg.Address, false, av) + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -330,6 +340,13 @@ func TestAutoReconnect(t *testing.T) { mCtx, cancel := context.WithCancel(ctx) defer cancel() + + clientBob := NewManager(mCtx, toURL(srvCfg), "bob") + err = clientBob.Serve() + if err != nil { + t.Fatalf("failed to serve manager: %s", err) + } + clientAlice := NewManager(mCtx, toURL(srvCfg), "alice") err = clientAlice.Serve() if err != nil { @@ -339,7 +356,7 @@ func TestAutoReconnect(t *testing.T) { if err != nil { t.Errorf("failed to get relay address: %s", err) } - conn, err := clientAlice.OpenConn(ra, "bob") + conn, err := clientAlice.OpenConn(ctx, ra, "bob") if err != nil { t.Errorf("failed to bind channel: %s", err) } @@ -357,7 +374,7 @@ func TestAutoReconnect(t *testing.T) { time.Sleep(reconnectingTimeout + 1*time.Second) log.Infof("reopent the connection") - _, err = clientAlice.OpenConn(ra, "bob") + _, err = clientAlice.OpenConn(ctx, ra, "bob") if err != nil { t.Errorf("failed to open channel: %s", err) } @@ -366,24 +383,27 @@ func TestAutoReconnect(t *testing.T) { func TestNotifierDoubleAdd(t *testing.T) { ctx := context.Background() - srvCfg1 := server.ListenerConfig{ + listenerCfg1 := server.ListenerConfig{ Address: "localhost:1234", } - srv1, err := server.NewServer(otel.Meter(""), srvCfg1.Address, false, av) + srv, err := server.NewServer(server.Config{ + Meter: otel.Meter(""), + ExposedAddress: listenerCfg1.Address, + TLSSupport: false, + AuthValidator: &allow.Auth{}, + }) if err != nil { t.Fatalf("failed to create server: %s", err) } errChan := make(chan error, 1) go func() { - err := srv1.Listen(srvCfg1) - if err != nil { + if err := srv.Listen(listenerCfg1); err != nil { errChan <- err } }() defer func() { - err := srv1.Shutdown(ctx) - if err != nil { + if err := srv.Shutdown(ctx); err != nil { t.Errorf("failed to close server: %s", err) } }() @@ -392,17 +412,21 @@ func TestNotifierDoubleAdd(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - idAlice := "alice" log.Debugf("connect by alice") mCtx, cancel := context.WithCancel(ctx) defer cancel() - clientAlice := NewManager(mCtx, toURL(srvCfg1), idAlice) - err = clientAlice.Serve() - if err != nil { + + clientBob := NewManager(mCtx, toURL(listenerCfg1), "bob") + if err = clientBob.Serve(); err != nil { t.Fatalf("failed to serve manager: %s", err) } - conn1, err := clientAlice.OpenConn(clientAlice.ServerURLs()[0], "idBob") + clientAlice := NewManager(mCtx, toURL(listenerCfg1), "alice") + if err = clientAlice.Serve(); err != nil { + t.Fatalf("failed to serve manager: %s", err) + } + + conn1, err := clientAlice.OpenConn(ctx, clientAlice.ServerURLs()[0], "bob") if err != nil { t.Fatalf("failed to bind channel: %s", err) } diff --git a/relay/client/peer_subscription.go b/relay/client/peer_subscription.go new file mode 100644 index 000000000..03e7127b3 --- /dev/null +++ b/relay/client/peer_subscription.go @@ -0,0 +1,168 @@ +package client + +import ( + "context" + "errors" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/relay/messages" +) + +const ( + OpenConnectionTimeout = 30 * time.Second +) + +type relayedConnWriter interface { + Write(p []byte) (n int, err error) +} + +// PeersStateSubscription manages subscriptions to peer state changes (online/offline) +// over a relay connection. It allows tracking peers' availability and handling offline +// events via a callback. We get online notification from the server only once. +type PeersStateSubscription struct { + log *log.Entry + relayConn relayedConnWriter + offlineCallback func(peerIDs []messages.PeerID) + + listenForOfflinePeers map[messages.PeerID]struct{} + waitingPeers map[messages.PeerID]chan struct{} +} + +func NewPeersStateSubscription(log *log.Entry, relayConn relayedConnWriter, offlineCallback func(peerIDs []messages.PeerID)) *PeersStateSubscription { + return &PeersStateSubscription{ + log: log, + relayConn: relayConn, + offlineCallback: offlineCallback, + listenForOfflinePeers: make(map[messages.PeerID]struct{}), + waitingPeers: make(map[messages.PeerID]chan struct{}), + } +} + +// OnPeersOnline should be called when a notification is received that certain peers have come online. +// It checks if any of the peers are being waited on and signals their availability. +func (s *PeersStateSubscription) OnPeersOnline(peersID []messages.PeerID) { + for _, peerID := range peersID { + waitCh, ok := s.waitingPeers[peerID] + if !ok { + continue + } + + close(waitCh) + delete(s.waitingPeers, peerID) + } +} + +func (s *PeersStateSubscription) OnPeersWentOffline(peersID []messages.PeerID) { + relevantPeers := make([]messages.PeerID, 0, len(peersID)) + for _, peerID := range peersID { + if _, ok := s.listenForOfflinePeers[peerID]; ok { + relevantPeers = append(relevantPeers, peerID) + } + } + + if len(relevantPeers) > 0 { + s.offlineCallback(relevantPeers) + } +} + +// WaitToBeOnlineAndSubscribe waits for a specific peer to come online and subscribes to its state changes. +// todo: when we unsubscribe while this is running, this will not return with error +func (s *PeersStateSubscription) WaitToBeOnlineAndSubscribe(ctx context.Context, peerID messages.PeerID) error { + // Check if already waiting for this peer + if _, exists := s.waitingPeers[peerID]; exists { + return errors.New("already waiting for peer to come online") + } + + // Create a channel to wait for the peer to come online + waitCh := make(chan struct{}) + s.waitingPeers[peerID] = waitCh + + if err := s.subscribeStateChange([]messages.PeerID{peerID}); err != nil { + s.log.Errorf("failed to subscribe to peer state: %s", err) + close(waitCh) + delete(s.waitingPeers, peerID) + return err + } + + defer func() { + if ch, exists := s.waitingPeers[peerID]; exists && ch == waitCh { + close(waitCh) + delete(s.waitingPeers, peerID) + } + }() + + // Wait for peer to come online or context to be cancelled + timeoutCtx, cancel := context.WithTimeout(ctx, OpenConnectionTimeout) + defer cancel() + select { + case <-waitCh: + s.log.Debugf("peer %s is now online", peerID) + return nil + case <-timeoutCtx.Done(): + s.log.Debugf("context timed out while waiting for peer %s to come online", peerID) + if err := s.unsubscribeStateChange([]messages.PeerID{peerID}); err != nil { + s.log.Errorf("failed to unsubscribe from peer state: %s", err) + } + return timeoutCtx.Err() + } +} + +func (s *PeersStateSubscription) UnsubscribeStateChange(peerIDs []messages.PeerID) error { + msgErr := s.unsubscribeStateChange(peerIDs) + + for _, peerID := range peerIDs { + if wch, ok := s.waitingPeers[peerID]; ok { + close(wch) + delete(s.waitingPeers, peerID) + } + + delete(s.listenForOfflinePeers, peerID) + } + + return msgErr +} + +func (s *PeersStateSubscription) Cleanup() { + for _, waitCh := range s.waitingPeers { + close(waitCh) + } + + s.waitingPeers = make(map[messages.PeerID]chan struct{}) + s.listenForOfflinePeers = make(map[messages.PeerID]struct{}) +} + +func (s *PeersStateSubscription) subscribeStateChange(peerIDs []messages.PeerID) error { + msgs, err := messages.MarshalSubPeerStateMsg(peerIDs) + if err != nil { + return err + } + + for _, peer := range peerIDs { + s.listenForOfflinePeers[peer] = struct{}{} + } + + for _, msg := range msgs { + if _, err := s.relayConn.Write(msg); err != nil { + return err + } + + } + return nil +} + +func (s *PeersStateSubscription) unsubscribeStateChange(peerIDs []messages.PeerID) error { + msgs, err := messages.MarshalUnsubPeerStateMsg(peerIDs) + if err != nil { + return err + } + + var connWriteErr error + for _, msg := range msgs { + if _, err := s.relayConn.Write(msg); err != nil { + connWriteErr = err + } + } + return connWriteErr +} diff --git a/relay/client/peer_subscription_test.go b/relay/client/peer_subscription_test.go new file mode 100644 index 000000000..0437efa04 --- /dev/null +++ b/relay/client/peer_subscription_test.go @@ -0,0 +1,99 @@ +package client + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/netbirdio/netbird/relay/messages" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockRelayedConn struct { +} + +func (m *mockRelayedConn) Write(p []byte) (n int, err error) { + return len(p), nil +} + +func TestWaitToBeOnlineAndSubscribe_Success(t *testing.T) { + peerID := messages.HashID("peer1") + mockConn := &mockRelayedConn{} + logger := logrus.New() + logger.SetOutput(&bytes.Buffer{}) // discard log output + sub := NewPeersStateSubscription(logrus.NewEntry(logger), mockConn, nil) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Launch wait in background + go func() { + time.Sleep(100 * time.Millisecond) + sub.OnPeersOnline([]messages.PeerID{peerID}) + }() + + err := sub.WaitToBeOnlineAndSubscribe(ctx, peerID) + assert.NoError(t, err) +} + +func TestWaitToBeOnlineAndSubscribe_Timeout(t *testing.T) { + peerID := messages.HashID("peer2") + mockConn := &mockRelayedConn{} + logger := logrus.New() + logger.SetOutput(&bytes.Buffer{}) + sub := NewPeersStateSubscription(logrus.NewEntry(logger), mockConn, nil) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + err := sub.WaitToBeOnlineAndSubscribe(ctx, peerID) + assert.Error(t, err) + assert.Equal(t, context.DeadlineExceeded, err) +} + +func TestWaitToBeOnlineAndSubscribe_Duplicate(t *testing.T) { + peerID := messages.HashID("peer3") + mockConn := &mockRelayedConn{} + logger := logrus.New() + logger.SetOutput(&bytes.Buffer{}) + sub := NewPeersStateSubscription(logrus.NewEntry(logger), mockConn, nil) + + ctx := context.Background() + go func() { + _ = sub.WaitToBeOnlineAndSubscribe(ctx, peerID) + + }() + time.Sleep(100 * time.Millisecond) + err := sub.WaitToBeOnlineAndSubscribe(ctx, peerID) + require.Error(t, err) + assert.Contains(t, err.Error(), "already waiting") +} + +func TestUnsubscribeStateChange(t *testing.T) { + peerID := messages.HashID("peer4") + mockConn := &mockRelayedConn{} + logger := logrus.New() + logger.SetOutput(&bytes.Buffer{}) + sub := NewPeersStateSubscription(logrus.NewEntry(logger), mockConn, nil) + + doneChan := make(chan struct{}) + go func() { + _ = sub.WaitToBeOnlineAndSubscribe(context.Background(), peerID) + close(doneChan) + }() + time.Sleep(100 * time.Millisecond) + + err := sub.UnsubscribeStateChange([]messages.PeerID{peerID}) + assert.NoError(t, err) + + select { + case <-doneChan: + case <-time.After(200 * time.Millisecond): + // Expected timeout, meaning the subscription was successfully unsubscribed + t.Errorf("timeout") + } +} diff --git a/relay/client/picker.go b/relay/client/picker.go index eb5062dbb..9565425a8 100644 --- a/relay/client/picker.go +++ b/relay/client/picker.go @@ -70,8 +70,8 @@ func (sp *ServerPicker) PickServer(parentCtx context.Context) (*Client, error) { func (sp *ServerPicker) startConnection(ctx context.Context, resultChan chan connResult, url string) { log.Infof("try to connecting to relay server: %s", url) - relayClient := NewClient(ctx, url, sp.TokenStore, sp.PeerID) - err := relayClient.Connect() + relayClient := NewClient(url, sp.TokenStore, sp.PeerID) + err := relayClient.Connect(ctx) resultChan <- connResult{ RelayClient: relayClient, Url: url, diff --git a/relay/cmd/root.go b/relay/cmd/root.go index d603ff73b..15090024c 100644 --- a/relay/cmd/root.go +++ b/relay/cmd/root.go @@ -141,7 +141,14 @@ func execute(cmd *cobra.Command, args []string) error { hashedSecret := sha256.Sum256([]byte(cobraConfig.AuthSecret)) authenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour) - srv, err := server.NewServer(metricsServer.Meter, cobraConfig.ExposedAddress, tlsSupport, authenticator) + cfg := server.Config{ + Meter: metricsServer.Meter, + ExposedAddress: cobraConfig.ExposedAddress, + AuthValidator: authenticator, + TLSSupport: tlsSupport, + } + + srv, err := server.NewServer(cfg) if err != nil { log.Debugf("failed to create relay server: %v", err) return fmt.Errorf("failed to create relay server: %v", err) diff --git a/relay/messages/id.go b/relay/messages/id.go index e2162cd3b..96ace3478 100644 --- a/relay/messages/id.go +++ b/relay/messages/id.go @@ -8,24 +8,24 @@ import ( const ( prefixLength = 4 - IDSize = prefixLength + sha256.Size + peerIDSize = prefixLength + sha256.Size ) var ( prefix = []byte("sha-") // 4 bytes ) -// HashID generates a sha256 hash from the peerID and returns the hash and the human-readable string -func HashID(peerID string) ([]byte, string) { - idHash := sha256.Sum256([]byte(peerID)) - idHashString := string(prefix) + base64.StdEncoding.EncodeToString(idHash[:]) - var prefixedHash []byte - prefixedHash = append(prefixedHash, prefix...) - prefixedHash = append(prefixedHash, idHash[:]...) - return prefixedHash, idHashString +type PeerID [peerIDSize]byte + +func (p PeerID) String() string { + return fmt.Sprintf("%s%s", p[:prefixLength], base64.StdEncoding.EncodeToString(p[prefixLength:])) } -// HashIDToString converts a hash to a human-readable string -func HashIDToString(idHash []byte) string { - return fmt.Sprintf("%s%s", idHash[:prefixLength], base64.StdEncoding.EncodeToString(idHash[prefixLength:])) +// HashID generates a sha256 hash from the peerID and returns the hash and the human-readable string +func HashID(peerID string) PeerID { + idHash := sha256.Sum256([]byte(peerID)) + var prefixedHash [peerIDSize]byte + copy(prefixedHash[:prefixLength], prefix) + copy(prefixedHash[prefixLength:], idHash[:]) + return prefixedHash } diff --git a/relay/messages/id_test.go b/relay/messages/id_test.go deleted file mode 100644 index 271a8f90d..000000000 --- a/relay/messages/id_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package messages - -import ( - "testing" -) - -func TestHashID(t *testing.T) { - hashedID, hashedStringId := HashID("alice") - enc := HashIDToString(hashedID) - if enc != hashedStringId { - t.Errorf("expected %s, got %s", hashedStringId, enc) - } -} diff --git a/relay/messages/message.go b/relay/messages/message.go index 7794c57bc..54671f5df 100644 --- a/relay/messages/message.go +++ b/relay/messages/message.go @@ -9,19 +9,26 @@ import ( const ( MaxHandshakeSize = 212 MaxHandshakeRespSize = 8192 + MaxMessageSize = 8820 CurrentProtocolVersion = 1 MsgTypeUnknown MsgType = 0 // Deprecated: Use MsgTypeAuth instead. - MsgTypeHello MsgType = 1 + MsgTypeHello = 1 // Deprecated: Use MsgTypeAuthResponse instead. - MsgTypeHelloResponse MsgType = 2 - MsgTypeTransport MsgType = 3 - MsgTypeClose MsgType = 4 - MsgTypeHealthCheck MsgType = 5 - MsgTypeAuth = 6 - MsgTypeAuthResponse = 7 + MsgTypeHelloResponse = 2 + MsgTypeTransport = 3 + MsgTypeClose = 4 + MsgTypeHealthCheck = 5 + MsgTypeAuth = 6 + MsgTypeAuthResponse = 7 + + // Peers state messages + MsgTypeSubscribePeerState = 8 + MsgTypeUnsubscribePeerState = 9 + MsgTypePeersOnline = 10 + MsgTypePeersWentOffline = 11 // base size of the message sizeOfVersionByte = 1 @@ -30,17 +37,17 @@ const ( // auth message sizeOfMagicByte = 4 - headerSizeAuth = sizeOfMagicByte + IDSize + headerSizeAuth = sizeOfMagicByte + peerIDSize offsetMagicByte = sizeOfProtoHeader offsetAuthPeerID = sizeOfProtoHeader + sizeOfMagicByte headerTotalSizeAuth = sizeOfProtoHeader + headerSizeAuth // hello message - headerSizeHello = sizeOfMagicByte + IDSize + headerSizeHello = sizeOfMagicByte + peerIDSize headerSizeHelloResp = 0 // transport - headerSizeTransport = IDSize + headerSizeTransport = peerIDSize offsetTransportID = sizeOfProtoHeader headerTotalSizeTransport = sizeOfProtoHeader + headerSizeTransport ) @@ -72,6 +79,14 @@ func (m MsgType) String() string { return "close" case MsgTypeHealthCheck: return "health check" + case MsgTypeSubscribePeerState: + return "subscribe peer state" + case MsgTypeUnsubscribePeerState: + return "unsubscribe peer state" + case MsgTypePeersOnline: + return "peers online" + case MsgTypePeersWentOffline: + return "peers went offline" default: return "unknown" } @@ -102,7 +117,9 @@ func DetermineClientMessageType(msg []byte) (MsgType, error) { MsgTypeAuth, MsgTypeTransport, MsgTypeClose, - MsgTypeHealthCheck: + MsgTypeHealthCheck, + MsgTypeSubscribePeerState, + MsgTypeUnsubscribePeerState: return msgType, nil default: return MsgTypeUnknown, fmt.Errorf("invalid msg type %d", msgType) @@ -122,7 +139,9 @@ func DetermineServerMessageType(msg []byte) (MsgType, error) { MsgTypeAuthResponse, MsgTypeTransport, MsgTypeClose, - MsgTypeHealthCheck: + MsgTypeHealthCheck, + MsgTypePeersOnline, + MsgTypePeersWentOffline: return msgType, nil default: return MsgTypeUnknown, fmt.Errorf("invalid msg type %d", msgType) @@ -135,11 +154,7 @@ func DetermineServerMessageType(msg []byte) (MsgType, error) { // message is used to authenticate the client with the server. The authentication is done using an HMAC method. // The protocol does not limit to use HMAC, it can be any other method. If the authentication failed the server will // close the network connection without any response. -func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) { - if len(peerID) != IDSize { - return nil, fmt.Errorf("invalid peerID length: %d", len(peerID)) - } - +func MarshalHelloMsg(peerID PeerID, additions []byte) ([]byte, error) { msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, sizeOfProtoHeader+headerSizeHello+len(additions)) msg[0] = byte(CurrentProtocolVersion) @@ -147,7 +162,7 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) { copy(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader) - msg = append(msg, peerID...) + msg = append(msg, peerID[:]...) msg = append(msg, additions...) return msg, nil @@ -156,7 +171,7 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) { // Deprecated: Use UnmarshalAuthMsg instead. // UnmarshalHelloMsg extracts peerID and the additional data from the hello message. The Additional data is used to // authenticate the client with the server. -func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) { +func UnmarshalHelloMsg(msg []byte) (*PeerID, []byte, error) { if len(msg) < sizeOfProtoHeader+headerSizeHello { return nil, nil, ErrInvalidMessageLength } @@ -164,7 +179,9 @@ func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) { return nil, nil, errors.New("invalid magic header") } - return msg[sizeOfProtoHeader+sizeOfMagicByte : sizeOfProtoHeader+headerSizeHello], msg[headerSizeHello:], nil + peerID := PeerID(msg[sizeOfProtoHeader+sizeOfMagicByte : sizeOfProtoHeader+headerSizeHello]) + + return &peerID, msg[headerSizeHello:], nil } // Deprecated: Use MarshalAuthResponse instead. @@ -197,34 +214,33 @@ func UnmarshalHelloResponse(msg []byte) ([]byte, error) { // message is used to authenticate the client with the server. The authentication is done using an HMAC method. // The protocol does not limit to use HMAC, it can be any other method. If the authentication failed the server will // close the network connection without any response. -func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) { - if len(peerID) != IDSize { - return nil, fmt.Errorf("invalid peerID length: %d", len(peerID)) +func MarshalAuthMsg(peerID PeerID, authPayload []byte) ([]byte, error) { + if headerTotalSizeAuth+len(authPayload) > MaxHandshakeSize { + return nil, fmt.Errorf("too large auth payload") } - msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, headerTotalSizeAuth+len(authPayload)) - + msg := make([]byte, headerTotalSizeAuth+len(authPayload)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeAuth) - copy(msg[sizeOfProtoHeader:], magicHeader) - - msg = append(msg, peerID...) - msg = append(msg, authPayload...) - + copy(msg[offsetAuthPeerID:], peerID[:]) + copy(msg[headerTotalSizeAuth:], authPayload) return msg, nil } // UnmarshalAuthMsg extracts peerID and the auth payload from the message -func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) { +func UnmarshalAuthMsg(msg []byte) (*PeerID, []byte, error) { if len(msg) < headerTotalSizeAuth { return nil, nil, ErrInvalidMessageLength } + + // Validate the magic header if !bytes.Equal(msg[offsetMagicByte:offsetMagicByte+sizeOfMagicByte], magicHeader) { return nil, nil, errors.New("invalid magic header") } - return msg[offsetAuthPeerID:headerTotalSizeAuth], msg[headerTotalSizeAuth:], nil + peerID := PeerID(msg[offsetAuthPeerID:headerTotalSizeAuth]) + return &peerID, msg[headerTotalSizeAuth:], nil } // MarshalAuthResponse creates a response message to the auth. @@ -268,45 +284,48 @@ func MarshalCloseMsg() []byte { // MarshalTransportMsg creates a transport message. // The transport message is used to exchange data between peers. The message contains the data to be exchanged and the // destination peer hashed ID. -func MarshalTransportMsg(peerID, payload []byte) ([]byte, error) { - if len(peerID) != IDSize { - return nil, fmt.Errorf("invalid peerID length: %d", len(peerID)) - } - - msg := make([]byte, headerTotalSizeTransport, headerTotalSizeTransport+len(payload)) +func MarshalTransportMsg(peerID PeerID, payload []byte) ([]byte, error) { + // todo validate size + msg := make([]byte, headerTotalSizeTransport+len(payload)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeTransport) - copy(msg[sizeOfProtoHeader:], peerID) - msg = append(msg, payload...) - + copy(msg[sizeOfProtoHeader:], peerID[:]) + copy(msg[sizeOfProtoHeader+peerIDSize:], payload) return msg, nil } // UnmarshalTransportMsg extracts the peerID and the payload from the transport message. -func UnmarshalTransportMsg(buf []byte) ([]byte, []byte, error) { +func UnmarshalTransportMsg(buf []byte) (*PeerID, []byte, error) { if len(buf) < headerTotalSizeTransport { return nil, nil, ErrInvalidMessageLength } - return buf[offsetTransportID:headerTotalSizeTransport], buf[headerTotalSizeTransport:], nil + const offsetEnd = offsetTransportID + peerIDSize + var peerID PeerID + copy(peerID[:], buf[offsetTransportID:offsetEnd]) + return &peerID, buf[headerTotalSizeTransport:], nil } // UnmarshalTransportID extracts the peerID from the transport message. -func UnmarshalTransportID(buf []byte) ([]byte, error) { +func UnmarshalTransportID(buf []byte) (*PeerID, error) { if len(buf) < headerTotalSizeTransport { return nil, ErrInvalidMessageLength } - return buf[offsetTransportID:headerTotalSizeTransport], nil + + const offsetEnd = offsetTransportID + peerIDSize + var id PeerID + copy(id[:], buf[offsetTransportID:offsetEnd]) + return &id, nil } // UpdateTransportMsg updates the peerID in the transport message. // With this function the server can reuse the given byte slice to update the peerID in the transport message. So do // need to allocate a new byte slice. -func UpdateTransportMsg(msg []byte, peerID []byte) error { - if len(msg) < offsetTransportID+len(peerID) { +func UpdateTransportMsg(msg []byte, peerID PeerID) error { + if len(msg) < offsetTransportID+peerIDSize { return ErrInvalidMessageLength } - copy(msg[offsetTransportID:], peerID) + copy(msg[offsetTransportID:], peerID[:]) return nil } diff --git a/relay/messages/message_test.go b/relay/messages/message_test.go index 19bede07b..59a89cad1 100644 --- a/relay/messages/message_test.go +++ b/relay/messages/message_test.go @@ -5,7 +5,7 @@ import ( ) func TestMarshalHelloMsg(t *testing.T) { - peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") + peerID := HashID("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") msg, err := MarshalHelloMsg(peerID, nil) if err != nil { t.Fatalf("error: %v", err) @@ -24,13 +24,13 @@ func TestMarshalHelloMsg(t *testing.T) { if err != nil { t.Fatalf("error: %v", err) } - if string(receivedPeerID) != string(peerID) { + if receivedPeerID.String() != peerID.String() { t.Errorf("expected %s, got %s", peerID, receivedPeerID) } } func TestMarshalAuthMsg(t *testing.T) { - peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") + peerID := HashID("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") msg, err := MarshalAuthMsg(peerID, []byte{}) if err != nil { t.Fatalf("error: %v", err) @@ -49,7 +49,7 @@ func TestMarshalAuthMsg(t *testing.T) { if err != nil { t.Fatalf("error: %v", err) } - if string(receivedPeerID) != string(peerID) { + if receivedPeerID.String() != peerID.String() { t.Errorf("expected %s, got %s", peerID, receivedPeerID) } } @@ -80,7 +80,7 @@ func TestMarshalAuthResponse(t *testing.T) { } func TestMarshalTransportMsg(t *testing.T) { - peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") + peerID := HashID("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") payload := []byte("payload") msg, err := MarshalTransportMsg(peerID, payload) if err != nil { @@ -101,7 +101,7 @@ func TestMarshalTransportMsg(t *testing.T) { t.Fatalf("failed to unmarshal transport id: %v", err) } - if string(uPeerID) != string(peerID) { + if uPeerID.String() != peerID.String() { t.Errorf("expected %s, got %s", peerID, uPeerID) } @@ -110,8 +110,8 @@ func TestMarshalTransportMsg(t *testing.T) { t.Fatalf("error: %v", err) } - if string(id) != string(peerID) { - t.Errorf("expected %s, got %s", peerID, id) + if id.String() != peerID.String() { + t.Errorf("expected: '%s', got: '%s'", peerID, id) } if string(respPayload) != string(payload) { diff --git a/relay/messages/peer_state.go b/relay/messages/peer_state.go new file mode 100644 index 000000000..f10bc7bdf --- /dev/null +++ b/relay/messages/peer_state.go @@ -0,0 +1,92 @@ +package messages + +import ( + "fmt" +) + +func MarshalSubPeerStateMsg(ids []PeerID) ([][]byte, error) { + return marshalPeerIDs(ids, byte(MsgTypeSubscribePeerState)) +} + +func UnmarshalSubPeerStateMsg(buf []byte) ([]PeerID, error) { + return unmarshalPeerIDs(buf) +} + +func MarshalUnsubPeerStateMsg(ids []PeerID) ([][]byte, error) { + return marshalPeerIDs(ids, byte(MsgTypeUnsubscribePeerState)) +} + +func UnmarshalUnsubPeerStateMsg(buf []byte) ([]PeerID, error) { + return unmarshalPeerIDs(buf) +} + +func MarshalPeersOnline(ids []PeerID) ([][]byte, error) { + return marshalPeerIDs(ids, byte(MsgTypePeersOnline)) +} + +func UnmarshalPeersOnlineMsg(buf []byte) ([]PeerID, error) { + return unmarshalPeerIDs(buf) +} + +func MarshalPeersWentOffline(ids []PeerID) ([][]byte, error) { + return marshalPeerIDs(ids, byte(MsgTypePeersWentOffline)) +} + +func UnMarshalPeersWentOffline(buf []byte) ([]PeerID, error) { + return unmarshalPeerIDs(buf) +} + +// marshalPeerIDs is a generic function to marshal peer IDs with a specific message type +func marshalPeerIDs(ids []PeerID, msgType byte) ([][]byte, error) { + if len(ids) == 0 { + return nil, fmt.Errorf("no list of peer ids provided") + } + + const maxPeersPerMessage = (MaxMessageSize - sizeOfProtoHeader) / peerIDSize + var messages [][]byte + + for i := 0; i < len(ids); i += maxPeersPerMessage { + end := i + maxPeersPerMessage + if end > len(ids) { + end = len(ids) + } + chunk := ids[i:end] + + totalSize := sizeOfProtoHeader + len(chunk)*peerIDSize + buf := make([]byte, totalSize) + buf[0] = byte(CurrentProtocolVersion) + buf[1] = msgType + + offset := sizeOfProtoHeader + for _, id := range chunk { + copy(buf[offset:], id[:]) + offset += peerIDSize + } + + messages = append(messages, buf) + } + + return messages, nil +} + +// unmarshalPeerIDs is a generic function to unmarshal peer IDs from a buffer +func unmarshalPeerIDs(buf []byte) ([]PeerID, error) { + if len(buf) < sizeOfProtoHeader { + return nil, fmt.Errorf("invalid message format") + } + + if (len(buf)-sizeOfProtoHeader)%peerIDSize != 0 { + return nil, fmt.Errorf("invalid peer list size: %d", len(buf)-sizeOfProtoHeader) + } + + numIDs := (len(buf) - sizeOfProtoHeader) / peerIDSize + + ids := make([]PeerID, numIDs) + offset := sizeOfProtoHeader + for i := 0; i < numIDs; i++ { + copy(ids[i][:], buf[offset:offset+peerIDSize]) + offset += peerIDSize + } + + return ids, nil +} diff --git a/relay/messages/peer_state_test.go b/relay/messages/peer_state_test.go new file mode 100644 index 000000000..9e366da55 --- /dev/null +++ b/relay/messages/peer_state_test.go @@ -0,0 +1,144 @@ +package messages + +import ( + "bytes" + "testing" +) + +const ( + testPeerCount = 10 +) + +// Helper function to generate test PeerIDs +func generateTestPeerIDs(n int) []PeerID { + ids := make([]PeerID, n) + for i := 0; i < n; i++ { + for j := 0; j < peerIDSize; j++ { + ids[i][j] = byte(i + j) + } + } + return ids +} + +// Helper function to compare slices of PeerID +func peerIDEqual(a, b []PeerID) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !bytes.Equal(a[i][:], b[i][:]) { + return false + } + } + return true +} + +func TestMarshalUnmarshalSubPeerState(t *testing.T) { + ids := generateTestPeerIDs(testPeerCount) + + msgs, err := MarshalSubPeerStateMsg(ids) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var allIDs []PeerID + for _, msg := range msgs { + decoded, err := UnmarshalSubPeerStateMsg(msg) + if err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + allIDs = append(allIDs, decoded...) + } + + if !peerIDEqual(ids, allIDs) { + t.Errorf("expected %v, got %v", ids, allIDs) + } +} + +func TestMarshalSubPeerState_EmptyInput(t *testing.T) { + _, err := MarshalSubPeerStateMsg([]PeerID{}) + if err == nil { + t.Errorf("expected error for empty input") + } +} + +func TestUnmarshalSubPeerState_Invalid(t *testing.T) { + // Too short + _, err := UnmarshalSubPeerStateMsg([]byte{1}) + if err == nil { + t.Errorf("expected error for short input") + } + + // Misaligned length + buf := make([]byte, sizeOfProtoHeader+1) + _, err = UnmarshalSubPeerStateMsg(buf) + if err == nil { + t.Errorf("expected error for misaligned input") + } +} + +func TestMarshalUnmarshalPeersOnline(t *testing.T) { + ids := generateTestPeerIDs(testPeerCount) + + msgs, err := MarshalPeersOnline(ids) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var allIDs []PeerID + for _, msg := range msgs { + decoded, err := UnmarshalPeersOnlineMsg(msg) + if err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + allIDs = append(allIDs, decoded...) + } + + if !peerIDEqual(ids, allIDs) { + t.Errorf("expected %v, got %v", ids, allIDs) + } +} + +func TestMarshalPeersOnline_EmptyInput(t *testing.T) { + _, err := MarshalPeersOnline([]PeerID{}) + if err == nil { + t.Errorf("expected error for empty input") + } +} + +func TestUnmarshalPeersOnline_Invalid(t *testing.T) { + _, err := UnmarshalPeersOnlineMsg([]byte{1}) + if err == nil { + t.Errorf("expected error for short input") + } +} + +func TestMarshalUnmarshalPeersWentOffline(t *testing.T) { + ids := generateTestPeerIDs(testPeerCount) + + msgs, err := MarshalPeersWentOffline(ids) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var allIDs []PeerID + for _, msg := range msgs { + // MarshalPeersWentOffline shares no unmarshal function, so reuse PeersOnline + decoded, err := UnmarshalPeersOnlineMsg(msg) + if err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + allIDs = append(allIDs, decoded...) + } + + if !peerIDEqual(ids, allIDs) { + t.Errorf("expected %v, got %v", ids, allIDs) + } +} + +func TestMarshalPeersWentOffline_EmptyInput(t *testing.T) { + _, err := MarshalPeersWentOffline([]PeerID{}) + if err == nil { + t.Errorf("expected error for empty input") + } +} diff --git a/relay/server/handshake.go b/relay/server/handshake.go index babd6f955..eb72b3bae 100644 --- a/relay/server/handshake.go +++ b/relay/server/handshake.go @@ -6,7 +6,6 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/relay/auth" "github.com/netbirdio/netbird/relay/messages" //nolint:staticcheck "github.com/netbirdio/netbird/relay/messages/address" @@ -14,6 +13,12 @@ import ( authmsg "github.com/netbirdio/netbird/relay/messages/auth" ) +type Validator interface { + Validate(any) error + // Deprecated: Use Validate instead. + ValidateHelloMsgType(any) error +} + // preparedMsg contains the marshalled success response messages type preparedMsg struct { responseHelloMsg []byte @@ -54,14 +59,14 @@ func marshalResponseHelloMsg(instanceURL string) ([]byte, error) { type handshake struct { conn net.Conn - validator auth.Validator + validator Validator preparedMsg *preparedMsg handshakeMethodAuth bool - peerID string + peerID *messages.PeerID } -func (h *handshake) handshakeReceive() ([]byte, error) { +func (h *handshake) handshakeReceive() (*messages.PeerID, error) { buf := make([]byte, messages.MaxHandshakeSize) n, err := h.conn.Read(buf) if err != nil { @@ -80,17 +85,14 @@ func (h *handshake) handshakeReceive() ([]byte, error) { return nil, fmt.Errorf("determine message type from %s: %w", h.conn.RemoteAddr(), err) } - var ( - bytePeerID []byte - peerID string - ) + var peerID *messages.PeerID switch msgType { //nolint:staticcheck case messages.MsgTypeHello: - bytePeerID, peerID, err = h.handleHelloMsg(buf) + peerID, err = h.handleHelloMsg(buf) case messages.MsgTypeAuth: h.handshakeMethodAuth = true - bytePeerID, peerID, err = h.handleAuthMsg(buf) + peerID, err = h.handleAuthMsg(buf) default: return nil, fmt.Errorf("invalid message type %d from %s", msgType, h.conn.RemoteAddr()) } @@ -98,7 +100,7 @@ func (h *handshake) handshakeReceive() ([]byte, error) { return nil, err } h.peerID = peerID - return bytePeerID, nil + return peerID, nil } func (h *handshake) handshakeResponse() error { @@ -116,40 +118,37 @@ func (h *handshake) handshakeResponse() error { return nil } -func (h *handshake) handleHelloMsg(buf []byte) ([]byte, string, error) { +func (h *handshake) handleHelloMsg(buf []byte) (*messages.PeerID, error) { //nolint:staticcheck - rawPeerID, authData, err := messages.UnmarshalHelloMsg(buf) + peerID, authData, err := messages.UnmarshalHelloMsg(buf) if err != nil { - return nil, "", fmt.Errorf("unmarshal hello message: %w", err) + return nil, fmt.Errorf("unmarshal hello message: %w", err) } - peerID := messages.HashIDToString(rawPeerID) log.Warnf("peer %s (%s) is using deprecated initial message type", peerID, h.conn.RemoteAddr()) authMsg, err := authmsg.UnmarshalMsg(authData) if err != nil { - return nil, "", fmt.Errorf("unmarshal auth message: %w", err) + return nil, fmt.Errorf("unmarshal auth message: %w", err) } //nolint:staticcheck if err := h.validator.ValidateHelloMsgType(authMsg.AdditionalData); err != nil { - return nil, "", fmt.Errorf("validate %s (%s): %w", peerID, h.conn.RemoteAddr(), err) + return nil, fmt.Errorf("validate %s (%s): %w", peerID, h.conn.RemoteAddr(), err) } - return rawPeerID, peerID, nil + return peerID, nil } -func (h *handshake) handleAuthMsg(buf []byte) ([]byte, string, error) { +func (h *handshake) handleAuthMsg(buf []byte) (*messages.PeerID, error) { rawPeerID, authPayload, err := messages.UnmarshalAuthMsg(buf) if err != nil { - return nil, "", fmt.Errorf("unmarshal hello message: %w", err) + return nil, fmt.Errorf("unmarshal hello message: %w", err) } - peerID := messages.HashIDToString(rawPeerID) - if err := h.validator.Validate(authPayload); err != nil { - return nil, "", fmt.Errorf("validate %s (%s): %w", peerID, h.conn.RemoteAddr(), err) + return nil, fmt.Errorf("validate %s (%s): %w", rawPeerID.String(), h.conn.RemoteAddr(), err) } - return rawPeerID, peerID, nil + return rawPeerID, nil } diff --git a/relay/server/peer.go b/relay/server/peer.go index aa9790f63..c6fa8508f 100644 --- a/relay/server/peer.go +++ b/relay/server/peer.go @@ -12,43 +12,50 @@ import ( "github.com/netbirdio/netbird/relay/healthcheck" "github.com/netbirdio/netbird/relay/messages" "github.com/netbirdio/netbird/relay/metrics" + "github.com/netbirdio/netbird/relay/server/store" ) const ( - bufferSize = 8820 + bufferSize = messages.MaxMessageSize errCloseConn = "failed to close connection to peer: %s" ) // Peer represents a peer connection type Peer struct { - metrics *metrics.Metrics - log *log.Entry - idS string - idB []byte - conn net.Conn - connMu sync.RWMutex - store *Store + metrics *metrics.Metrics + log *log.Entry + id messages.PeerID + conn net.Conn + connMu sync.RWMutex + store *store.Store + notifier *store.PeerNotifier + + peersListener *store.Listener } // NewPeer creates a new Peer instance and prepare custom logging -func NewPeer(metrics *metrics.Metrics, id []byte, conn net.Conn, store *Store) *Peer { - stringID := messages.HashIDToString(id) - return &Peer{ - metrics: metrics, - log: log.WithField("peer_id", stringID), - idS: stringID, - idB: id, - conn: conn, - store: store, +func NewPeer(metrics *metrics.Metrics, id messages.PeerID, conn net.Conn, store *store.Store, notifier *store.PeerNotifier) *Peer { + p := &Peer{ + metrics: metrics, + log: log.WithField("peer_id", id.String()), + id: id, + conn: conn, + store: store, + notifier: notifier, } + + return p } // Work reads data from the connection // It manages the protocol (healthcheck, transport, close). Read the message and determine the message type and handle // the message accordingly. func (p *Peer) Work() { + p.peersListener = p.notifier.NewListener(p.sendPeersOnline, p.sendPeersWentOffline) defer func() { + p.notifier.RemoveListener(p.peersListener) + if err := p.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { p.log.Errorf(errCloseConn, err) } @@ -94,6 +101,10 @@ func (p *Peer) Work() { } } +func (p *Peer) ID() messages.PeerID { + return p.id +} + func (p *Peer) handleMsgType(ctx context.Context, msgType messages.MsgType, hc *healthcheck.Sender, n int, msg []byte) { switch msgType { case messages.MsgTypeHealthCheck: @@ -107,6 +118,10 @@ func (p *Peer) handleMsgType(ctx context.Context, msgType messages.MsgType, hc * if err := p.conn.Close(); err != nil { log.Errorf(errCloseConn, err) } + case messages.MsgTypeSubscribePeerState: + p.handleSubscribePeerState(msg) + case messages.MsgTypeUnsubscribePeerState: + p.handleUnsubscribePeerState(msg) default: p.log.Warnf("received unexpected message type: %s", msgType) } @@ -145,7 +160,7 @@ func (p *Peer) Close() { // String returns the peer ID func (p *Peer) String() string { - return p.idS + return p.id.String() } func (p *Peer) writeWithTimeout(ctx context.Context, buf []byte) error { @@ -197,14 +212,14 @@ func (p *Peer) handleTransportMsg(msg []byte) { return } - stringPeerID := messages.HashIDToString(peerID) - dp, ok := p.store.Peer(stringPeerID) + item, ok := p.store.Peer(*peerID) if !ok { - p.log.Debugf("peer not found: %s", stringPeerID) + p.log.Debugf("peer not found: %s", peerID) return } + dp := item.(*Peer) - err = messages.UpdateTransportMsg(msg, p.idB) + err = messages.UpdateTransportMsg(msg, p.id) if err != nil { p.log.Errorf("failed to update transport message: %s", err) return @@ -217,3 +232,57 @@ func (p *Peer) handleTransportMsg(msg []byte) { } p.metrics.TransferBytesSent.Add(context.Background(), int64(n)) } + +func (p *Peer) handleSubscribePeerState(msg []byte) { + peerIDs, err := messages.UnmarshalSubPeerStateMsg(msg) + if err != nil { + p.log.Errorf("failed to unmarshal open connection message: %s", err) + return + } + + p.log.Debugf("received subscription message for %d peers", len(peerIDs)) + onlinePeers := p.peersListener.AddInterestedPeers(peerIDs) + if len(onlinePeers) == 0 { + return + } + p.log.Debugf("response with %d online peers", len(onlinePeers)) + p.sendPeersOnline(onlinePeers) +} + +func (p *Peer) handleUnsubscribePeerState(msg []byte) { + peerIDs, err := messages.UnmarshalUnsubPeerStateMsg(msg) + if err != nil { + p.log.Errorf("failed to unmarshal open connection message: %s", err) + return + } + + p.peersListener.RemoveInterestedPeer(peerIDs) +} + +func (p *Peer) sendPeersOnline(peers []messages.PeerID) { + msgs, err := messages.MarshalPeersOnline(peers) + if err != nil { + p.log.Errorf("failed to marshal peer location message: %s", err) + return + } + + for n, msg := range msgs { + if _, err := p.Write(msg); err != nil { + p.log.Errorf("failed to write %d. peers offline message: %s", n, err) + } + } +} + +func (p *Peer) sendPeersWentOffline(peers []messages.PeerID) { + msgs, err := messages.MarshalPeersWentOffline(peers) + if err != nil { + p.log.Errorf("failed to marshal peer location message: %s", err) + return + } + + for n, msg := range msgs { + if _, err := p.Write(msg); err != nil { + p.log.Errorf("failed to write %d. peers offline message: %s", n, err) + } + } +} diff --git a/relay/server/relay.go b/relay/server/relay.go index a5e77bc61..93fb00edb 100644 --- a/relay/server/relay.go +++ b/relay/server/relay.go @@ -4,26 +4,55 @@ import ( "context" "fmt" "net" - "net/url" - "strings" "sync" "time" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" - "github.com/netbirdio/netbird/relay/auth" //nolint:staticcheck "github.com/netbirdio/netbird/relay/metrics" + "github.com/netbirdio/netbird/relay/server/store" ) +type Config struct { + Meter metric.Meter + ExposedAddress string + TLSSupport bool + AuthValidator Validator + + instanceURL string +} + +func (c *Config) validate() error { + if c.Meter == nil { + c.Meter = otel.Meter("") + } + if c.ExposedAddress == "" { + return fmt.Errorf("exposed address is required") + } + + instanceURL, err := getInstanceURL(c.ExposedAddress, c.TLSSupport) + if err != nil { + return fmt.Errorf("invalid url: %v", err) + } + c.instanceURL = instanceURL + + if c.AuthValidator == nil { + return fmt.Errorf("auth validator is required") + } + return nil +} + // Relay represents the relay server type Relay struct { metrics *metrics.Metrics metricsCancel context.CancelFunc - validator auth.Validator + validator Validator - store *Store + store *store.Store + notifier *store.PeerNotifier instanceURL string preparedMsg *preparedMsg @@ -31,40 +60,40 @@ type Relay struct { closeMu sync.RWMutex } -// NewRelay creates a new Relay instance +// NewRelay creates and returns a new Relay instance. // // Parameters: -// meter: An instance of metric.Meter from the go.opentelemetry.io/otel/metric package. It is used to create and manage -// metrics for the relay server. -// exposedAddress: A string representing the address that the relay server is exposed on. The client will use this -// address as the relay server's instance URL. -// tlsSupport: A boolean indicating whether the relay server supports TLS (Transport Layer Security) or not. The -// instance URL depends on this value. -// validator: An instance of auth.Validator from the auth package. It is used to validate the authentication of the -// peers. +// +// config: A Config struct that holds the configuration needed to initialize the relay server. +// - Meter: A metric.Meter used for emitting metrics. If not set, a default no-op meter will be used. +// - ExposedAddress: The external address clients use to reach this relay. Required. +// - TLSSupport: A boolean indicating if the relay uses TLS. Affects the generated instance URL. +// - AuthValidator: A Validator implementation used to authenticate peers. Required. // // Returns: -// A pointer to a Relay instance and an error. If the Relay instance is successfully created, the error is nil. -// Otherwise, the error contains the details of what went wrong. -func NewRelay(meter metric.Meter, exposedAddress string, tlsSupport bool, validator auth.Validator) (*Relay, error) { +// +// A pointer to a Relay instance and an error. If initialization is successful, the error will be nil; +// otherwise, it will contain the reason the relay could not be created (e.g., invalid configuration). +func NewRelay(config Config) (*Relay, error) { + if err := config.validate(); err != nil { + return nil, fmt.Errorf("invalid config: %v", err) + } + ctx, metricsCancel := context.WithCancel(context.Background()) - m, err := metrics.NewMetrics(ctx, meter) + m, err := metrics.NewMetrics(ctx, config.Meter) if err != nil { metricsCancel() return nil, fmt.Errorf("creating app metrics: %v", err) } + peerStore := store.NewStore() r := &Relay{ metrics: m, metricsCancel: metricsCancel, - validator: validator, - store: NewStore(), - } - - r.instanceURL, err = getInstanceURL(exposedAddress, tlsSupport) - if err != nil { - metricsCancel() - return nil, fmt.Errorf("get instance URL: %v", err) + validator: config.AuthValidator, + instanceURL: config.instanceURL, + store: peerStore, + notifier: store.NewPeerNotifier(peerStore), } r.preparedMsg, err = newPreparedMsg(r.instanceURL) @@ -76,32 +105,6 @@ func NewRelay(meter metric.Meter, exposedAddress string, tlsSupport bool, valida return r, nil } -// getInstanceURL checks if user supplied a URL scheme otherwise adds to the -// provided address according to TLS definition and parses the address before returning it -func getInstanceURL(exposedAddress string, tlsSupported bool) (string, error) { - addr := exposedAddress - split := strings.Split(exposedAddress, "://") - switch { - case len(split) == 1 && tlsSupported: - addr = "rels://" + exposedAddress - case len(split) == 1 && !tlsSupported: - addr = "rel://" + exposedAddress - case len(split) > 2: - return "", fmt.Errorf("invalid exposed address: %s", exposedAddress) - } - - parsedURL, err := url.ParseRequestURI(addr) - if err != nil { - return "", fmt.Errorf("invalid exposed address: %v", err) - } - - if parsedURL.Scheme != "rel" && parsedURL.Scheme != "rels" { - return "", fmt.Errorf("invalid scheme: %s", parsedURL.Scheme) - } - - return parsedURL.String(), nil -} - // Accept start to handle a new peer connection func (r *Relay) Accept(conn net.Conn) { acceptTime := time.Now() @@ -125,14 +128,17 @@ func (r *Relay) Accept(conn net.Conn) { return } - peer := NewPeer(r.metrics, peerID, conn, r.store) + peer := NewPeer(r.metrics, *peerID, conn, r.store, r.notifier) peer.log.Infof("peer connected from: %s", conn.RemoteAddr()) storeTime := time.Now() r.store.AddPeer(peer) + r.notifier.PeerCameOnline(peer.ID()) + r.metrics.RecordPeerStoreTime(time.Since(storeTime)) r.metrics.PeerConnected(peer.String()) go func() { peer.Work() + r.notifier.PeerWentOffline(peer.ID()) r.store.DeletePeer(peer) peer.log.Debugf("relay connection closed") r.metrics.PeerDisconnected(peer.String()) @@ -154,12 +160,12 @@ func (r *Relay) Shutdown(ctx context.Context) { wg := sync.WaitGroup{} peers := r.store.Peers() - for _, peer := range peers { + for _, v := range peers { wg.Add(1) go func(p *Peer) { p.CloseGracefully(ctx) wg.Done() - }(peer) + }(v.(*Peer)) } wg.Wait() r.metricsCancel() diff --git a/relay/server/server.go b/relay/server/server.go index 10aabcace..f0b480ee4 100644 --- a/relay/server/server.go +++ b/relay/server/server.go @@ -6,15 +6,12 @@ import ( "sync" "github.com/hashicorp/go-multierror" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/metric" - nberrors "github.com/netbirdio/netbird/client/errors" - "github.com/netbirdio/netbird/relay/auth" "github.com/netbirdio/netbird/relay/server/listener" "github.com/netbirdio/netbird/relay/server/listener/quic" "github.com/netbirdio/netbird/relay/server/listener/ws" quictls "github.com/netbirdio/netbird/relay/tls" + log "github.com/sirupsen/logrus" ) // ListenerConfig is the configuration for the listener. @@ -33,13 +30,22 @@ type Server struct { listeners []listener.Listener } -// NewServer creates a new relay server instance. -// meter: the OpenTelemetry meter -// exposedAddress: this address will be used as the instance URL. It should be a domain:port format. -// tlsSupport: if true, the server will support TLS -// authValidator: the auth validator to use for the server -func NewServer(meter metric.Meter, exposedAddress string, tlsSupport bool, authValidator auth.Validator) (*Server, error) { - relay, err := NewRelay(meter, exposedAddress, tlsSupport, authValidator) +// NewServer creates and returns a new relay server instance. +// +// Parameters: +// +// config: A Config struct containing the necessary configuration: +// - Meter: An OpenTelemetry metric.Meter used for recording metrics. If nil, a default no-op meter is used. +// - ExposedAddress: The public address (in domain:port format) used as the server's instance URL. Required. +// - TLSSupport: A boolean indicating whether TLS is enabled for the server. +// - AuthValidator: A Validator used to authenticate peers. Required. +// +// Returns: +// +// A pointer to a Server instance and an error. If the configuration is valid and initialization succeeds, +// the returned error will be nil. Otherwise, the error will describe the problem. +func NewServer(config Config) (*Server, error) { + relay, err := NewRelay(config) if err != nil { return nil, err } diff --git a/relay/server/store/listener.go b/relay/server/store/listener.go new file mode 100644 index 000000000..e5f455795 --- /dev/null +++ b/relay/server/store/listener.go @@ -0,0 +1,121 @@ +package store + +import ( + "context" + "sync" + + "github.com/netbirdio/netbird/relay/messages" +) + +type Listener struct { + store *Store + + onlineChan chan messages.PeerID + offlineChan chan messages.PeerID + interestedPeersForOffline map[messages.PeerID]struct{} + interestedPeersForOnline map[messages.PeerID]struct{} + mu sync.RWMutex + + listenerCtx context.Context +} + +func newListener(store *Store) *Listener { + l := &Listener{ + store: store, + + onlineChan: make(chan messages.PeerID, 244), //244 is the message size limit in the relay protocol + offlineChan: make(chan messages.PeerID, 244), //244 is the message size limit in the relay protocol + interestedPeersForOffline: make(map[messages.PeerID]struct{}), + interestedPeersForOnline: make(map[messages.PeerID]struct{}), + } + + return l +} + +func (l *Listener) AddInterestedPeers(peerIDs []messages.PeerID) []messages.PeerID { + availablePeers := make([]messages.PeerID, 0) + l.mu.Lock() + defer l.mu.Unlock() + + for _, id := range peerIDs { + l.interestedPeersForOnline[id] = struct{}{} + l.interestedPeersForOffline[id] = struct{}{} + } + + // collect online peers to response back to the caller + for _, id := range peerIDs { + _, ok := l.store.Peer(id) + if !ok { + continue + } + + availablePeers = append(availablePeers, id) + } + return availablePeers +} + +func (l *Listener) RemoveInterestedPeer(peerIDs []messages.PeerID) { + l.mu.Lock() + defer l.mu.Unlock() + + for _, id := range peerIDs { + delete(l.interestedPeersForOffline, id) + delete(l.interestedPeersForOnline, id) + + } +} + +func (l *Listener) listenForEvents(ctx context.Context, onPeersComeOnline, onPeersWentOffline func([]messages.PeerID)) { + l.listenerCtx = ctx + for { + select { + case <-ctx.Done(): + return + case pID := <-l.onlineChan: + peers := make([]messages.PeerID, 0) + peers = append(peers, pID) + + for len(l.onlineChan) > 0 { + pID = <-l.onlineChan + peers = append(peers, pID) + } + + onPeersComeOnline(peers) + case pID := <-l.offlineChan: + peers := make([]messages.PeerID, 0) + peers = append(peers, pID) + + for len(l.offlineChan) > 0 { + pID = <-l.offlineChan + peers = append(peers, pID) + } + + onPeersWentOffline(peers) + } + } +} + +func (l *Listener) peerWentOffline(peerID messages.PeerID) { + l.mu.RLock() + defer l.mu.RUnlock() + + if _, ok := l.interestedPeersForOffline[peerID]; ok { + select { + case l.offlineChan <- peerID: + case <-l.listenerCtx.Done(): + } + } +} + +func (l *Listener) peerComeOnline(peerID messages.PeerID) { + l.mu.Lock() + defer l.mu.Unlock() + + if _, ok := l.interestedPeersForOnline[peerID]; ok { + select { + case l.onlineChan <- peerID: + case <-l.listenerCtx.Done(): + } + delete(l.interestedPeersForOnline, peerID) + } +} diff --git a/relay/server/store/notifier.go b/relay/server/store/notifier.go new file mode 100644 index 000000000..d04db478b --- /dev/null +++ b/relay/server/store/notifier.go @@ -0,0 +1,64 @@ +package store + +import ( + "context" + "sync" + + "github.com/netbirdio/netbird/relay/messages" +) + +type PeerNotifier struct { + store *Store + + listeners map[*Listener]context.CancelFunc + listenersMutex sync.RWMutex +} + +func NewPeerNotifier(store *Store) *PeerNotifier { + pn := &PeerNotifier{ + store: store, + listeners: make(map[*Listener]context.CancelFunc), + } + return pn +} + +func (pn *PeerNotifier) NewListener(onPeersComeOnline, onPeersWentOffline func([]messages.PeerID)) *Listener { + ctx, cancel := context.WithCancel(context.Background()) + listener := newListener(pn.store) + go listener.listenForEvents(ctx, onPeersComeOnline, onPeersWentOffline) + + pn.listenersMutex.Lock() + pn.listeners[listener] = cancel + pn.listenersMutex.Unlock() + return listener +} + +func (pn *PeerNotifier) RemoveListener(listener *Listener) { + pn.listenersMutex.Lock() + defer pn.listenersMutex.Unlock() + + cancel, ok := pn.listeners[listener] + if !ok { + return + } + cancel() + delete(pn.listeners, listener) +} + +func (pn *PeerNotifier) PeerWentOffline(peerID messages.PeerID) { + pn.listenersMutex.RLock() + defer pn.listenersMutex.RUnlock() + + for listener := range pn.listeners { + listener.peerWentOffline(peerID) + } +} + +func (pn *PeerNotifier) PeerCameOnline(peerID messages.PeerID) { + pn.listenersMutex.RLock() + defer pn.listenersMutex.RUnlock() + + for listener := range pn.listeners { + listener.peerComeOnline(peerID) + } +} diff --git a/relay/server/store.go b/relay/server/store/store.go similarity index 61% rename from relay/server/store.go rename to relay/server/store/store.go index 4288e62c5..c19fb416f 100644 --- a/relay/server/store.go +++ b/relay/server/store/store.go @@ -1,41 +1,48 @@ -package server +package store import ( "sync" + + "github.com/netbirdio/netbird/relay/messages" ) +type IPeer interface { + Close() + ID() messages.PeerID +} + // Store is a thread-safe store of peers // It is used to store the peers that are connected to the relay server type Store struct { - peers map[string]*Peer // consider to use [32]byte as key. The Peer(id string) would be faster + peers map[messages.PeerID]IPeer peersLock sync.RWMutex } // NewStore creates a new Store instance func NewStore() *Store { return &Store{ - peers: make(map[string]*Peer), + peers: make(map[messages.PeerID]IPeer), } } // AddPeer adds a peer to the store -func (s *Store) AddPeer(peer *Peer) { +func (s *Store) AddPeer(peer IPeer) { s.peersLock.Lock() defer s.peersLock.Unlock() - odlPeer, ok := s.peers[peer.String()] + odlPeer, ok := s.peers[peer.ID()] if ok { odlPeer.Close() } - s.peers[peer.String()] = peer + s.peers[peer.ID()] = peer } // DeletePeer deletes a peer from the store -func (s *Store) DeletePeer(peer *Peer) { +func (s *Store) DeletePeer(peer IPeer) { s.peersLock.Lock() defer s.peersLock.Unlock() - dp, ok := s.peers[peer.String()] + dp, ok := s.peers[peer.ID()] if !ok { return } @@ -43,11 +50,11 @@ func (s *Store) DeletePeer(peer *Peer) { return } - delete(s.peers, peer.String()) + delete(s.peers, peer.ID()) } // Peer returns a peer by its ID -func (s *Store) Peer(id string) (*Peer, bool) { +func (s *Store) Peer(id messages.PeerID) (IPeer, bool) { s.peersLock.RLock() defer s.peersLock.RUnlock() @@ -56,11 +63,11 @@ func (s *Store) Peer(id string) (*Peer, bool) { } // Peers returns all the peers in the store -func (s *Store) Peers() []*Peer { +func (s *Store) Peers() []IPeer { s.peersLock.RLock() defer s.peersLock.RUnlock() - peers := make([]*Peer, 0, len(s.peers)) + peers := make([]IPeer, 0, len(s.peers)) for _, p := range s.peers { peers = append(peers, p) } diff --git a/relay/server/store/store_test.go b/relay/server/store/store_test.go new file mode 100644 index 000000000..ad549a62c --- /dev/null +++ b/relay/server/store/store_test.go @@ -0,0 +1,49 @@ +package store + +import ( + "testing" + + "github.com/netbirdio/netbird/relay/messages" +) + +type MocPeer struct { + id messages.PeerID +} + +func (m *MocPeer) Close() { + +} + +func (m *MocPeer) ID() messages.PeerID { + return m.id +} + +func TestStore_DeletePeer(t *testing.T) { + s := NewStore() + + pID := messages.HashID("peer_one") + p := &MocPeer{id: pID} + s.AddPeer(p) + s.DeletePeer(p) + if _, ok := s.Peer(pID); ok { + t.Errorf("peer was not deleted") + } +} + +func TestStore_DeleteDeprecatedPeer(t *testing.T) { + s := NewStore() + + pID1 := messages.HashID("peer_one") + pID2 := messages.HashID("peer_one") + + p1 := &MocPeer{id: pID1} + p2 := &MocPeer{id: pID2} + + s.AddPeer(p1) + s.AddPeer(p2) + s.DeletePeer(p1) + + if _, ok := s.Peer(pID2); !ok { + t.Errorf("second peer was deleted") + } +} diff --git a/relay/server/store_test.go b/relay/server/store_test.go deleted file mode 100644 index 41c7baa92..000000000 --- a/relay/server/store_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package server - -import ( - "context" - "net" - "testing" - "time" - - "go.opentelemetry.io/otel" - - "github.com/netbirdio/netbird/relay/metrics" -) - -type mockConn struct { -} - -func (m mockConn) Read(b []byte) (n int, err error) { - //TODO implement me - panic("implement me") -} - -func (m mockConn) Write(b []byte) (n int, err error) { - //TODO implement me - panic("implement me") -} - -func (m mockConn) Close() error { - return nil -} - -func (m mockConn) LocalAddr() net.Addr { - //TODO implement me - panic("implement me") -} - -func (m mockConn) RemoteAddr() net.Addr { - //TODO implement me - panic("implement me") -} - -func (m mockConn) SetDeadline(t time.Time) error { - //TODO implement me - panic("implement me") -} - -func (m mockConn) SetReadDeadline(t time.Time) error { - //TODO implement me - panic("implement me") -} - -func (m mockConn) SetWriteDeadline(t time.Time) error { - //TODO implement me - panic("implement me") -} - -func TestStore_DeletePeer(t *testing.T) { - s := NewStore() - - m, _ := metrics.NewMetrics(context.Background(), otel.Meter("")) - - p := NewPeer(m, []byte("peer_one"), nil, nil) - s.AddPeer(p) - s.DeletePeer(p) - if _, ok := s.Peer(p.String()); ok { - t.Errorf("peer was not deleted") - } -} - -func TestStore_DeleteDeprecatedPeer(t *testing.T) { - s := NewStore() - - m, _ := metrics.NewMetrics(context.Background(), otel.Meter("")) - - conn := &mockConn{} - p1 := NewPeer(m, []byte("peer_id"), conn, nil) - p2 := NewPeer(m, []byte("peer_id"), conn, nil) - - s.AddPeer(p1) - s.AddPeer(p2) - s.DeletePeer(p1) - - if _, ok := s.Peer(p2.String()); !ok { - t.Errorf("second peer was deleted") - } -} diff --git a/relay/server/url.go b/relay/server/url.go new file mode 100644 index 000000000..9cbf44642 --- /dev/null +++ b/relay/server/url.go @@ -0,0 +1,33 @@ +package server + +import ( + "fmt" + "net/url" + "strings" +) + +// getInstanceURL checks if user supplied a URL scheme otherwise adds to the +// provided address according to TLS definition and parses the address before returning it +func getInstanceURL(exposedAddress string, tlsSupported bool) (string, error) { + addr := exposedAddress + split := strings.Split(exposedAddress, "://") + switch { + case len(split) == 1 && tlsSupported: + addr = "rels://" + exposedAddress + case len(split) == 1 && !tlsSupported: + addr = "rel://" + exposedAddress + case len(split) > 2: + return "", fmt.Errorf("invalid exposed address: %s", exposedAddress) + } + + parsedURL, err := url.ParseRequestURI(addr) + if err != nil { + return "", fmt.Errorf("invalid exposed address: %v", err) + } + + if parsedURL.Scheme != "rel" && parsedURL.Scheme != "rels" { + return "", fmt.Errorf("invalid scheme: %s", parsedURL.Scheme) + } + + return parsedURL.String(), nil +} diff --git a/relay/test/benchmark_test.go b/relay/test/benchmark_test.go index ec2aa488c..2e67ab803 100644 --- a/relay/test/benchmark_test.go +++ b/relay/test/benchmark_test.go @@ -12,7 +12,6 @@ import ( "github.com/pion/logging" "github.com/pion/turn/v3" - "go.opentelemetry.io/otel" "github.com/netbirdio/netbird/relay/auth/allow" "github.com/netbirdio/netbird/relay/auth/hmac" @@ -22,7 +21,6 @@ import ( ) var ( - av = &allow.Auth{} hmacTokenStore = &hmac.TokenStore{} pairs = []int{1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100} dataSize = 1024 * 1024 * 10 @@ -70,8 +68,12 @@ func transfer(t *testing.T, testData []byte, peerPairs int) { port := 35000 + peerPairs serverAddress := fmt.Sprintf("127.0.0.1:%d", port) serverConnURL := fmt.Sprintf("rel://%s", serverAddress) - - srv, err := server.NewServer(otel.Meter(""), serverConnURL, false, av) + serverCfg := server.Config{ + ExposedAddress: serverConnURL, + TLSSupport: false, + AuthValidator: &allow.Auth{}, + } + srv, err := server.NewServer(serverCfg) if err != nil { t.Fatalf("failed to create server: %s", err) } @@ -98,8 +100,8 @@ func transfer(t *testing.T, testData []byte, peerPairs int) { clientsSender := make([]*client.Client, peerPairs) for i := 0; i < cap(clientsSender); i++ { - c := client.NewClient(ctx, serverConnURL, hmacTokenStore, "sender-"+fmt.Sprint(i)) - err := c.Connect() + c := client.NewClient(serverConnURL, hmacTokenStore, "sender-"+fmt.Sprint(i)) + err := c.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } @@ -108,8 +110,8 @@ func transfer(t *testing.T, testData []byte, peerPairs int) { clientsReceiver := make([]*client.Client, peerPairs) for i := 0; i < cap(clientsReceiver); i++ { - c := client.NewClient(ctx, serverConnURL, hmacTokenStore, "receiver-"+fmt.Sprint(i)) - err := c.Connect() + c := client.NewClient(serverConnURL, hmacTokenStore, "receiver-"+fmt.Sprint(i)) + err := c.Connect(ctx) if err != nil { t.Fatalf("failed to connect to server: %s", err) } @@ -119,13 +121,13 @@ func transfer(t *testing.T, testData []byte, peerPairs int) { connsSender := make([]net.Conn, 0, peerPairs) connsReceiver := make([]net.Conn, 0, peerPairs) for i := 0; i < len(clientsSender); i++ { - conn, err := clientsSender[i].OpenConn("receiver-" + fmt.Sprint(i)) + conn, err := clientsSender[i].OpenConn(ctx, "receiver-"+fmt.Sprint(i)) if err != nil { t.Fatalf("failed to bind channel: %s", err) } connsSender = append(connsSender, conn) - conn, err = clientsReceiver[i].OpenConn("sender-" + fmt.Sprint(i)) + conn, err = clientsReceiver[i].OpenConn(ctx, "sender-"+fmt.Sprint(i)) if err != nil { t.Fatalf("failed to bind channel: %s", err) } diff --git a/relay/testec2/relay.go b/relay/testec2/relay.go index 93d084387..9e22a80ea 100644 --- a/relay/testec2/relay.go +++ b/relay/testec2/relay.go @@ -70,8 +70,8 @@ func prepareConnsSender(serverConnURL string, peerPairs int) []net.Conn { ctx := context.Background() clientsSender := make([]*client.Client, peerPairs) for i := 0; i < cap(clientsSender); i++ { - c := client.NewClient(ctx, serverConnURL, hmacTokenStore, "sender-"+fmt.Sprint(i)) - if err := c.Connect(); err != nil { + c := client.NewClient(serverConnURL, hmacTokenStore, "sender-"+fmt.Sprint(i)) + if err := c.Connect(ctx); err != nil { log.Fatalf("failed to connect to server: %s", err) } clientsSender[i] = c @@ -79,7 +79,7 @@ func prepareConnsSender(serverConnURL string, peerPairs int) []net.Conn { connsSender := make([]net.Conn, 0, peerPairs) for i := 0; i < len(clientsSender); i++ { - conn, err := clientsSender[i].OpenConn("receiver-" + fmt.Sprint(i)) + conn, err := clientsSender[i].OpenConn(ctx, "receiver-"+fmt.Sprint(i)) if err != nil { log.Fatalf("failed to bind channel: %s", err) } @@ -156,8 +156,8 @@ func runReader(conn net.Conn) time.Duration { func prepareConnsReceiver(serverConnURL string, peerPairs int) []net.Conn { clientsReceiver := make([]*client.Client, peerPairs) for i := 0; i < cap(clientsReceiver); i++ { - c := client.NewClient(context.Background(), serverConnURL, hmacTokenStore, "receiver-"+fmt.Sprint(i)) - err := c.Connect() + c := client.NewClient(serverConnURL, hmacTokenStore, "receiver-"+fmt.Sprint(i)) + err := c.Connect(context.Background()) if err != nil { log.Fatalf("failed to connect to server: %s", err) } @@ -166,7 +166,7 @@ func prepareConnsReceiver(serverConnURL string, peerPairs int) []net.Conn { connsReceiver := make([]net.Conn, 0, peerPairs) for i := 0; i < len(clientsReceiver); i++ { - conn, err := clientsReceiver[i].OpenConn("sender-" + fmt.Sprint(i)) + conn, err := clientsReceiver[i].OpenConn(context.Background(), "sender-"+fmt.Sprint(i)) if err != nil { log.Fatalf("failed to bind channel: %s", err) } From b524f486e2434bf07bd51efc7f10652cc7292d7b Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 16 Jul 2025 00:00:18 +0200 Subject: [PATCH 151/213] [client] Fix/nil relayed address (#4153) Fix nil pointer in Relay conn address Meanwhile, we create a relayed net.Conn struct instance, it is possible to set the relayedURL to nil. panic: value method github.com/netbirdio/netbird/relay/client.RelayAddr.String called using nil *RelayAddr pointer Fix relayed URL variable protection Protect the channel closing --- relay/client/client.go | 61 ++++++++++++++++++------------- relay/client/manager_test.go | 17 ++------- relay/client/peer_subscription.go | 59 +++++++++++++++++++++--------- relay/server/store/listener.go | 15 ++++---- relay/server/store/notifier.go | 4 +- 5 files changed, 90 insertions(+), 66 deletions(-) diff --git a/relay/client/client.go b/relay/client/client.go index 2bf679ecb..32dfbb4db 100644 --- a/relay/client/client.go +++ b/relay/client/client.go @@ -181,13 +181,17 @@ func (c *Client) Connect(ctx context.Context) error { return nil } - if err := c.connect(ctx); err != nil { + instanceURL, err := c.connect(ctx) + if err != nil { return err } + c.muInstanceURL.Lock() + c.instanceURL = instanceURL + c.muInstanceURL.Unlock() c.stateSubscription = NewPeersStateSubscription(c.log, c.relayConn, c.closeConnsByPeerID) - c.log = c.log.WithField("relay", c.instanceURL.String()) + c.log = c.log.WithField("relay", instanceURL.String()) c.log.Infof("relay connection established") c.serviceIsRunning = true @@ -229,9 +233,18 @@ func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, erro c.log.Infof("remote peer is available, prepare the relayed connection: %s", peerID) msgChannel := make(chan Msg, 100) - conn := NewConn(c, peerID, msgChannel, c.instanceURL) c.mu.Lock() + if !c.serviceIsRunning { + c.mu.Unlock() + return nil, fmt.Errorf("relay connection is not established") + } + + c.muInstanceURL.Lock() + instanceURL := c.instanceURL + c.muInstanceURL.Unlock() + conn := NewConn(c, peerID, msgChannel, instanceURL) + _, ok = c.conns[peerID] if ok { c.mu.Unlock() @@ -278,69 +291,67 @@ func (c *Client) Close() error { return c.close(true) } -func (c *Client) connect(ctx context.Context) error { +func (c *Client) connect(ctx context.Context) (*RelayAddr, error) { rd := dialer.NewRaceDial(c.log, c.connectionURL, quic.Dialer{}, ws.Dialer{}) conn, err := rd.Dial() if err != nil { - return err + return nil, err } c.relayConn = conn - if err = c.handShake(ctx); err != nil { + instanceURL, err := c.handShake(ctx) + if err != nil { cErr := conn.Close() if cErr != nil { c.log.Errorf("failed to close connection: %s", cErr) } - return err + return nil, err } - return nil + return instanceURL, nil } -func (c *Client) handShake(ctx context.Context) error { +func (c *Client) handShake(ctx context.Context) (*RelayAddr, error) { msg, err := messages.MarshalAuthMsg(c.hashedID, c.authTokenStore.TokenBinary()) if err != nil { c.log.Errorf("failed to marshal auth message: %s", err) - return err + return nil, err } _, err = c.relayConn.Write(msg) if err != nil { c.log.Errorf("failed to send auth message: %s", err) - return err + return nil, err } buf := make([]byte, messages.MaxHandshakeRespSize) n, err := c.readWithTimeout(ctx, buf) if err != nil { c.log.Errorf("failed to read auth response: %s", err) - return err + return nil, err } _, err = messages.ValidateVersion(buf[:n]) if err != nil { - return fmt.Errorf("validate version: %w", err) + return nil, fmt.Errorf("validate version: %w", err) } msgType, err := messages.DetermineServerMessageType(buf[:n]) if err != nil { c.log.Errorf("failed to determine message type: %s", err) - return err + return nil, err } if msgType != messages.MsgTypeAuthResponse { c.log.Errorf("unexpected message type: %s", msgType) - return fmt.Errorf("unexpected message type") + return nil, fmt.Errorf("unexpected message type") } addr, err := messages.UnmarshalAuthResponse(buf[:n]) if err != nil { - return err + return nil, err } - c.muInstanceURL.Lock() - c.instanceURL = &RelayAddr{addr: addr} - c.muInstanceURL.Unlock() - return nil + return &RelayAddr{addr: addr}, nil } func (c *Client) readLoop(hc *healthcheck.Receiver, relayConn net.Conn, internallyStoppedFlag *internalStopFlag) { @@ -386,10 +397,6 @@ func (c *Client) readLoop(hc *healthcheck.Receiver, relayConn net.Conn, internal hc.Stop() - c.muInstanceURL.Lock() - c.instanceURL = nil - c.muInstanceURL.Unlock() - c.stateSubscription.Cleanup() c.wgReadLoop.Done() _ = c.close(false) @@ -578,8 +585,12 @@ func (c *Client) close(gracefullyExit bool) error { c.log.Warn("relay connection was already marked as not running") return nil } - c.serviceIsRunning = false + + c.muInstanceURL.Lock() + c.instanceURL = nil + c.muInstanceURL.Unlock() + c.log.Infof("closing all peer connections") c.closeAllConns() if gracefullyExit { diff --git a/relay/client/manager_test.go b/relay/client/manager_test.go index d20cdaac0..52f2833e4 100644 --- a/relay/client/manager_test.go +++ b/relay/client/manager_test.go @@ -229,16 +229,14 @@ func TestForeginAutoClose(t *testing.T) { errChan := make(chan error, 1) go func() { t.Log("binding server 1.") - err := srv1.Listen(srvCfg1) - if err != nil { + if err := srv1.Listen(srvCfg1); err != nil { errChan <- err } }() defer func() { t.Logf("closing server 1.") - err := srv1.Shutdown(ctx) - if err != nil { + if err := srv1.Shutdown(ctx); err != nil { t.Errorf("failed to close server: %s", err) } t.Logf("server 1. closed") @@ -287,15 +285,8 @@ func TestForeginAutoClose(t *testing.T) { } t.Log("open connection to another peer") - conn, err := mgr.OpenConn(ctx, toURL(srvCfg2)[0], "anotherpeer") - if err != nil { - t.Fatalf("failed to bind channel: %s", err) - } - - t.Log("close conn") - err = conn.Close() - if err != nil { - t.Fatalf("failed to close connection: %s", err) + if _, err = mgr.OpenConn(ctx, toURL(srvCfg2)[0], "anotherpeer"); err == nil { + t.Fatalf("should have failed to open connection to another peer") } timeout := relayCleanupInterval + keepUnusedServerTime + 1*time.Second diff --git a/relay/client/peer_subscription.go b/relay/client/peer_subscription.go index 03e7127b3..85bd41cbd 100644 --- a/relay/client/peer_subscription.go +++ b/relay/client/peer_subscription.go @@ -3,6 +3,8 @@ package client import ( "context" "errors" + "fmt" + "sync" "time" log "github.com/sirupsen/logrus" @@ -28,6 +30,7 @@ type PeersStateSubscription struct { listenForOfflinePeers map[messages.PeerID]struct{} waitingPeers map[messages.PeerID]chan struct{} + mu sync.Mutex // Mutex to protect access to waitingPeers and listenForOfflinePeers } func NewPeersStateSubscription(log *log.Entry, relayConn relayedConnWriter, offlineCallback func(peerIDs []messages.PeerID)) *PeersStateSubscription { @@ -43,24 +46,31 @@ func NewPeersStateSubscription(log *log.Entry, relayConn relayedConnWriter, offl // OnPeersOnline should be called when a notification is received that certain peers have come online. // It checks if any of the peers are being waited on and signals their availability. func (s *PeersStateSubscription) OnPeersOnline(peersID []messages.PeerID) { + s.mu.Lock() + defer s.mu.Unlock() + for _, peerID := range peersID { waitCh, ok := s.waitingPeers[peerID] if !ok { + // If meanwhile the peer was unsubscribed, we don't need to signal it continue } - close(waitCh) + waitCh <- struct{}{} delete(s.waitingPeers, peerID) + close(waitCh) } } func (s *PeersStateSubscription) OnPeersWentOffline(peersID []messages.PeerID) { + s.mu.Lock() relevantPeers := make([]messages.PeerID, 0, len(peersID)) for _, peerID := range peersID { if _, ok := s.listenForOfflinePeers[peerID]; ok { relevantPeers = append(relevantPeers, peerID) } } + s.mu.Unlock() if len(relevantPeers) > 0 { s.offlineCallback(relevantPeers) @@ -68,36 +78,41 @@ func (s *PeersStateSubscription) OnPeersWentOffline(peersID []messages.PeerID) { } // WaitToBeOnlineAndSubscribe waits for a specific peer to come online and subscribes to its state changes. -// todo: when we unsubscribe while this is running, this will not return with error func (s *PeersStateSubscription) WaitToBeOnlineAndSubscribe(ctx context.Context, peerID messages.PeerID) error { // Check if already waiting for this peer + s.mu.Lock() if _, exists := s.waitingPeers[peerID]; exists { + s.mu.Unlock() return errors.New("already waiting for peer to come online") } // Create a channel to wait for the peer to come online - waitCh := make(chan struct{}) + waitCh := make(chan struct{}, 1) s.waitingPeers[peerID] = waitCh + s.listenForOfflinePeers[peerID] = struct{}{} + s.mu.Unlock() - if err := s.subscribeStateChange([]messages.PeerID{peerID}); err != nil { + if err := s.subscribeStateChange(peerID); err != nil { s.log.Errorf("failed to subscribe to peer state: %s", err) - close(waitCh) - delete(s.waitingPeers, peerID) - return err - } - - defer func() { + s.mu.Lock() if ch, exists := s.waitingPeers[peerID]; exists && ch == waitCh { close(waitCh) delete(s.waitingPeers, peerID) + delete(s.listenForOfflinePeers, peerID) } - }() + s.mu.Unlock() + return err + } // Wait for peer to come online or context to be cancelled timeoutCtx, cancel := context.WithTimeout(ctx, OpenConnectionTimeout) defer cancel() select { - case <-waitCh: + case _, ok := <-waitCh: + if !ok { + return fmt.Errorf("wait for peer to come online has been cancelled") + } + s.log.Debugf("peer %s is now online", peerID) return nil case <-timeoutCtx.Done(): @@ -105,6 +120,13 @@ func (s *PeersStateSubscription) WaitToBeOnlineAndSubscribe(ctx context.Context, if err := s.unsubscribeStateChange([]messages.PeerID{peerID}); err != nil { s.log.Errorf("failed to unsubscribe from peer state: %s", err) } + s.mu.Lock() + if ch, exists := s.waitingPeers[peerID]; exists && ch == waitCh { + close(waitCh) + delete(s.waitingPeers, peerID) + delete(s.listenForOfflinePeers, peerID) + } + s.mu.Unlock() return timeoutCtx.Err() } } @@ -112,6 +134,7 @@ func (s *PeersStateSubscription) WaitToBeOnlineAndSubscribe(ctx context.Context, func (s *PeersStateSubscription) UnsubscribeStateChange(peerIDs []messages.PeerID) error { msgErr := s.unsubscribeStateChange(peerIDs) + s.mu.Lock() for _, peerID := range peerIDs { if wch, ok := s.waitingPeers[peerID]; ok { close(wch) @@ -120,11 +143,15 @@ func (s *PeersStateSubscription) UnsubscribeStateChange(peerIDs []messages.PeerI delete(s.listenForOfflinePeers, peerID) } + s.mu.Unlock() return msgErr } func (s *PeersStateSubscription) Cleanup() { + s.mu.Lock() + defer s.mu.Unlock() + for _, waitCh := range s.waitingPeers { close(waitCh) } @@ -133,16 +160,12 @@ func (s *PeersStateSubscription) Cleanup() { s.listenForOfflinePeers = make(map[messages.PeerID]struct{}) } -func (s *PeersStateSubscription) subscribeStateChange(peerIDs []messages.PeerID) error { - msgs, err := messages.MarshalSubPeerStateMsg(peerIDs) +func (s *PeersStateSubscription) subscribeStateChange(peerID messages.PeerID) error { + msgs, err := messages.MarshalSubPeerStateMsg([]messages.PeerID{peerID}) if err != nil { return err } - for _, peer := range peerIDs { - s.listenForOfflinePeers[peer] = struct{}{} - } - for _, msg := range msgs { if _, err := s.relayConn.Write(msg); err != nil { return err diff --git a/relay/server/store/listener.go b/relay/server/store/listener.go index e5f455795..b7c5f4ce8 100644 --- a/relay/server/store/listener.go +++ b/relay/server/store/listener.go @@ -8,6 +8,7 @@ import ( ) type Listener struct { + ctx context.Context store *Store onlineChan chan messages.PeerID @@ -15,12 +16,11 @@ type Listener struct { interestedPeersForOffline map[messages.PeerID]struct{} interestedPeersForOnline map[messages.PeerID]struct{} mu sync.RWMutex - - listenerCtx context.Context } -func newListener(store *Store) *Listener { +func newListener(ctx context.Context, store *Store) *Listener { l := &Listener{ + ctx: ctx, store: store, onlineChan: make(chan messages.PeerID, 244), //244 is the message size limit in the relay protocol @@ -65,11 +65,10 @@ func (l *Listener) RemoveInterestedPeer(peerIDs []messages.PeerID) { } } -func (l *Listener) listenForEvents(ctx context.Context, onPeersComeOnline, onPeersWentOffline func([]messages.PeerID)) { - l.listenerCtx = ctx +func (l *Listener) listenForEvents(onPeersComeOnline, onPeersWentOffline func([]messages.PeerID)) { for { select { - case <-ctx.Done(): + case <-l.ctx.Done(): return case pID := <-l.onlineChan: peers := make([]messages.PeerID, 0) @@ -102,7 +101,7 @@ func (l *Listener) peerWentOffline(peerID messages.PeerID) { if _, ok := l.interestedPeersForOffline[peerID]; ok { select { case l.offlineChan <- peerID: - case <-l.listenerCtx.Done(): + case <-l.ctx.Done(): } } } @@ -114,7 +113,7 @@ func (l *Listener) peerComeOnline(peerID messages.PeerID) { if _, ok := l.interestedPeersForOnline[peerID]; ok { select { case l.onlineChan <- peerID: - case <-l.listenerCtx.Done(): + case <-l.ctx.Done(): } delete(l.interestedPeersForOnline, peerID) } diff --git a/relay/server/store/notifier.go b/relay/server/store/notifier.go index d04db478b..ad2e53545 100644 --- a/relay/server/store/notifier.go +++ b/relay/server/store/notifier.go @@ -24,8 +24,8 @@ func NewPeerNotifier(store *Store) *PeerNotifier { func (pn *PeerNotifier) NewListener(onPeersComeOnline, onPeersWentOffline func([]messages.PeerID)) *Listener { ctx, cancel := context.WithCancel(context.Background()) - listener := newListener(pn.store) - go listener.listenForEvents(ctx, onPeersComeOnline, onPeersWentOffline) + listener := newListener(ctx, pn.store) + go listener.listenForEvents(onPeersComeOnline, onPeersWentOffline) pn.listenersMutex.Lock() pn.listeners[listener] = cancel From e67f44f47c2d19bf7902c5303c4ee6b242c23fd4 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:09:38 +0100 Subject: [PATCH 152/213] [client] fix test (#4156) --- client/internal/engine_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 4b7a2d600..01bfbcef5 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1481,6 +1481,10 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri GetSettings(gomock.Any(), gomock.Any(), gomock.Any()). Return(&types.Settings{}, nil). AnyTimes() + settingsMockManager.EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() permissionsManager := permissions.NewManager(store) From 58185ced1664e37e7680aa2aa241f220777c185b Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 16 Jul 2025 14:10:28 +0200 Subject: [PATCH 153/213] [misc] add forum post and update sign pipeline (#4155) use old git-town version --- .github/workflows/git-town.yml | 4 ++-- .github/workflows/release.yml | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/git-town.yml b/.github/workflows/git-town.yml index c54fcb449..699ed7d93 100644 --- a/.github/workflows/git-town.yml +++ b/.github/workflows/git-town.yml @@ -16,6 +16,6 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: git-town/action@v1 + - uses: git-town/action@v1.2.1 with: - skip-single-stacks: true \ No newline at end of file + skip-single-stacks: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00898ab29..44e02f457 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.0.20" + SIGN_PIPE_VER: "v0.0.21" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" COPYRIGHT: "NetBird GmbH" @@ -231,3 +231,17 @@ jobs: ref: ${{ env.SIGN_PIPE_VER }} token: ${{ secrets.SIGN_GITHUB_TOKEN }} inputs: '{ "tag": "${{ github.ref }}", "skipRelease": false }' + + post_on_forum: + runs-on: ubuntu-latest + continue-on-error: true + needs: [trigger_signer] + steps: + - uses: Codixer/discourse-topic-github-release-action@v2.0.1 + with: + discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }} + discourse-base-url: https://forum.netbird.io + discourse-author-username: NetBird + discourse-category: 17 + discourse-tags: + releases From 4f74509d55b54e5db316f3c50ee587706570ee93 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:07:31 +0200 Subject: [PATCH 154/213] [management] fix index creation if exist on mysql (#4150) --- management/server/migration/migration.go | 12 +- management/server/migration/migration_test.go | 127 +++++++++++++++--- 2 files changed, 120 insertions(+), 19 deletions(-) diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go index ab11be731..c2f1a5abf 100644 --- a/management/server/migration/migration.go +++ b/management/server/migration/migration.go @@ -283,7 +283,7 @@ func MigrateSetupKeyToHashedSetupKey[T any](ctx context.Context, db *gorm.DB) er } } - if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN %s", "peers", "setup_key")).Error; err != nil { + if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN IF EXISTS %s", "peers", "setup_key")).Error; err != nil { log.WithContext(ctx).Errorf("Failed to drop column %s: %v", "setup_key", err) } @@ -377,6 +377,11 @@ func DropIndex[T any](ctx context.Context, db *gorm.DB, indexName string) error func CreateIndexIfNotExists[T any](ctx context.Context, db *gorm.DB, indexName string, columns ...string) error { var model T + if !db.Migrator().HasTable(&model) { + log.WithContext(ctx).Debugf("table for %T does not exist, no migration needed", model) + return nil + } + stmt := &gorm.Statement{DB: db} if err := stmt.Parse(&model); err != nil { return fmt.Errorf("failed to parse model schema: %w", err) @@ -384,6 +389,11 @@ func CreateIndexIfNotExists[T any](ctx context.Context, db *gorm.DB, indexName s tableName := stmt.Schema.Table dialect := db.Dialector.Name() + if db.Migrator().HasIndex(&model, indexName) { + log.WithContext(ctx).Infof("index %s already exists on table %s", indexName, tableName) + return nil + } + var columnClause string if dialect == "mysql" { var withLength []string diff --git a/management/server/migration/migration_test.go b/management/server/migration/migration_test.go index 94377930a..ce76bd668 100644 --- a/management/server/migration/migration_test.go +++ b/management/server/migration/migration_test.go @@ -4,16 +4,21 @@ import ( "context" "encoding/gob" "net" + "os" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/netbirdio/netbird/management/server/migration" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" ) @@ -21,7 +26,41 @@ import ( func setupDatabase(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + var db *gorm.DB + var err error + var dsn string + var cleanup func() + switch os.Getenv("NETBIRD_STORE_ENGINE") { + case "mysql": + cleanup, dsn, err = testutil.CreateMysqlTestContainer() + if err != nil { + t.Fatalf("Failed to create MySQL test container: %v", err) + } + + if dsn == "" { + t.Fatal("MySQL connection string is empty, ensure the test container is running") + } + + db, err = gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{}) + case "postgres": + cleanup, dsn, err = testutil.CreatePostgresTestContainer() + if err != nil { + t.Fatalf("Failed to create PostgreSQL test container: %v", err) + } + + if dsn == "" { + t.Fatalf("PostgreSQL connection string is empty, ensure the test container is running") + } + + db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + case "sqlite": + db, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + default: + db, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + } + if cleanup != nil { + t.Cleanup(cleanup) + } require.NoError(t, err, "Failed to open database") return db @@ -34,6 +73,7 @@ func TestMigrateFieldFromGobToJSON_EmptyDB(t *testing.T) { } func TestMigrateFieldFromGobToJSON_WithGobData(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", "sqlite") db := setupDatabase(t) err := db.AutoMigrate(&types.Account{}, &route.Route{}) @@ -97,6 +137,7 @@ func TestMigrateNetIPFieldFromBlobToJSON_EmptyDB(t *testing.T) { } func TestMigrateNetIPFieldFromBlobToJSON_WithBlobData(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", "sqlite") db := setupDatabase(t) err := db.AutoMigrate(&types.Account{}, &nbpeer.Peer{}) @@ -117,12 +158,18 @@ func TestMigrateNetIPFieldFromBlobToJSON_WithBlobData(t *testing.T) { Peers []peer `gorm:"foreignKey:AccountID;references:id"` } - err = db.Save(&account{ + a := &account{ Account: types.Account{Id: "123"}, - Peers: []peer{ - {Location: location{ConnectionIP: net.IP{10, 0, 0, 1}}}, - }}, - ).Error + } + + err = db.Save(a).Error + require.NoError(t, err, "Failed to insert account") + + a.Peers = []peer{ + {Location: location{ConnectionIP: net.IP{10, 0, 0, 1}}}, + } + + err = db.Save(a).Error require.NoError(t, err, "Failed to insert blob data") var blobValue string @@ -143,12 +190,18 @@ func TestMigrateNetIPFieldFromBlobToJSON_WithJSONData(t *testing.T) { err := db.AutoMigrate(&types.Account{}, &nbpeer.Peer{}) require.NoError(t, err, "Failed to auto-migrate tables") - err = db.Save(&types.Account{ + account := &types.Account{ Id: "1234", - PeersG: []nbpeer.Peer{ - {Location: nbpeer.Location{ConnectionIP: net.IP{10, 0, 0, 1}}}, - }}, - ).Error + } + + err = db.Save(account).Error + require.NoError(t, err, "Failed to insert account") + + account.PeersG = []nbpeer.Peer{ + {AccountID: "1234", Location: nbpeer.Location{ConnectionIP: net.IP{10, 0, 0, 1}}}, + } + + err = db.Save(account).Error require.NoError(t, err, "Failed to insert JSON data") err = migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](context.Background(), db, "location_connection_ip", "") @@ -162,12 +215,13 @@ func TestMigrateNetIPFieldFromBlobToJSON_WithJSONData(t *testing.T) { func TestMigrateSetupKeyToHashedSetupKey_ForPlainKey(t *testing.T) { db := setupDatabase(t) - err := db.AutoMigrate(&types.SetupKey{}) + err := db.AutoMigrate(&types.SetupKey{}, &nbpeer.Peer{}) require.NoError(t, err, "Failed to auto-migrate tables") err = db.Save(&types.SetupKey{ - Id: "1", - Key: "EEFDAB47-C1A5-4472-8C05-71DE9A1E8382", + Id: "1", + Key: "EEFDAB47-C1A5-4472-8C05-71DE9A1E8382", + UpdatedAt: time.Now(), }).Error require.NoError(t, err, "Failed to insert setup key") @@ -192,6 +246,7 @@ func TestMigrateSetupKeyToHashedSetupKey_ForAlreadyMigratedKey_Case1(t *testing. Id: "1", Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", KeySecret: "EEFDA****", + UpdatedAt: time.Now(), }).Error require.NoError(t, err, "Failed to insert setup key") @@ -213,8 +268,9 @@ func TestMigrateSetupKeyToHashedSetupKey_ForAlreadyMigratedKey_Case2(t *testing. require.NoError(t, err, "Failed to auto-migrate tables") err = db.Save(&types.SetupKey{ - Id: "1", - Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", + Id: "1", + Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", + UpdatedAt: time.Now(), }).Error require.NoError(t, err, "Failed to insert setup key") @@ -235,8 +291,9 @@ func TestDropIndex(t *testing.T) { require.NoError(t, err, "Failed to auto-migrate tables") err = db.Save(&types.SetupKey{ - Id: "1", - Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", + Id: "1", + Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", + UpdatedAt: time.Now(), }).Error require.NoError(t, err, "Failed to insert setup key") @@ -249,3 +306,37 @@ func TestDropIndex(t *testing.T) { exist = db.Migrator().HasIndex(&types.SetupKey{}, "idx_setup_keys_account_id") assert.False(t, exist, "Should not have the index") } + +func TestCreateIndex(t *testing.T) { + db := setupDatabase(t) + err := db.AutoMigrate(&nbpeer.Peer{}) + assert.NoError(t, err, "Failed to auto-migrate tables") + + indexName := "idx_account_ip" + + err = migration.CreateIndexIfNotExists[nbpeer.Peer](context.Background(), db, indexName, "account_id", "ip") + assert.NoError(t, err, "Migration should not fail to create index") + + exist := db.Migrator().HasIndex(&nbpeer.Peer{}, indexName) + assert.True(t, exist, "Should have the index") +} + +func TestCreateIndexIfExists(t *testing.T) { + db := setupDatabase(t) + err := db.AutoMigrate(&nbpeer.Peer{}) + assert.NoError(t, err, "Failed to auto-migrate tables") + + indexName := "idx_account_ip" + + err = migration.CreateIndexIfNotExists[nbpeer.Peer](context.Background(), db, indexName, "account_id", "ip") + assert.NoError(t, err, "Migration should not fail to create index") + + exist := db.Migrator().HasIndex(&nbpeer.Peer{}, indexName) + assert.True(t, exist, "Should have the index") + + err = migration.CreateIndexIfNotExists[nbpeer.Peer](context.Background(), db, indexName, "account_id", "ip") + assert.NoError(t, err, "Create index should not fail if index exists") + + exist = db.Migrator().HasIndex(&nbpeer.Peer{}, indexName) + assert.True(t, exist, "Should have the index") +} From 08fd460867f5675ebfcdf38a157248a5d33aa9dd Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 18 Jul 2025 12:18:52 +0200 Subject: [PATCH 155/213] [management] Add validate flow response (#4172) This PR adds a validate flow response feature to the management server by integrating an IntegratedValidator component. The main purpose is to enable validation of PKCE authorization flows through an integrated validator interface. - Adds a new ValidateFlowResponse method to the IntegratedValidator interface - Integrates the validator into the management server to validate PKCE authorization flows - Updates dependency version for management-integrations --- client/cmd/testutil_test.go | 2 +- client/internal/engine_test.go | 2 +- client/server/server_test.go | 2 +- go.mod | 2 +- go.sum | 4 +- management/client/client_test.go | 2 +- management/cmd/management.go | 2 +- management/server/account_test.go | 2 +- management/server/dns_test.go | 2 +- management/server/grpcserver.go | 38 +++++++++++-------- .../http/testing/testing_tools/tools.go | 3 +- management/server/integrated_validator.go | 20 +++++----- .../integrated_validator/interface.go | 2 + management/server/management_proto_test.go | 4 +- management/server/management_test.go | 3 +- management/server/nameserver_test.go | 2 +- management/server/peer_test.go | 12 +++--- management/server/route_test.go | 2 +- management/server/user_test.go | 2 +- 19 files changed, 60 insertions(+), 48 deletions(-) diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index b5a80d63a..228a5d507 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -109,7 +109,7 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc } secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) - mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) + mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, &mgmt.MockIntegratedValidator{}) if err != nil { t.Fatal(err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 01bfbcef5..e75672ed1 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1494,7 +1494,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) + mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, &server.MockIntegratedValidator{}) if err != nil { return nil, "", err } diff --git a/client/server/server_test.go b/client/server/server_test.go index 376b7e8bd..7c46aac5d 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -212,7 +212,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) + mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, &server.MockIntegratedValidator{}) if err != nil { return nil, "", err } diff --git a/go.mod b/go.mod index 4a9727373..cf2a23758 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65 + github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5 github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index a622f203f..699a832dd 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65 h1:5OfYiLjpr4dbQYJI5ouZaylkVdi2KlErLFOwBeBo5Hw= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5 h1:Zfn8d83OVyELCdxgprcyXR3D8uqoxHtXE9PUxVXDx/w= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/client/client_test.go b/management/client/client_test.go index 1847af73e..b59b7c982 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -112,7 +112,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { } secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) - mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) + mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, mgmt.MockIntegratedValidator{}) if err != nil { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index 878e4c39e..24c260e9c 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -292,7 +292,7 @@ var ( ephemeralManager.LoadInitialPeers(ctx) gRPCAPIHandler := grpc.NewServer(gRPCOpts...) - srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager, authManager) + srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager, authManager, integratedPeerValidator) if err != nil { return fmt.Errorf("failed creating gRPC API handler: %v", err) } diff --git a/management/server/account_test.go b/management/server/account_test.go index fcd40b082..b65dffe6c 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -2887,7 +2887,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, error) { permissionsManager := permissions.NewManager(store) - manager, err := BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + manager, err := BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) if err != nil { return nil, err } diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 31c944a25..f2295450f 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -219,7 +219,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { // return empty extra settings for expected calls to UpdateAccountPeers settingsMockManager.EXPECT().GetExtraSettings(gomock.Any(), gomock.Any()).Return(&types.ExtraSettings{}, nil).AnyTimes() permissionsManager := permissions.NewManager(store) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } func createDNSStore(t *testing.T) (store.Store, error) { diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 2b27f9e0f..2f1bc3673 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -19,6 +19,7 @@ import ( "google.golang.org/grpc/status" integrationsConfig "github.com/netbirdio/management-integrations/integrations/config" + "github.com/netbirdio/netbird/management/server/integrations/integrated_validator" "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/management/proto" @@ -40,13 +41,14 @@ type GRPCServer struct { settingsManager settings.Manager wgKey wgtypes.Key proto.UnimplementedManagementServiceServer - peersUpdateManager *PeersUpdateManager - config *types.Config - secretsManager SecretsManager - appMetrics telemetry.AppMetrics - ephemeralManager *EphemeralManager - peerLocks sync.Map - authManager auth.Manager + peersUpdateManager *PeersUpdateManager + config *types.Config + secretsManager SecretsManager + appMetrics telemetry.AppMetrics + ephemeralManager *EphemeralManager + peerLocks sync.Map + authManager auth.Manager + integratedPeerValidator integrated_validator.IntegratedValidator } // NewServer creates a new Management server @@ -60,6 +62,7 @@ func NewServer( appMetrics telemetry.AppMetrics, ephemeralManager *EphemeralManager, authManager auth.Manager, + integratedPeerValidator integrated_validator.IntegratedValidator, ) (*GRPCServer, error) { key, err := wgtypes.GeneratePrivateKey() if err != nil { @@ -79,14 +82,15 @@ func NewServer( return &GRPCServer{ wgKey: key, // peerKey -> event channel - peersUpdateManager: peersUpdateManager, - accountManager: accountManager, - settingsManager: settingsManager, - config: config, - secretsManager: secretsManager, - authManager: authManager, - appMetrics: appMetrics, - ephemeralManager: ephemeralManager, + peersUpdateManager: peersUpdateManager, + accountManager: accountManager, + settingsManager: settingsManager, + config: config, + secretsManager: secretsManager, + authManager: authManager, + appMetrics: appMetrics, + ephemeralManager: ephemeralManager, + integratedPeerValidator: integratedPeerValidator, }, nil } @@ -850,7 +854,7 @@ func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.En return nil, status.Error(codes.NotFound, "no pkce authorization flow information available") } - flowInfoResp := &proto.PKCEAuthorizationFlow{ + initInfoFlow := &proto.PKCEAuthorizationFlow{ ProviderConfig: &proto.ProviderConfig{ Audience: s.config.PKCEAuthorizationFlow.ProviderConfig.Audience, ClientID: s.config.PKCEAuthorizationFlow.ProviderConfig.ClientID, @@ -865,6 +869,8 @@ func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.En }, } + flowInfoResp := s.integratedPeerValidator.ValidateFlowResponse(ctx, peerKey.String(), initInfoFlow) + encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, flowInfoResp) if err != nil { return nil, status.Error(codes.Internal, "failed to encrypt no pkce authorization flow information") diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go index 829bff455..e308f100f 100644 --- a/management/server/http/testing/testing_tools/tools.go +++ b/management/server/http/testing/testing_tools/tools.go @@ -1,4 +1,5 @@ package testing_tools + import ( "bytes" "context" @@ -132,7 +133,7 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve } geoMock := &geolocation.Mock{} - validatorMock := server.MocIntegratedValidator{} + validatorMock := server.MockIntegratedValidator{} proxyController := integrations.NewController(store) userManager := users.NewManager(store) permissionsManager := permissions.NewManager(store) diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index cfde7c614..e3e474411 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/server/integrations/integrated_validator" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" @@ -101,22 +102,23 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI return am.integratedPeerValidator.GetValidatedPeers(accountID, groups, peers, settings.Extra) } -type MocIntegratedValidator struct { +type MockIntegratedValidator struct { + integrated_validator.IntegratedValidator ValidatePeerFunc func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) } -func (a MocIntegratedValidator) ValidateExtraSettings(_ context.Context, newExtraSettings *types.ExtraSettings, oldExtraSettings *types.ExtraSettings, peers map[string]*nbpeer.Peer, userID string, accountID string) error { +func (a MockIntegratedValidator) ValidateExtraSettings(_ context.Context, newExtraSettings *types.ExtraSettings, oldExtraSettings *types.ExtraSettings, peers map[string]*nbpeer.Peer, userID string, accountID string) error { return nil } -func (a MocIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) { +func (a MockIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) { if a.ValidatePeerFunc != nil { return a.ValidatePeerFunc(context.Background(), update, peer, userID, accountID, dnsDomain, peersGroup, extraSettings) } return update, false, nil } -func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) { +func (a MockIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) { validatedPeers := make(map[string]struct{}) for _, peer := range peers { validatedPeers[peer.ID] = struct{}{} @@ -124,22 +126,22 @@ func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups []*ty return validatedPeers, nil } -func (MocIntegratedValidator) PreparePeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) *nbpeer.Peer { +func (MockIntegratedValidator) PreparePeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) *nbpeer.Peer { return peer } -func (MocIntegratedValidator) IsNotValidPeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) { +func (MockIntegratedValidator) IsNotValidPeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) { return false, false, nil } -func (MocIntegratedValidator) PeerDeleted(_ context.Context, _, _ string) error { +func (MockIntegratedValidator) PeerDeleted(_ context.Context, _, _ string) error { return nil } -func (MocIntegratedValidator) SetPeerInvalidationListener(func(accountID string)) { +func (MockIntegratedValidator) SetPeerInvalidationListener(func(accountID string)) { // just a dummy } -func (MocIntegratedValidator) Stop(_ context.Context) { +func (MockIntegratedValidator) Stop(_ context.Context) { // just a dummy } diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go index 083baa65e..245c0168f 100644 --- a/management/server/integrations/integrated_validator/interface.go +++ b/management/server/integrations/integrated_validator/interface.go @@ -3,6 +3,7 @@ package integrated_validator import ( "context" + "github.com/netbirdio/netbird/management/proto" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" ) @@ -17,4 +18,5 @@ type IntegratedValidator interface { PeerDeleted(ctx context.Context, accountID, peerID string) error SetPeerInvalidationListener(fn func(accountID string)) Stop(ctx context.Context) + ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow } diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 57c00ed9f..0d61b3a10 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -448,7 +448,7 @@ func startManagementForTest(t *testing.T, testFile string, config *types.Config) permissionsManager := permissions.NewManager(store) accountManager, err := BuildManager(ctx, store, peersUpdateManager, nil, "", "netbird.selfhosted", - eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) if err != nil { cleanup() @@ -458,7 +458,7 @@ func startManagementForTest(t *testing.T, testFile string, config *types.Config) secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) ephemeralMgr := NewEphemeralManager(store, accountManager) - mgmtServer, err := NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, ephemeralMgr, nil) + mgmtServer, err := NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, ephemeralMgr, nil, MockIntegratedValidator{}) if err != nil { return nil, nil, "", cleanup, err } diff --git a/management/server/management_test.go b/management/server/management_test.go index 0a6b3f751..ab6f0095b 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -206,7 +206,7 @@ func startServer( eventStore, nil, false, - server.MocIntegratedValidator{}, + server.MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, @@ -227,6 +227,7 @@ func startServer( nil, nil, nil, + server.MockIntegratedValidator{}, ) if err != nil { t.Fatalf("failed creating management server: %v", err) diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 8fada742c..25eb03b83 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -785,7 +785,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { AnyTimes() permissionsManager := permissions.NewManager(store) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } func createNSStore(t *testing.T) (store.Store, error) { diff --git a/management/server/peer_test.go b/management/server/peer_test.go index d41020514..4f6ae500e 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1273,7 +1273,7 @@ func Test_RegisterPeerByUser(t *testing.T) { settingsMockManager := settings.NewMockManager(ctrl) permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1353,7 +1353,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { AnyTimes() permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1496,7 +1496,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) { permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1570,7 +1570,7 @@ func Test_LoginPeer(t *testing.T) { AnyTimes() permissionsManager := permissions.NewManager(s) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1848,7 +1848,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) { return update, true, nil } - manager.integratedPeerValidator = MocIntegratedValidator{ValidatePeerFunc: requireUpdateFunc} + manager.integratedPeerValidator = MockIntegratedValidator{ValidatePeerFunc: requireUpdateFunc} done := make(chan struct{}) go func() { peerShouldReceiveUpdate(t, updMsg) @@ -1870,7 +1870,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) { return update, false, nil } - manager.integratedPeerValidator = MocIntegratedValidator{ValidatePeerFunc: requireNoUpdateFunc} + manager.integratedPeerValidator = MockIntegratedValidator{ValidatePeerFunc: requireNoUpdateFunc} done := make(chan struct{}) go func() { peerShouldNotReceiveUpdate(t, updMsg) diff --git a/management/server/route_test.go b/management/server/route_test.go index 77cbc75b9..37c37f624 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1284,7 +1284,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { permissionsManager := permissions.NewManager(store) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false) } func createRouterStore(t *testing.T) (store.Store, error) { diff --git a/management/server/user_test.go b/management/server/user_test.go index 7508e0609..53baf8f7e 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -852,7 +852,7 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) { am := DefaultAccountManager{ Store: store, eventStore: &activity.InMemoryEventStore{}, - integratedPeerValidator: MocIntegratedValidator{}, + integratedPeerValidator: MockIntegratedValidator{}, permissionsManager: permissionsManager, } From f6e9d755e4a3069f30bcff40a2a0eb131f9a5cbe Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 21 Jul 2025 09:46:53 +0200 Subject: [PATCH 156/213] [client, relay] The openConn function no longer blocks the relayAddress function call (#4180) The openConn function no longer blocks the relayAddress function call in manager layer --- relay/client/manager.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/relay/client/manager.go b/relay/client/manager.go index 0fb682d95..b97bc0b99 100644 --- a/relay/client/manager.go +++ b/relay/client/manager.go @@ -65,7 +65,7 @@ type Manager struct { relayClient *Client // the guard logic can overwrite the relayClient variable, this mutex protect the usage of the variable - relayClientMu sync.Mutex + relayClientMu sync.RWMutex reconnectGuard *Guard relayClients map[string]*RelayTrack @@ -124,8 +124,8 @@ func (m *Manager) Serve() error { // established via the relay server. If the peer is on a different relay server, the manager will establish a new // connection to the relay server. It returns back with a net.Conn what represent the remote peer connection. func (m *Manager) OpenConn(ctx context.Context, serverAddress, peerKey string) (net.Conn, error) { - m.relayClientMu.Lock() - defer m.relayClientMu.Unlock() + m.relayClientMu.RLock() + defer m.relayClientMu.RUnlock() if m.relayClient == nil { return nil, ErrRelayClientNotConnected @@ -155,8 +155,8 @@ func (m *Manager) OpenConn(ctx context.Context, serverAddress, peerKey string) ( // Ready returns true if the home Relay client is connected to the relay server. func (m *Manager) Ready() bool { - m.relayClientMu.Lock() - defer m.relayClientMu.Unlock() + m.relayClientMu.RLock() + defer m.relayClientMu.RUnlock() if m.relayClient == nil { return false @@ -174,8 +174,8 @@ func (m *Manager) SetOnReconnectedListener(f func()) { // AddCloseListener adds a listener to the given server instance address. The listener will be called if the connection // closed. func (m *Manager) AddCloseListener(serverAddress string, onClosedListener OnServerCloseListener) error { - m.relayClientMu.Lock() - defer m.relayClientMu.Unlock() + m.relayClientMu.RLock() + defer m.relayClientMu.RUnlock() if m.relayClient == nil { return ErrRelayClientNotConnected @@ -199,8 +199,8 @@ func (m *Manager) AddCloseListener(serverAddress string, onClosedListener OnServ // RelayInstanceAddress returns the address of the permanent relay server. It could change if the network connection is // lost. This address will be sent to the target peer to choose the common relay server for the communication. func (m *Manager) RelayInstanceAddress() (string, error) { - m.relayClientMu.Lock() - defer m.relayClientMu.Unlock() + m.relayClientMu.RLock() + defer m.relayClientMu.RUnlock() if m.relayClient == nil { return "", ErrRelayClientNotConnected @@ -300,7 +300,9 @@ func (m *Manager) onServerConnected() { func (m *Manager) onServerDisconnected(serverAddress string) { m.relayClientMu.Lock() if serverAddress == m.relayClient.connectionURL { - go m.reconnectGuard.StartReconnectTrys(m.ctx, m.relayClient) + go func(client *Client) { + m.reconnectGuard.StartReconnectTrys(m.ctx, client) + }(m.relayClient) } m.relayClientMu.Unlock() From 40fdeda838101b1bdc644026ecc66d9fbca9adb4 Mon Sep 17 00:00:00 2001 From: Ali Amer <76897266+aliamerj@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:55:17 +0300 Subject: [PATCH 157/213] [client] add new filter-by-connection-type flag (#4010) introduces a new flag --filter-by-connection-type to the status command. It allows users to filter peers by connection type (P2P or Relayed) in both JSON and detailed views. Input validation is added in parseFilters() to ensure proper usage, and --detail is auto-enabled if no output format is specified (consistent with other filters). --- client/cmd/debug.go | 2 +- client/cmd/status.go | 13 ++++++++++++- client/proto/daemon.pb.go | 7 +++++++ client/status/status.go | 20 +++++++++++--------- client/status/status_test.go | 2 +- client/ui/debug.go | 6 +++--- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/client/cmd/debug.go b/client/cmd/debug.go index 4036bb8f6..3f13a0c3a 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -307,7 +307,7 @@ func getStatusOutput(cmd *cobra.Command, anon bool) string { cmd.PrintErrf("Failed to get status: %v\n", err) } else { statusOutputString = nbstatus.ParseToFullDetailSummary( - nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil), + nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, ""), ) } return statusOutputString diff --git a/client/cmd/status.go b/client/cmd/status.go index b108ca57a..2d6e41bc2 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -26,6 +26,7 @@ var ( statusFilter string ipsFilterMap map[string]struct{} prefixNamesFilterMap map[string]struct{} + connectionTypeFilter string ) var statusCmd = &cobra.Command{ @@ -45,6 +46,7 @@ func init() { statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200") statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud") statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected") + statusCmd.PersistentFlags().StringVar(&connectionTypeFilter, "filter-by-connection-type", "", "filters the detailed output by connection type (P2P|Relayed), e.g., --filter-by-connection-type P2P") } func statusFunc(cmd *cobra.Command, args []string) error { @@ -89,7 +91,7 @@ func statusFunc(cmd *cobra.Command, args []string) error { return nil } - var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap) + var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap, connectionTypeFilter) var statusOutputString string switch { case detailFlag: @@ -156,6 +158,15 @@ func parseFilters() error { enableDetailFlagWhenFilterFlag() } + switch strings.ToLower(connectionTypeFilter) { + case "", "p2p", "relayed": + if strings.ToLower(connectionTypeFilter) != "" { + enableDetailFlagWhenFilterFlag() + } + default: + return fmt.Errorf("wrong connection-type filter, should be one of P2P|Relayed, got: %s", connectionTypeFilter) + } + return nil } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 26e58d183..753aa62d1 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1330,6 +1330,13 @@ func (x *PeerState) GetRelayAddress() string { return "" } +func (x *PeerState) GetConnectionType() string { + if x.Relayed { + return "Relayed" + } + return "P2P" +} + // LocalPeerState contains the latest state of the local peer type LocalPeerState struct { state protoimpl.MessageState `protogen:"open.v1"` diff --git a/client/status/status.go b/client/status/status.go index 18056e363..507c7ea80 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -100,7 +100,7 @@ type OutputOverview struct { LazyConnectionEnabled bool `json:"lazyConnectionEnabled" yaml:"lazyConnectionEnabled"` } -func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) OutputOverview { +func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter string) OutputOverview { pbFullStatus := resp.GetFullStatus() managementState := pbFullStatus.GetManagementState() @@ -118,7 +118,7 @@ func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, status } relayOverview := mapRelays(pbFullStatus.GetRelays()) - peersOverview := mapPeers(resp.GetFullStatus().GetPeers(), statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) + peersOverview := mapPeers(resp.GetFullStatus().GetPeers(), statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter, connectionTypeFilter) overview := OutputOverview{ Peers: peersOverview, @@ -193,6 +193,7 @@ func mapPeers( prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, + connectionTypeFilter string, ) PeersStateOutput { var peersStateDetail []PeerStateDetailOutput peersConnected := 0 @@ -208,7 +209,7 @@ func mapPeers( transferSent := int64(0) isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() - if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) { + if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter, connectionTypeFilter) { continue } if isPeerConnected { @@ -218,10 +219,7 @@ func mapPeers( remoteICE = pbPeerState.GetRemoteIceCandidateType() localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() - connType = "P2P" - if pbPeerState.Relayed { - connType = "Relayed" - } + connType = pbPeerState.GetConnectionType() relayServerAddress = pbPeerState.GetRelayAddress() lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() transferReceived = pbPeerState.GetBytesRx() @@ -542,10 +540,11 @@ func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bo return peersString } -func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) bool { +func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter string) bool { statusEval := false ipEval := false nameEval := true + connectionTypeEval := false if statusFilter != "" { if !strings.EqualFold(peerStatus, statusFilter) { @@ -570,8 +569,11 @@ func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFi } else { nameEval = false } + if connectionTypeFilter != "" && !strings.EqualFold(peerState.GetConnectionType(), connectionTypeFilter) { + connectionTypeEval = true + } - return statusEval || ipEval || nameEval + return statusEval || ipEval || nameEval || connectionTypeEval } func toIEC(b int64) string { diff --git a/client/status/status_test.go b/client/status/status_test.go index 33eda4b9e..5b5d23efd 100644 --- a/client/status/status_test.go +++ b/client/status/status_test.go @@ -234,7 +234,7 @@ var overview = OutputOverview{ } func TestConversionFromFullStatusToOutputOverview(t *testing.T) { - convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil) + convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil, "") assert.Equal(t, overview, convertedResult) } diff --git a/client/ui/debug.go b/client/ui/debug.go index ab7dba37a..55829de1e 100644 --- a/client/ui/debug.go +++ b/client/ui/debug.go @@ -433,7 +433,7 @@ func (s *serviceClient) collectDebugData( var postUpStatusOutput string if postUpStatus != nil { - overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil) + overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil, "") postUpStatusOutput = nbstatus.ParseToFullDetailSummary(overview) } headerPostUp := fmt.Sprintf("----- NetBird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) @@ -450,7 +450,7 @@ func (s *serviceClient) collectDebugData( var preDownStatusOutput string if preDownStatus != nil { - overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil) + overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil, "") preDownStatusOutput = nbstatus.ParseToFullDetailSummary(overview) } headerPreDown := fmt.Sprintf("----- NetBird pre-down - Timestamp: %s - Duration: %s", @@ -581,7 +581,7 @@ func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploa var statusOutput string if statusResp != nil { - overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil) + overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil, "") statusOutput = nbstatus.ParseToFullDetailSummary(overview) } From d6ed9c037ed31a1b4a527b3e49f9a1aaafa8db74 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:13:21 +0200 Subject: [PATCH 158/213] [client] Fix bind exclusion routes (#4154) --- client/iface/bind/control.go | 15 ++++ client/iface/bind/control_android.go | 12 --- client/iface/bind/ice_bind.go | 3 +- client/iface/bind/udp_mux.go | 25 +++--- client/iface/bind/udp_mux_generic.go | 21 +++++ client/iface/bind/udp_mux_ios.go | 7 ++ client/iface/bind/udp_mux_universal.go | 20 +++-- client/internal/engine.go | 14 +--- client/internal/engine_test.go | 2 +- client/internal/peer/conn.go | 52 ------------- .../routemanager/client/client_test.go | 2 +- client/internal/routemanager/manager.go | 13 ++-- client/internal/routemanager/manager_test.go | 2 +- client/internal/routemanager/mock.go | 5 +- .../routemanager/notifier/notifier_other.go | 2 +- .../routemanager/systemops/systemops.go | 5 ++ .../systemops/systemops_android.go | 5 +- .../systemops/systemops_generic.go | 76 ++++++++++++++----- .../systemops/systemops_generic_test.go | 6 +- .../routemanager/systemops/systemops_ios.go | 5 +- .../routemanager/systemops/systemops_linux.go | 6 +- .../routemanager/systemops/systemops_unix.go | 3 +- .../systemops/systemops_windows.go | 3 +- util/net/listener_listen.go | 67 ++++++++++++++-- util/net/listener_listen_ios.go | 10 +++ 25 files changed, 230 insertions(+), 151 deletions(-) create mode 100644 client/iface/bind/control.go delete mode 100644 client/iface/bind/control_android.go create mode 100644 client/iface/bind/udp_mux_generic.go create mode 100644 client/iface/bind/udp_mux_ios.go create mode 100644 util/net/listener_listen_ios.go diff --git a/client/iface/bind/control.go b/client/iface/bind/control.go new file mode 100644 index 000000000..89bddf12c --- /dev/null +++ b/client/iface/bind/control.go @@ -0,0 +1,15 @@ +package bind + +import ( + wireguard "golang.zx2c4.com/wireguard/conn" + + nbnet "github.com/netbirdio/netbird/util/net" +) + +// TODO: This is most likely obsolete since the control fns should be called by the wrapped udpconn (ice_bind.go) +func init() { + listener := nbnet.NewListener() + if listener.ListenConfig.Control != nil { + *wireguard.ControlFns = append(*wireguard.ControlFns, listener.ListenConfig.Control) + } +} diff --git a/client/iface/bind/control_android.go b/client/iface/bind/control_android.go deleted file mode 100644 index b8a865e39..000000000 --- a/client/iface/bind/control_android.go +++ /dev/null @@ -1,12 +0,0 @@ -package bind - -import ( - wireguard "golang.zx2c4.com/wireguard/conn" - - nbnet "github.com/netbirdio/netbird/util/net" -) - -func init() { - // ControlFns is not thread safe and should only be modified during init. - *wireguard.ControlFns = append(*wireguard.ControlFns, nbnet.ControlProtectSocket) -} diff --git a/client/iface/bind/ice_bind.go b/client/iface/bind/ice_bind.go index bb7a27279..c3d5ef377 100644 --- a/client/iface/bind/ice_bind.go +++ b/client/iface/bind/ice_bind.go @@ -16,6 +16,7 @@ import ( wgConn "golang.zx2c4.com/wireguard/conn" "github.com/netbirdio/netbird/client/iface/wgaddr" + nbnet "github.com/netbirdio/netbird/util/net" ) type RecvMessage struct { @@ -153,7 +154,7 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r s.udpMux = NewUniversalUDPMuxDefault( UniversalUDPMuxParams{ - UDPConn: conn, + UDPConn: nbnet.WrapUDPConn(conn), Net: s.transportNet, FilterFn: s.filterFn, WGAddress: s.address, diff --git a/client/iface/bind/udp_mux.go b/client/iface/bind/udp_mux.go index 0e58499aa..29e5d7937 100644 --- a/client/iface/bind/udp_mux.go +++ b/client/iface/bind/udp_mux.go @@ -296,14 +296,20 @@ func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) { return } - m.addressMapMu.Lock() - defer m.addressMapMu.Unlock() - + var allAddresses []string for _, c := range removedConns { addresses := c.getAddresses() - for _, addr := range addresses { - delete(m.addressMap, addr) - } + allAddresses = append(allAddresses, addresses...) + } + + m.addressMapMu.Lock() + for _, addr := range allAddresses { + delete(m.addressMap, addr) + } + m.addressMapMu.Unlock() + + for _, addr := range allAddresses { + m.notifyAddressRemoval(addr) } } @@ -351,14 +357,13 @@ func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr string) } m.addressMapMu.Lock() - defer m.addressMapMu.Unlock() - existing, ok := m.addressMap[addr] if !ok { existing = []*udpMuxedConn{} } existing = append(existing, conn) m.addressMap[addr] = existing + m.addressMapMu.Unlock() log.Debugf("ICE: registered %s for %s", addr, conn.params.Key) } @@ -386,12 +391,12 @@ func (m *UDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.Addr) erro // If you are using the same socket for the Host and SRFLX candidates, it might be that there are more than one // muxed connection - one for the SRFLX candidate and the other one for the HOST one. // We will then forward STUN packets to each of these connections. - m.addressMapMu.Lock() + m.addressMapMu.RLock() var destinationConnList []*udpMuxedConn if storedConns, ok := m.addressMap[addr.String()]; ok { destinationConnList = append(destinationConnList, storedConns...) } - m.addressMapMu.Unlock() + m.addressMapMu.RUnlock() var isIPv6 bool if udpAddr, _ := addr.(*net.UDPAddr); udpAddr != nil && udpAddr.IP.To4() == nil { diff --git a/client/iface/bind/udp_mux_generic.go b/client/iface/bind/udp_mux_generic.go new file mode 100644 index 000000000..e42d25462 --- /dev/null +++ b/client/iface/bind/udp_mux_generic.go @@ -0,0 +1,21 @@ +//go:build !ios + +package bind + +import ( + nbnet "github.com/netbirdio/netbird/util/net" +) + +func (m *UDPMuxDefault) notifyAddressRemoval(addr string) { + wrapped, ok := m.params.UDPConn.(*UDPConn) + if !ok { + return + } + + nbnetConn, ok := wrapped.GetPacketConn().(*nbnet.UDPConn) + if !ok { + return + } + + nbnetConn.RemoveAddress(addr) +} diff --git a/client/iface/bind/udp_mux_ios.go b/client/iface/bind/udp_mux_ios.go new file mode 100644 index 000000000..15e26d02f --- /dev/null +++ b/client/iface/bind/udp_mux_ios.go @@ -0,0 +1,7 @@ +//go:build ios + +package bind + +func (m *UDPMuxDefault) notifyAddressRemoval(addr string) { + // iOS doesn't support nbnet hooks, so this is a no-op +} \ No newline at end of file diff --git a/client/iface/bind/udp_mux_universal.go b/client/iface/bind/udp_mux_universal.go index 5cc634955..b755a7827 100644 --- a/client/iface/bind/udp_mux_universal.go +++ b/client/iface/bind/udp_mux_universal.go @@ -62,7 +62,7 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef // wrap UDP connection, process server reflexive messages // before they are passed to the UDPMux connection handler (connWorker) - m.params.UDPConn = &udpConn{ + m.params.UDPConn = &UDPConn{ PacketConn: params.UDPConn, mux: m, logger: params.Logger, @@ -70,7 +70,6 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef address: params.WGAddress, } - // embed UDPMux udpMuxParams := UDPMuxParams{ Logger: params.Logger, UDPConn: m.params.UDPConn, @@ -114,8 +113,8 @@ func (m *UniversalUDPMuxDefault) ReadFromConn(ctx context.Context) { } } -// udpConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets -type udpConn struct { +// UDPConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets +type UDPConn struct { net.PacketConn mux *UniversalUDPMuxDefault logger logging.LeveledLogger @@ -125,7 +124,12 @@ type udpConn struct { address wgaddr.Address } -func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) { +// GetPacketConn returns the underlying PacketConn +func (u *UDPConn) GetPacketConn() net.PacketConn { + return u.PacketConn +} + +func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { if u.filterFn == nil { return u.PacketConn.WriteTo(b, addr) } @@ -137,21 +141,21 @@ func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) { return u.handleUncachedAddress(b, addr) } -func (u *udpConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) { +func (u *UDPConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) { if isRouted { return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr) } return u.PacketConn.WriteTo(b, addr) } -func (u *udpConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) { +func (u *UDPConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) { if err := u.performFilterCheck(addr); err != nil { return 0, err } return u.PacketConn.WriteTo(b, addr) } -func (u *udpConn) performFilterCheck(addr net.Addr) error { +func (u *UDPConn) performFilterCheck(addr net.Addr) error { host, err := getHostFromAddr(addr) if err != nil { log.Errorf("Failed to get host from address %s: %v", addr, err) diff --git a/client/internal/engine.go b/client/internal/engine.go index e9772b359..1abb8163d 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -61,7 +61,6 @@ import ( signal "github.com/netbirdio/netbird/signal/client" sProto "github.com/netbirdio/netbird/signal/proto" "github.com/netbirdio/netbird/util" - nbnet "github.com/netbirdio/netbird/util/net" ) // PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer. @@ -138,9 +137,6 @@ type Engine struct { connMgr *ConnMgr - beforePeerHook nbnet.AddHookFunc - afterPeerHook nbnet.RemoveHookFunc - // rpManager is a Rosenpass manager rpManager *rosenpass.Manager @@ -409,12 +405,8 @@ func (e *Engine) Start() error { DisableClientRoutes: e.config.DisableClientRoutes, DisableServerRoutes: e.config.DisableServerRoutes, }) - beforePeerHook, afterPeerHook, err := e.routeManager.Init() - if err != nil { + if err := e.routeManager.Init(); err != nil { log.Errorf("Failed to initialize route manager: %s", err) - } else { - e.beforePeerHook = beforePeerHook - e.afterPeerHook = afterPeerHook } e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener) @@ -1261,10 +1253,6 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error { return fmt.Errorf("peer already exists: %s", peerKey) } - if e.beforePeerHook != nil && e.afterPeerHook != nil { - conn.AddBeforeAddPeerHook(e.beforePeerHook) - conn.AddAfterRemovePeerHook(e.afterPeerHook) - } return nil } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index e75672ed1..f02138686 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -400,7 +400,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { StatusRecorder: engine.statusRecorder, RelayManager: relayMgr, }) - _, _, err = engine.routeManager.Init() + err = engine.routeManager.Init() require.NoError(t, err) engine.dnsServer = &dns.MockServer{ UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil }, diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 7765bb51c..ddd90450d 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -26,7 +26,6 @@ import ( "github.com/netbirdio/netbird/client/internal/stdnet" relayClient "github.com/netbirdio/netbird/relay/client" "github.com/netbirdio/netbird/route" - nbnet "github.com/netbirdio/netbird/util/net" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" ) @@ -106,10 +105,6 @@ type Conn struct { workerRelay *WorkerRelay wgWatcherWg sync.WaitGroup - connIDRelay nbnet.ConnectionID - connIDICE nbnet.ConnectionID - beforeAddPeerHooks []nbnet.AddHookFunc - afterRemovePeerHooks []nbnet.RemoveHookFunc // used to store the remote Rosenpass key for Relayed connection in case of connection update from ice rosenpassRemoteKey []byte @@ -267,8 +262,6 @@ func (conn *Conn) Close(signalToRemote bool) { conn.Log.Errorf("failed to remove wg endpoint: %v", err) } - conn.freeUpConnID() - if conn.evalStatus() == StatusConnected && conn.onDisconnected != nil { conn.onDisconnected(conn.config.WgConfig.RemoteKey) } @@ -293,13 +286,6 @@ func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HAMa conn.workerICE.OnRemoteCandidate(candidate, haRoutes) } -func (conn *Conn) AddBeforeAddPeerHook(hook nbnet.AddHookFunc) { - conn.beforeAddPeerHooks = append(conn.beforeAddPeerHooks, hook) -} -func (conn *Conn) AddAfterRemovePeerHook(hook nbnet.RemoveHookFunc) { - conn.afterRemovePeerHooks = append(conn.afterRemovePeerHooks, hook) -} - // SetOnConnected sets a handler function to be triggered by Conn when a new connection to a remote peer established func (conn *Conn) SetOnConnected(handler func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)) { conn.onConnected = handler @@ -387,10 +373,6 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn ep = directEp } - if err := conn.runBeforeAddPeerHooks(ep.IP); err != nil { - conn.Log.Errorf("Before add peer hook failed: %v", err) - } - conn.workerRelay.DisableWgWatcher() // todo consider to run conn.wgWatcherWg.Wait() here @@ -503,10 +485,6 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { return } - if err := conn.runBeforeAddPeerHooks(wgProxy.EndpointAddr().IP); err != nil { - conn.Log.Errorf("Before add peer hook failed: %v", err) - } - wgProxy.Work() if err := conn.configureWGEndpoint(wgProxy.EndpointAddr(), rci.rosenpassPubKey); err != nil { if err := wgProxy.CloseConn(); err != nil { @@ -707,36 +685,6 @@ func (conn *Conn) isConnectedOnAllWay() (connected bool) { return true } -func (conn *Conn) runBeforeAddPeerHooks(ip net.IP) error { - conn.connIDICE = nbnet.GenerateConnID() - for _, hook := range conn.beforeAddPeerHooks { - if err := hook(conn.connIDICE, ip); err != nil { - return err - } - } - return nil -} - -func (conn *Conn) freeUpConnID() { - if conn.connIDRelay != "" { - for _, hook := range conn.afterRemovePeerHooks { - if err := hook(conn.connIDRelay); err != nil { - conn.Log.Errorf("After remove peer hook failed: %v", err) - } - } - conn.connIDRelay = "" - } - - if conn.connIDICE != "" { - for _, hook := range conn.afterRemovePeerHooks { - if err := hook(conn.connIDICE); err != nil { - conn.Log.Errorf("After remove peer hook failed: %v", err) - } - } - conn.connIDICE = "" - } -} - func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) { conn.Log.Debugf("setup proxied WireGuard connection") udpAddr := &net.UDPAddr{ diff --git a/client/internal/routemanager/client/client_test.go b/client/internal/routemanager/client/client_test.go index ec8e0e944..850f6691f 100644 --- a/client/internal/routemanager/client/client_test.go +++ b/client/internal/routemanager/client/client_test.go @@ -812,7 +812,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) { } params := common.HandlerParams{ - Route: &route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, + Route: &route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, } // create new clientNetwork client := &Watcher{ diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index e0974ab2a..e51778811 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -44,7 +44,7 @@ import ( // Manager is a route manager interface type Manager interface { - Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) + Init() error UpdateRoutes(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error ClassifyRoutes(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) TriggerSelection(route.HAMap) @@ -201,11 +201,11 @@ func (m *DefaultManager) setupRefCounters(useNoop bool) { } // Init sets up the routing -func (m *DefaultManager) Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (m *DefaultManager) Init() error { m.routeSelector = m.initSelector() if nbnet.CustomRoutingDisabled() || m.disableClientRoutes { - return nil, nil, nil + return nil } if err := m.sysOps.CleanupRouting(nil); err != nil { @@ -219,13 +219,12 @@ func (m *DefaultManager) Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) ips := resolveURLsToIPs(initialAddresses) - beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips, m.stateManager) - if err != nil { - return nil, nil, fmt.Errorf("setup routing: %w", err) + if err := m.sysOps.SetupRouting(ips, m.stateManager); err != nil { + return fmt.Errorf("setup routing: %w", err) } log.Info("Routing setup complete") - return beforePeerHook, afterPeerHook, nil + return nil } func (m *DefaultManager) initSelector() *routeselector.RouteSelector { diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 486ee080a..2f13c2134 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -430,7 +430,7 @@ func TestManagerUpdateRoutes(t *testing.T) { StatusRecorder: statusRecorder, }) - _, _, err = routeManager.Init() + err = routeManager.Init() require.NoError(t, err, "should init route manager") defer routeManager.Stop(nil) diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index 4e182f82c..be633c3fa 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -9,7 +9,6 @@ import ( "github.com/netbirdio/netbird/client/internal/routeselector" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/route" - "github.com/netbirdio/netbird/util/net" ) // MockManager is the mock instance of a route manager @@ -23,8 +22,8 @@ type MockManager struct { StopFunc func(manager *statemanager.Manager) } -func (m *MockManager) Init() (net.AddHookFunc, net.RemoveHookFunc, error) { - return nil, nil, nil +func (m *MockManager) Init() error { + return nil } // InitialRouteRange mock implementation of InitialRouteRange from Manager interface diff --git a/client/internal/routemanager/notifier/notifier_other.go b/client/internal/routemanager/notifier/notifier_other.go index 77045b839..0521e3dc2 100644 --- a/client/internal/routemanager/notifier/notifier_other.go +++ b/client/internal/routemanager/notifier/notifier_other.go @@ -33,4 +33,4 @@ func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) { func (n *Notifier) GetInitialRouteRanges() []string { return []string{} -} \ No newline at end of file +} diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index 106c520da..b91348e94 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -6,6 +6,7 @@ import ( "net/netip" "sync" "sync/atomic" + "time" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/routemanager/notifier" @@ -56,6 +57,10 @@ type SysOps struct { // seq is an atomic counter for generating unique sequence numbers for route messages //nolint:unused // only used on BSD systems seq atomic.Uint32 + + localSubnetsCache []*net.IPNet + localSubnetsCacheMu sync.RWMutex + localSubnetsCacheTime time.Time } func NewSysOps(wgInterface wgIface, notifier *notifier.Notifier) *SysOps { diff --git a/client/internal/routemanager/systemops/systemops_android.go b/client/internal/routemanager/systemops/systemops_android.go index ca8aea3fb..a375ce832 100644 --- a/client/internal/routemanager/systemops/systemops_android.go +++ b/client/internal/routemanager/systemops/systemops_android.go @@ -10,11 +10,10 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/statemanager" - nbnet "github.com/netbirdio/netbird/util/net" ) -func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { - return nil, nil, nil +func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager) error { + return nil } func (r *SysOps) CleanupRouting(*statemanager.Manager) error { diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go index d223a27b2..128afa2a5 100644 --- a/client/internal/routemanager/systemops/systemops_generic.go +++ b/client/internal/routemanager/systemops/systemops_generic.go @@ -10,6 +10,7 @@ import ( "net/netip" "runtime" "strconv" + "time" "github.com/hashicorp/go-multierror" "github.com/libp2p/go-netroute" @@ -24,6 +25,8 @@ import ( nbnet "github.com/netbirdio/netbird/util/net" ) +const localSubnetsCacheTTL = 15 * time.Minute + var splitDefaultv4_1 = netip.PrefixFrom(netip.IPv4Unspecified(), 1) var splitDefaultv4_2 = netip.PrefixFrom(netip.AddrFrom4([4]byte{128}), 1) var splitDefaultv6_1 = netip.PrefixFrom(netip.IPv6Unspecified(), 1) @@ -31,7 +34,7 @@ var splitDefaultv6_2 = netip.PrefixFrom(netip.AddrFrom16([16]byte{0x80}), 1) var ErrRoutingIsSeparate = errors.New("routing is separate") -func (r *SysOps) setupRefCounter(initAddresses []net.IP, stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (r *SysOps) setupRefCounter(initAddresses []net.IP, stateManager *statemanager.Manager) error { stateManager.RegisterState(&ShutdownState{}) initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified()) @@ -75,7 +78,10 @@ func (r *SysOps) setupRefCounter(initAddresses []net.IP, stateManager *statemana r.refCounter = refCounter - return r.setupHooks(initAddresses, stateManager) + if err := r.setupHooks(initAddresses, stateManager); err != nil { + return fmt.Errorf("setup hooks: %w", err) + } + return nil } // updateState updates state on every change so it will be persisted regularly @@ -128,18 +134,14 @@ func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf wgIface, init return Nexthop{}, fmt.Errorf("get next hop: %w", err) } - log.Debugf("Found next hop %s for prefix %s with interface %v", nexthop.IP, prefix, nexthop.IP) - exitNextHop := Nexthop{ - IP: nexthop.IP, - Intf: nexthop.Intf, - } + log.Debugf("Found next hop %s for prefix %s with interface %v", nexthop.IP, prefix, nexthop.Intf) + exitNextHop := nexthop vpnAddr := vpnIntf.Address().IP // if next hop is the VPN address or the interface is the VPN interface, we should use the initial values if exitNextHop.IP == vpnAddr || exitNextHop.Intf != nil && exitNextHop.Intf.Name == vpnIntf.Name() { log.Debugf("Route for prefix %s is pointing to the VPN interface, using initial next hop %v", prefix, initialNextHop) - exitNextHop = initialNextHop } @@ -152,12 +154,37 @@ func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf wgIface, init } func (r *SysOps) isPrefixInLocalSubnets(prefix netip.Prefix) (bool, *net.IPNet) { + r.localSubnetsCacheMu.RLock() + cacheAge := time.Since(r.localSubnetsCacheTime) + subnets := r.localSubnetsCache + r.localSubnetsCacheMu.RUnlock() + + if cacheAge > localSubnetsCacheTTL || subnets == nil { + r.localSubnetsCacheMu.Lock() + if time.Since(r.localSubnetsCacheTime) > localSubnetsCacheTTL || r.localSubnetsCache == nil { + r.refreshLocalSubnetsCache() + } + subnets = r.localSubnetsCache + r.localSubnetsCacheMu.Unlock() + } + + for _, subnet := range subnets { + if subnet.Contains(prefix.Addr().AsSlice()) { + return true, subnet + } + } + + return false, nil +} + +func (r *SysOps) refreshLocalSubnetsCache() { localInterfaces, err := net.Interfaces() if err != nil { log.Errorf("Failed to get local interfaces: %v", err) - return false, nil + return } + var newSubnets []*net.IPNet for _, intf := range localInterfaces { addrs, err := intf.Addrs() if err != nil { @@ -171,14 +198,12 @@ func (r *SysOps) isPrefixInLocalSubnets(prefix netip.Prefix) (bool, *net.IPNet) log.Errorf("Failed to convert address to IPNet: %v", addr) continue } - - if ipnet.Contains(prefix.Addr().AsSlice()) { - return true, ipnet - } + newSubnets = append(newSubnets, ipnet) } } - return false, nil + r.localSubnetsCache = newSubnets + r.localSubnetsCacheTime = time.Now() } // genericAddVPNRoute adds a new route to the vpn interface, it splits the default prefix @@ -264,7 +289,7 @@ func (r *SysOps) genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) return r.removeFromRouteTable(prefix, nextHop) } -func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.Manager) error { beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error { prefix, err := util.GetPrefixFromIP(ip) if err != nil { @@ -289,9 +314,11 @@ func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.M return nil } + var merr *multierror.Error + for _, ip := range initAddresses { if err := beforeHook("init", ip); err != nil { - log.Errorf("Failed to add route reference: %v", err) + merr = multierror.Append(merr, fmt.Errorf("add initial route for %s: %w", ip, err)) } } @@ -300,11 +327,11 @@ func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.M return ctx.Err() } - var result *multierror.Error + var merr *multierror.Error for _, ip := range resolvedIPs { - result = multierror.Append(result, beforeHook(connID, ip.IP)) + merr = multierror.Append(merr, beforeHook(connID, ip.IP)) } - return nberrors.FormatErrorOrNil(result) + return nberrors.FormatErrorOrNil(merr) }) nbnet.AddDialerCloseHook(func(connID nbnet.ConnectionID, conn *net.Conn) error { @@ -319,7 +346,16 @@ func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.M return afterHook(connID) }) - return beforeHook, afterHook, nil + nbnet.AddListenerAddressRemoveHook(func(connID nbnet.ConnectionID, prefix netip.Prefix) error { + if _, err := r.refCounter.Decrement(prefix); err != nil { + return fmt.Errorf("remove route reference: %w", err) + } + + r.updateState(stateManager) + return nil + }) + + return nberrors.FormatErrorOrNil(merr) } func GetNextHop(ip netip.Addr) (Nexthop, error) { diff --git a/client/internal/routemanager/systemops/systemops_generic_test.go b/client/internal/routemanager/systemops/systemops_generic_test.go index 2a57e6044..c1c1182bc 100644 --- a/client/internal/routemanager/systemops/systemops_generic_test.go +++ b/client/internal/routemanager/systemops/systemops_generic_test.go @@ -143,7 +143,7 @@ func TestAddVPNRoute(t *testing.T) { wgInterface := createWGInterface(t, fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100+n) r := NewSysOps(wgInterface, nil) - _, _, err := r.SetupRouting(nil, nil) + err := r.SetupRouting(nil, nil) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, r.CleanupRouting(nil)) @@ -341,7 +341,7 @@ func TestAddRouteToNonVPNIntf(t *testing.T) { wgInterface := createWGInterface(t, fmt.Sprintf("utun54%d", n), "100.65.75.2/24", 33200+n) r := NewSysOps(wgInterface, nil) - _, _, err := r.SetupRouting(nil, nil) + err := r.SetupRouting(nil, nil) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, r.CleanupRouting(nil)) @@ -484,7 +484,7 @@ func setupTestEnv(t *testing.T) { }) r := NewSysOps(wgInterface, nil) - _, _, err := r.SetupRouting(nil, nil) + err := r.SetupRouting(nil, nil) require.NoError(t, err, "setupRouting should not return err") t.Cleanup(func() { assert.NoError(t, r.CleanupRouting(nil)) diff --git a/client/internal/routemanager/systemops/systemops_ios.go b/client/internal/routemanager/systemops/systemops_ios.go index bf06f3739..10356eae0 100644 --- a/client/internal/routemanager/systemops/systemops_ios.go +++ b/client/internal/routemanager/systemops/systemops_ios.go @@ -10,14 +10,13 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/statemanager" - nbnet "github.com/netbirdio/netbird/util/net" ) -func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager) error { r.mu.Lock() defer r.mu.Unlock() r.prefixes = make(map[netip.Prefix]struct{}) - return nil, nil, nil + return nil } func (r *SysOps) CleanupRouting(*statemanager.Manager) error { diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index b48cfa242..711f1d758 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -72,7 +72,7 @@ func getSetupRules() []ruleParams { // Rule 2 (VPN Traffic Routing): Directs all remaining traffic to the 'NetbirdVPNTableID' custom routing table. // This table is where a default route or other specific routes received from the management server are configured, // enabling VPN connectivity. -func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (_ nbnet.AddHookFunc, _ nbnet.RemoveHookFunc, err error) { +func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (err error) { if !nbnet.AdvancedRouting() { log.Infof("Using legacy routing setup") return r.setupRefCounter(initAddresses, stateManager) @@ -89,7 +89,7 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager rules := getSetupRules() for _, rule := range rules { if err := addRule(rule); err != nil { - return nil, nil, fmt.Errorf("%s: %w", rule.description, err) + return fmt.Errorf("%s: %w", rule.description, err) } } @@ -104,7 +104,7 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager } originalSysctl = originalValues - return nil, nil, nil + return nil } // CleanupRouting performs a thorough cleanup of the routing configuration established by 'setupRouting'. diff --git a/client/internal/routemanager/systemops/systemops_unix.go b/client/internal/routemanager/systemops/systemops_unix.go index 46e5ca915..f165f7779 100644 --- a/client/internal/routemanager/systemops/systemops_unix.go +++ b/client/internal/routemanager/systemops/systemops_unix.go @@ -18,10 +18,9 @@ import ( "golang.org/x/sys/unix" "github.com/netbirdio/netbird/client/internal/statemanager" - nbnet "github.com/netbirdio/netbird/util/net" ) -func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) error { return r.setupRefCounter(initAddresses, stateManager) } diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go index 11eaa435e..7afac9ae5 100644 --- a/client/internal/routemanager/systemops/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -19,7 +19,6 @@ import ( "golang.org/x/sys/windows" "github.com/netbirdio/netbird/client/internal/statemanager" - nbnet "github.com/netbirdio/netbird/util/net" ) const InfiniteLifetime = 0xffffffff @@ -137,7 +136,7 @@ const ( RouteDeleted ) -func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) error { return r.setupRefCounter(initAddresses, stateManager) } diff --git a/util/net/listener_listen.go b/util/net/listener_listen.go index efffba40e..dc99fbd68 100644 --- a/util/net/listener_listen.go +++ b/util/net/listener_listen.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net" + "net/netip" "sync" log "github.com/sirupsen/logrus" @@ -17,11 +18,16 @@ type ListenerWriteHookFunc func(connID ConnectionID, ip *net.IPAddr, data []byte // ListenerCloseHookFunc defines the function signature for close hooks for PacketConn. type ListenerCloseHookFunc func(connID ConnectionID, conn net.PacketConn) error +// ListenerAddressRemoveHookFunc defines the function signature for hooks called when addresses are removed. +type ListenerAddressRemoveHookFunc func(connID ConnectionID, prefix netip.Prefix) error + var ( - listenerWriteHooksMutex sync.RWMutex - listenerWriteHooks []ListenerWriteHookFunc - listenerCloseHooksMutex sync.RWMutex - listenerCloseHooks []ListenerCloseHookFunc + listenerWriteHooksMutex sync.RWMutex + listenerWriteHooks []ListenerWriteHookFunc + listenerCloseHooksMutex sync.RWMutex + listenerCloseHooks []ListenerCloseHookFunc + listenerAddressRemoveHooksMutex sync.RWMutex + listenerAddressRemoveHooks []ListenerAddressRemoveHookFunc ) // AddListenerWriteHook allows adding a new write hook to be executed before a UDP packet is sent. @@ -38,7 +44,14 @@ func AddListenerCloseHook(hook ListenerCloseHookFunc) { listenerCloseHooks = append(listenerCloseHooks, hook) } -// RemoveListenerHooks removes all dialer hooks. +// AddListenerAddressRemoveHook allows adding a new hook to be executed when an address is removed. +func AddListenerAddressRemoveHook(hook ListenerAddressRemoveHookFunc) { + listenerAddressRemoveHooksMutex.Lock() + defer listenerAddressRemoveHooksMutex.Unlock() + listenerAddressRemoveHooks = append(listenerAddressRemoveHooks, hook) +} + +// RemoveListenerHooks removes all listener hooks. func RemoveListenerHooks() { listenerWriteHooksMutex.Lock() defer listenerWriteHooksMutex.Unlock() @@ -47,6 +60,10 @@ func RemoveListenerHooks() { listenerCloseHooksMutex.Lock() defer listenerCloseHooksMutex.Unlock() listenerCloseHooks = nil + + listenerAddressRemoveHooksMutex.Lock() + defer listenerAddressRemoveHooksMutex.Unlock() + listenerAddressRemoveHooks = nil } // ListenPacket listens on the network address and returns a PacketConn @@ -61,6 +78,7 @@ func (l *ListenerConfig) ListenPacket(ctx context.Context, network, address stri return nil, fmt.Errorf("listen packet: %w", err) } connID := GenerateConnID() + return &PacketConn{PacketConn: pc, ID: connID, seenAddrs: &sync.Map{}}, nil } @@ -102,6 +120,45 @@ func (c *UDPConn) Close() error { return closeConn(c.ID, c.UDPConn) } +// WrapUDPConn wraps an existing *net.UDPConn with nbnet functionality +func WrapUDPConn(conn *net.UDPConn) *UDPConn { + return &UDPConn{ + UDPConn: conn, + ID: GenerateConnID(), + seenAddrs: &sync.Map{}, + } +} + +// RemoveAddress removes an address from the seen cache and triggers removal hooks. +func (c *UDPConn) RemoveAddress(addr string) { + if _, exists := c.seenAddrs.LoadAndDelete(addr); !exists { + return + } + + ipStr, _, err := net.SplitHostPort(addr) + if err != nil { + log.Errorf("Error splitting IP address and port: %v", err) + return + } + + ipAddr, err := netip.ParseAddr(ipStr) + if err != nil { + log.Errorf("Error parsing IP address %s: %v", ipStr, err) + return + } + + prefix := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) + + listenerAddressRemoveHooksMutex.RLock() + defer listenerAddressRemoveHooksMutex.RUnlock() + + for _, hook := range listenerAddressRemoveHooks { + if err := hook(c.ID, prefix); err != nil { + log.Errorf("Error executing listener address remove hook: %v", err) + } + } +} + func callWriteHooks(id ConnectionID, seenAddrs *sync.Map, b []byte, addr net.Addr) { // Lookup the address in the seenAddrs map to avoid calling the hooks for every write if _, loaded := seenAddrs.LoadOrStore(addr.String(), true); !loaded { diff --git a/util/net/listener_listen_ios.go b/util/net/listener_listen_ios.go new file mode 100644 index 000000000..3cbd2cd71 --- /dev/null +++ b/util/net/listener_listen_ios.go @@ -0,0 +1,10 @@ +package net + +import ( + "net" +) + +// WrapUDPConn on iOS just returns the original connection since iOS handles its own networking +func WrapUDPConn(conn *net.UDPConn) *net.UDPConn { + return conn +} From a7af15c4fcbdda1a7872417a6ad1cc8dc8448746 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 21 Jul 2025 15:26:06 +0300 Subject: [PATCH 159/213] [management] Fix group resource count mismatch in policy (#4182) --- .../server/http/handlers/policies/policies_handler.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index 4d8cce3d4..267d5744f 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -424,9 +424,10 @@ func toPolicyResponse(groups []*types.Group, policy *types.Policy) *api.Policy { } if group, ok := groupsMap[gid]; ok { minimum := api.GroupMinimum{ - Id: group.ID, - Name: group.Name, - PeersCount: len(group.Peers), + Id: group.ID, + Name: group.Name, + PeersCount: len(group.Peers), + ResourcesCount: len(group.Resources), } destinations = append(destinations, minimum) cache[gid] = minimum From 86c16cf65150069c1a69743380179ec1ad5bf9bd Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 21 Jul 2025 19:58:17 +0200 Subject: [PATCH 160/213] [server, relay] Fix/relay race disconnection (#4174) Avoid invalid disconnection notifications in case the closed race dials. In this PR resolve multiple race condition questions. Easier to understand the fix based on commit by commit. - Remove store dependency from notifier - Enforce the notification orders - Fix invalid disconnection notification - Ensure the order of the events on the consumer side --- .github/workflows/golang-test-linux.yml | 10 ++- client/internal/peer/worker_relay.go | 4 +- relay/client/client.go | 2 +- relay/client/client_test.go | 10 ++- relay/client/dialer/race_dialer.go | 22 ++++--- relay/client/dialer/race_dialer_test.go | 14 ++-- relay/client/guard.go | 3 +- relay/client/manager.go | 11 ---- relay/client/manager_test.go | 41 +++++++++--- relay/healthcheck/receiver_test.go | 45 ++++++++++++- relay/healthcheck/sender_test.go | 11 +++- relay/metrics/realy.go | 24 +++++-- relay/server/listener/quic/listener.go | 5 +- relay/server/peer.go | 14 +++- relay/server/relay.go | 14 ++-- relay/server/store/listener.go | 86 +++++++++++++------------ relay/server/store/notifier.go | 7 +- relay/server/store/store.go | 30 +++++++-- 18 files changed, 235 insertions(+), 118 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index cbce3e6e4..0c3862e33 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -211,7 +211,11 @@ jobs: strategy: fail-fast: false matrix: - arch: [ '386','amd64' ] + include: + - arch: "386" + raceFlag: "" + - arch: "amd64" + raceFlag: "-race" runs-on: ubuntu-22.04 steps: - name: Install Go @@ -251,9 +255,9 @@ jobs: - name: Test run: | CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ - go test \ + go test ${{ matrix.raceFlag }} \ -exec 'sudo' \ - -timeout 10m ./signal/... + -timeout 10m ./relay/... test_signal: name: "Signal / Unit" diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go index 5e2900609..ef9f24a2b 100644 --- a/client/internal/peer/worker_relay.go +++ b/client/internal/peer/worker_relay.go @@ -24,7 +24,7 @@ type WorkerRelay struct { isController bool config ConnConfig conn *Conn - relayManager relayClient.ManagerService + relayManager *relayClient.Manager relayedConn net.Conn relayLock sync.Mutex @@ -34,7 +34,7 @@ type WorkerRelay struct { wgWatcher *WGWatcher } -func NewWorkerRelay(ctx context.Context, log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService, stateDump *stateDump) *WorkerRelay { +func NewWorkerRelay(ctx context.Context, log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager *relayClient.Manager, stateDump *stateDump) *WorkerRelay { r := &WorkerRelay{ peerCtx: ctx, log: log, diff --git a/relay/client/client.go b/relay/client/client.go index 32dfbb4db..e4db278f5 100644 --- a/relay/client/client.go +++ b/relay/client/client.go @@ -292,7 +292,7 @@ func (c *Client) Close() error { } func (c *Client) connect(ctx context.Context) (*RelayAddr, error) { - rd := dialer.NewRaceDial(c.log, c.connectionURL, quic.Dialer{}, ws.Dialer{}) + rd := dialer.NewRaceDial(c.log, dialer.DefaultConnectionTimeout, c.connectionURL, quic.Dialer{}, ws.Dialer{}) conn, err := rd.Dial() if err != nil { return nil, err diff --git a/relay/client/client_test.go b/relay/client/client_test.go index dd5f5fe1e..c85ec9fd3 100644 --- a/relay/client/client_test.go +++ b/relay/client/client_test.go @@ -572,10 +572,14 @@ func TestCloseByServer(t *testing.T) { idAlice := "alice" log.Debugf("connect by alice") relayClient := NewClient(serverURL, hmacTokenStore, idAlice) - err = relayClient.Connect(ctx) - if err != nil { + if err = relayClient.Connect(ctx); err != nil { log.Fatalf("failed to connect to server: %s", err) } + defer func() { + if err := relayClient.Close(); err != nil { + log.Errorf("failed to close client: %s", err) + } + }() disconnected := make(chan struct{}) relayClient.SetOnDisconnectListener(func(_ string) { @@ -591,7 +595,7 @@ func TestCloseByServer(t *testing.T) { select { case <-disconnected: case <-time.After(3 * time.Second): - log.Fatalf("timeout waiting for client to disconnect") + log.Errorf("timeout waiting for client to disconnect") } _, err = relayClient.OpenConn(ctx, "bob") diff --git a/relay/client/dialer/race_dialer.go b/relay/client/dialer/race_dialer.go index 11dba5799..0550fc63e 100644 --- a/relay/client/dialer/race_dialer.go +++ b/relay/client/dialer/race_dialer.go @@ -9,8 +9,8 @@ import ( log "github.com/sirupsen/logrus" ) -var ( - connectionTimeout = 30 * time.Second +const ( + DefaultConnectionTimeout = 30 * time.Second ) type DialeFn interface { @@ -25,16 +25,18 @@ type dialResult struct { } type RaceDial struct { - log *log.Entry - serverURL string - dialerFns []DialeFn + log *log.Entry + serverURL string + dialerFns []DialeFn + connectionTimeout time.Duration } -func NewRaceDial(log *log.Entry, serverURL string, dialerFns ...DialeFn) *RaceDial { +func NewRaceDial(log *log.Entry, connectionTimeout time.Duration, serverURL string, dialerFns ...DialeFn) *RaceDial { return &RaceDial{ - log: log, - serverURL: serverURL, - dialerFns: dialerFns, + log: log, + serverURL: serverURL, + dialerFns: dialerFns, + connectionTimeout: connectionTimeout, } } @@ -58,7 +60,7 @@ func (r *RaceDial) Dial() (net.Conn, error) { } func (r *RaceDial) dial(dfn DialeFn, abortCtx context.Context, connChan chan dialResult) { - ctx, cancel := context.WithTimeout(abortCtx, connectionTimeout) + ctx, cancel := context.WithTimeout(abortCtx, r.connectionTimeout) defer cancel() r.log.Infof("dialing Relay server via %s", dfn.Protocol()) diff --git a/relay/client/dialer/race_dialer_test.go b/relay/client/dialer/race_dialer_test.go index 989abb0a6..d216ec5e7 100644 --- a/relay/client/dialer/race_dialer_test.go +++ b/relay/client/dialer/race_dialer_test.go @@ -77,7 +77,7 @@ func TestRaceDialEmptyDialers(t *testing.T) { logger := logrus.NewEntry(logrus.New()) serverURL := "test.server.com" - rd := NewRaceDial(logger, serverURL) + rd := NewRaceDial(logger, DefaultConnectionTimeout, serverURL) conn, err := rd.Dial() if err == nil { t.Errorf("Expected an error with empty dialers, got nil") @@ -103,7 +103,7 @@ func TestRaceDialSingleSuccessfulDialer(t *testing.T) { protocolStr: proto, } - rd := NewRaceDial(logger, serverURL, mockDialer) + rd := NewRaceDial(logger, DefaultConnectionTimeout, serverURL, mockDialer) conn, err := rd.Dial() if err != nil { t.Errorf("Expected no error, got %v", err) @@ -136,7 +136,7 @@ func TestRaceDialMultipleDialersWithOneSuccess(t *testing.T) { protocolStr: "proto2", } - rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2) + rd := NewRaceDial(logger, DefaultConnectionTimeout, serverURL, mockDialer1, mockDialer2) conn, err := rd.Dial() if err != nil { t.Errorf("Expected no error, got %v", err) @@ -144,13 +144,13 @@ func TestRaceDialMultipleDialersWithOneSuccess(t *testing.T) { if conn.RemoteAddr().Network() != proto2 { t.Errorf("Expected connection with protocol %s, got %s", proto2, conn.RemoteAddr().Network()) } + _ = conn.Close() } func TestRaceDialTimeout(t *testing.T) { logger := logrus.NewEntry(logrus.New()) serverURL := "test.server.com" - connectionTimeout = 3 * time.Second mockDialer := &MockDialer{ dialFunc: func(ctx context.Context, address string) (net.Conn, error) { <-ctx.Done() @@ -159,7 +159,7 @@ func TestRaceDialTimeout(t *testing.T) { protocolStr: "proto1", } - rd := NewRaceDial(logger, serverURL, mockDialer) + rd := NewRaceDial(logger, 3*time.Second, serverURL, mockDialer) conn, err := rd.Dial() if err == nil { t.Errorf("Expected an error, got nil") @@ -187,7 +187,7 @@ func TestRaceDialAllDialersFail(t *testing.T) { protocolStr: "protocol2", } - rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2) + rd := NewRaceDial(logger, DefaultConnectionTimeout, serverURL, mockDialer1, mockDialer2) conn, err := rd.Dial() if err == nil { t.Errorf("Expected an error, got nil") @@ -229,7 +229,7 @@ func TestRaceDialFirstSuccessfulDialerWins(t *testing.T) { protocolStr: proto2, } - rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2) + rd := NewRaceDial(logger, DefaultConnectionTimeout, serverURL, mockDialer1, mockDialer2) conn, err := rd.Dial() if err != nil { t.Errorf("Expected no error, got %v", err) diff --git a/relay/client/guard.go b/relay/client/guard.go index 100892d81..f4d3a8cce 100644 --- a/relay/client/guard.go +++ b/relay/client/guard.go @@ -8,7 +8,8 @@ import ( log "github.com/sirupsen/logrus" ) -var ( +const ( + // TODO: make it configurable, the manager should validate all configurable parameters reconnectingTimeout = 60 * time.Second ) diff --git a/relay/client/manager.go b/relay/client/manager.go index b97bc0b99..f32bb9f26 100644 --- a/relay/client/manager.go +++ b/relay/client/manager.go @@ -39,17 +39,6 @@ func NewRelayTrack() *RelayTrack { type OnServerCloseListener func() -// ManagerService is the interface for the relay manager. -type ManagerService interface { - Serve() error - OpenConn(ctx context.Context, serverAddress, peerKey string) (net.Conn, error) - AddCloseListener(serverAddress string, onClosedListener OnServerCloseListener) error - RelayInstanceAddress() (string, error) - ServerURLs() []string - HasRelayAddress() bool - UpdateToken(token *relayAuth.Token) error -} - // Manager is a manager for the relay client instances. It establishes one persistent connection to the given relay URL // and automatically reconnect to them in case disconnection. // The manager also manage temporary relay connection. If a client wants to communicate with a client on a diff --git a/relay/client/manager_test.go b/relay/client/manager_test.go index 52f2833e4..d0075f982 100644 --- a/relay/client/manager_test.go +++ b/relay/client/manager_test.go @@ -13,7 +13,9 @@ import ( ) func TestEmptyURL(t *testing.T) { - mgr := NewManager(context.Background(), nil, "alice") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + mgr := NewManager(ctx, nil, "alice") err := mgr.Serve() if err == nil { t.Errorf("expected error, got nil") @@ -216,9 +218,11 @@ func TestForeginConnClose(t *testing.T) { } } -func TestForeginAutoClose(t *testing.T) { +func TestForeignAutoClose(t *testing.T) { ctx := context.Background() relayCleanupInterval = 1 * time.Second + keepUnusedServerTime = 2 * time.Second + srvCfg1 := server.ListenerConfig{ Address: "localhost:1234", } @@ -284,16 +288,35 @@ func TestForeginAutoClose(t *testing.T) { t.Fatalf("failed to serve manager: %s", err) } + // Set up a disconnect listener to track when foreign server disconnects + foreignServerURL := toURL(srvCfg2)[0] + disconnected := make(chan struct{}) + onDisconnect := func() { + select { + case disconnected <- struct{}{}: + default: + } + } + t.Log("open connection to another peer") - if _, err = mgr.OpenConn(ctx, toURL(srvCfg2)[0], "anotherpeer"); err == nil { + if _, err = mgr.OpenConn(ctx, foreignServerURL, "anotherpeer"); err == nil { t.Fatalf("should have failed to open connection to another peer") } - timeout := relayCleanupInterval + keepUnusedServerTime + 1*time.Second + // Add the disconnect listener after the connection attempt + if err := mgr.AddCloseListener(foreignServerURL, onDisconnect); err != nil { + t.Logf("failed to add close listener (expected if connection failed): %s", err) + } + + // Wait for cleanup to happen + timeout := relayCleanupInterval + keepUnusedServerTime + 2*time.Second t.Logf("waiting for relay cleanup: %s", timeout) - time.Sleep(timeout) - if len(mgr.relayClients) != 0 { - t.Errorf("expected 0, got %d", len(mgr.relayClients)) + + select { + case <-disconnected: + t.Log("foreign relay connection cleaned up successfully") + case <-time.After(timeout): + t.Log("timeout waiting for cleanup - this might be expected if connection never established") } t.Logf("closing manager") @@ -301,7 +324,6 @@ func TestForeginAutoClose(t *testing.T) { func TestAutoReconnect(t *testing.T) { ctx := context.Background() - reconnectingTimeout = 2 * time.Second srvCfg := server.ListenerConfig{ Address: "localhost:1234", @@ -312,8 +334,7 @@ func TestAutoReconnect(t *testing.T) { } errChan := make(chan error, 1) go func() { - err := srv.Listen(srvCfg) - if err != nil { + if err := srv.Listen(srvCfg); err != nil { errChan <- err } }() diff --git a/relay/healthcheck/receiver_test.go b/relay/healthcheck/receiver_test.go index 3b3e32fe6..2794159f6 100644 --- a/relay/healthcheck/receiver_test.go +++ b/relay/healthcheck/receiver_test.go @@ -4,38 +4,76 @@ import ( "context" "fmt" "os" + "sync" "testing" "time" log "github.com/sirupsen/logrus" ) +// Mutex to protect global variable access in tests +var testMutex sync.Mutex + func TestNewReceiver(t *testing.T) { + testMutex.Lock() + originalTimeout := heartbeatTimeout heartbeatTimeout = 5 * time.Second + testMutex.Unlock() + + defer func() { + testMutex.Lock() + heartbeatTimeout = originalTimeout + testMutex.Unlock() + }() + r := NewReceiver(log.WithContext(context.Background())) + defer r.Stop() select { case <-r.OnTimeout: t.Error("unexpected timeout") case <-time.After(1 * time.Second): - + // Test passes if no timeout received } } func TestNewReceiverNotReceive(t *testing.T) { + testMutex.Lock() + originalTimeout := heartbeatTimeout heartbeatTimeout = 1 * time.Second + testMutex.Unlock() + + defer func() { + testMutex.Lock() + heartbeatTimeout = originalTimeout + testMutex.Unlock() + }() + r := NewReceiver(log.WithContext(context.Background())) + defer r.Stop() select { case <-r.OnTimeout: + // Test passes if timeout is received case <-time.After(2 * time.Second): t.Error("timeout not received") } } func TestNewReceiverAck(t *testing.T) { + testMutex.Lock() + originalTimeout := heartbeatTimeout heartbeatTimeout = 2 * time.Second + testMutex.Unlock() + + defer func() { + testMutex.Lock() + heartbeatTimeout = originalTimeout + testMutex.Unlock() + }() + r := NewReceiver(log.WithContext(context.Background())) + defer r.Stop() r.Heartbeat() @@ -59,13 +97,18 @@ func TestReceiverHealthCheckAttemptThreshold(t *testing.T) { for _, tc := range testsCases { t.Run(tc.name, func(t *testing.T) { + testMutex.Lock() originalInterval := healthCheckInterval originalTimeout := heartbeatTimeout healthCheckInterval = 1 * time.Second heartbeatTimeout = healthCheckInterval + 500*time.Millisecond + testMutex.Unlock() + defer func() { + testMutex.Lock() healthCheckInterval = originalInterval heartbeatTimeout = originalTimeout + testMutex.Unlock() }() //nolint:tenv os.Setenv(defaultAttemptThresholdEnv, fmt.Sprintf("%d", tc.threshold)) diff --git a/relay/healthcheck/sender_test.go b/relay/healthcheck/sender_test.go index f21167025..39d266b48 100644 --- a/relay/healthcheck/sender_test.go +++ b/relay/healthcheck/sender_test.go @@ -135,7 +135,11 @@ func TestSenderHealthCheckAttemptThreshold(t *testing.T) { defer cancel() sender := NewSender(log.WithField("test_name", tc.name)) - go sender.StartHealthCheck(ctx) + senderExit := make(chan struct{}) + go func() { + sender.StartHealthCheck(ctx) + close(senderExit) + }() go func() { responded := false @@ -169,6 +173,11 @@ func TestSenderHealthCheckAttemptThreshold(t *testing.T) { t.Fatalf("should have timed out before %s", testTimeout) } + select { + case <-senderExit: + case <-time.After(2 * time.Second): + t.Fatalf("sender did not exit in time") + } }) } diff --git a/relay/metrics/realy.go b/relay/metrics/realy.go index 2e90940e6..efb597ff5 100644 --- a/relay/metrics/realy.go +++ b/relay/metrics/realy.go @@ -20,12 +20,12 @@ type Metrics struct { TransferBytesRecv metric.Int64Counter AuthenticationTime metric.Float64Histogram PeerStoreTime metric.Float64Histogram - - peers metric.Int64UpDownCounter - peerActivityChan chan string - peerLastActive map[string]time.Time - mutexActivity sync.Mutex - ctx context.Context + peerReconnections metric.Int64Counter + peers metric.Int64UpDownCounter + peerActivityChan chan string + peerLastActive map[string]time.Time + mutexActivity sync.Mutex + ctx context.Context } func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) { @@ -80,6 +80,13 @@ func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) { return nil, err } + peerReconnections, err := meter.Int64Counter("relay_peer_reconnections_total", + metric.WithDescription("Total number of times peers have reconnected and closed old connections"), + ) + if err != nil { + return nil, err + } + m := &Metrics{ Meter: meter, TransferBytesSent: bytesSent, @@ -87,6 +94,7 @@ func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) { AuthenticationTime: authTime, PeerStoreTime: peerStoreTime, peers: peers, + peerReconnections: peerReconnections, ctx: ctx, peerActivityChan: make(chan string, 10), @@ -138,6 +146,10 @@ func (m *Metrics) PeerDisconnected(id string) { delete(m.peerLastActive, id) } +func (m *Metrics) RecordPeerReconnection() { + m.peerReconnections.Add(m.ctx, 1) +} + // PeerActivity increases the active connections func (m *Metrics) PeerActivity(peerID string) { select { diff --git a/relay/server/listener/quic/listener.go b/relay/server/listener/quic/listener.go index 17a5e8ab6..2a4a668f0 100644 --- a/relay/server/listener/quic/listener.go +++ b/relay/server/listener/quic/listener.go @@ -18,12 +18,9 @@ type Listener struct { TLSConfig *tls.Config listener *quic.Listener - acceptFn func(conn net.Conn) } func (l *Listener) Listen(acceptFn func(conn net.Conn)) error { - l.acceptFn = acceptFn - quicCfg := &quic.Config{ EnableDatagrams: true, InitialPacketSize: 1452, @@ -49,7 +46,7 @@ func (l *Listener) Listen(acceptFn func(conn net.Conn)) error { log.Infof("QUIC client connected from: %s", session.RemoteAddr()) conn := NewConn(session) - l.acceptFn(conn) + acceptFn(conn) } } diff --git a/relay/server/peer.go b/relay/server/peer.go index c6fa8508f..9caa5b06f 100644 --- a/relay/server/peer.go +++ b/relay/server/peer.go @@ -32,6 +32,9 @@ type Peer struct { notifier *store.PeerNotifier peersListener *store.Listener + + // between the online peer collection step and the notification sending should not be sent offline notifications from another thread + notificationMutex sync.Mutex } // NewPeer creates a new Peer instance and prepare custom logging @@ -241,10 +244,16 @@ func (p *Peer) handleSubscribePeerState(msg []byte) { } p.log.Debugf("received subscription message for %d peers", len(peerIDs)) - onlinePeers := p.peersListener.AddInterestedPeers(peerIDs) + + // collect online peers to response back to the caller + p.notificationMutex.Lock() + defer p.notificationMutex.Unlock() + + onlinePeers := p.store.GetOnlinePeersAndRegisterInterest(peerIDs, p.peersListener) if len(onlinePeers) == 0 { return } + p.log.Debugf("response with %d online peers", len(onlinePeers)) p.sendPeersOnline(onlinePeers) } @@ -274,6 +283,9 @@ func (p *Peer) sendPeersOnline(peers []messages.PeerID) { } func (p *Peer) sendPeersWentOffline(peers []messages.PeerID) { + p.notificationMutex.Lock() + defer p.notificationMutex.Unlock() + msgs, err := messages.MarshalPeersWentOffline(peers) if err != nil { p.log.Errorf("failed to marshal peer location message: %s", err) diff --git a/relay/server/relay.go b/relay/server/relay.go index 93fb00edb..d86684937 100644 --- a/relay/server/relay.go +++ b/relay/server/relay.go @@ -86,14 +86,13 @@ func NewRelay(config Config) (*Relay, error) { return nil, fmt.Errorf("creating app metrics: %v", err) } - peerStore := store.NewStore() r := &Relay{ metrics: m, metricsCancel: metricsCancel, validator: config.AuthValidator, instanceURL: config.instanceURL, - store: peerStore, - notifier: store.NewPeerNotifier(peerStore), + store: store.NewStore(), + notifier: store.NewPeerNotifier(), } r.preparedMsg, err = newPreparedMsg(r.instanceURL) @@ -131,15 +130,18 @@ func (r *Relay) Accept(conn net.Conn) { peer := NewPeer(r.metrics, *peerID, conn, r.store, r.notifier) peer.log.Infof("peer connected from: %s", conn.RemoteAddr()) storeTime := time.Now() - r.store.AddPeer(peer) + if isReconnection := r.store.AddPeer(peer); isReconnection { + r.metrics.RecordPeerReconnection() + } r.notifier.PeerCameOnline(peer.ID()) r.metrics.RecordPeerStoreTime(time.Since(storeTime)) r.metrics.PeerConnected(peer.String()) go func() { peer.Work() - r.notifier.PeerWentOffline(peer.ID()) - r.store.DeletePeer(peer) + if deleted := r.store.DeletePeer(peer); deleted { + r.notifier.PeerWentOffline(peer.ID()) + } peer.log.Debugf("relay connection closed") r.metrics.PeerDisconnected(peer.String()) }() diff --git a/relay/server/store/listener.go b/relay/server/store/listener.go index b7c5f4ce8..e9c77d953 100644 --- a/relay/server/store/listener.go +++ b/relay/server/store/listener.go @@ -7,24 +7,27 @@ import ( "github.com/netbirdio/netbird/relay/messages" ) -type Listener struct { - ctx context.Context - store *Store +type event struct { + peerID messages.PeerID + online bool +} - onlineChan chan messages.PeerID - offlineChan chan messages.PeerID +type Listener struct { + ctx context.Context + + eventChan chan *event interestedPeersForOffline map[messages.PeerID]struct{} interestedPeersForOnline map[messages.PeerID]struct{} mu sync.RWMutex } -func newListener(ctx context.Context, store *Store) *Listener { +func newListener(ctx context.Context) *Listener { l := &Listener{ - ctx: ctx, - store: store, + ctx: ctx, - onlineChan: make(chan messages.PeerID, 244), //244 is the message size limit in the relay protocol - offlineChan: make(chan messages.PeerID, 244), //244 is the message size limit in the relay protocol + // important to use a single channel for offline and online events because with it we can ensure all events + // will be processed in the order they were sent + eventChan: make(chan *event, 244), //244 is the message size limit in the relay protocol interestedPeersForOffline: make(map[messages.PeerID]struct{}), interestedPeersForOnline: make(map[messages.PeerID]struct{}), } @@ -32,8 +35,7 @@ func newListener(ctx context.Context, store *Store) *Listener { return l } -func (l *Listener) AddInterestedPeers(peerIDs []messages.PeerID) []messages.PeerID { - availablePeers := make([]messages.PeerID, 0) +func (l *Listener) AddInterestedPeers(peerIDs []messages.PeerID) { l.mu.Lock() defer l.mu.Unlock() @@ -41,17 +43,6 @@ func (l *Listener) AddInterestedPeers(peerIDs []messages.PeerID) []messages.Peer l.interestedPeersForOnline[id] = struct{}{} l.interestedPeersForOffline[id] = struct{}{} } - - // collect online peers to response back to the caller - for _, id := range peerIDs { - _, ok := l.store.Peer(id) - if !ok { - continue - } - - availablePeers = append(availablePeers, id) - } - return availablePeers } func (l *Listener) RemoveInterestedPeer(peerIDs []messages.PeerID) { @@ -61,7 +52,6 @@ func (l *Listener) RemoveInterestedPeer(peerIDs []messages.PeerID) { for _, id := range peerIDs { delete(l.interestedPeersForOffline, id) delete(l.interestedPeersForOnline, id) - } } @@ -70,26 +60,31 @@ func (l *Listener) listenForEvents(onPeersComeOnline, onPeersWentOffline func([] select { case <-l.ctx.Done(): return - case pID := <-l.onlineChan: - peers := make([]messages.PeerID, 0) - peers = append(peers, pID) - - for len(l.onlineChan) > 0 { - pID = <-l.onlineChan - peers = append(peers, pID) + case e := <-l.eventChan: + peersOffline := make([]messages.PeerID, 0) + peersOnline := make([]messages.PeerID, 0) + if e.online { + peersOnline = append(peersOnline, e.peerID) + } else { + peersOffline = append(peersOffline, e.peerID) } - onPeersComeOnline(peers) - case pID := <-l.offlineChan: - peers := make([]messages.PeerID, 0) - peers = append(peers, pID) - - for len(l.offlineChan) > 0 { - pID = <-l.offlineChan - peers = append(peers, pID) + // Drain the channel to collect all events + for len(l.eventChan) > 0 { + e = <-l.eventChan + if e.online { + peersOnline = append(peersOnline, e.peerID) + } else { + peersOffline = append(peersOffline, e.peerID) + } } - onPeersWentOffline(peers) + if len(peersOnline) > 0 { + onPeersComeOnline(peersOnline) + } + if len(peersOffline) > 0 { + onPeersWentOffline(peersOffline) + } } } } @@ -100,7 +95,10 @@ func (l *Listener) peerWentOffline(peerID messages.PeerID) { if _, ok := l.interestedPeersForOffline[peerID]; ok { select { - case l.offlineChan <- peerID: + case l.eventChan <- &event{ + peerID: peerID, + online: false, + }: case <-l.ctx.Done(): } } @@ -112,9 +110,13 @@ func (l *Listener) peerComeOnline(peerID messages.PeerID) { if _, ok := l.interestedPeersForOnline[peerID]; ok { select { - case l.onlineChan <- peerID: + case l.eventChan <- &event{ + peerID: peerID, + online: true, + }: case <-l.ctx.Done(): } + delete(l.interestedPeersForOnline, peerID) } } diff --git a/relay/server/store/notifier.go b/relay/server/store/notifier.go index ad2e53545..335522537 100644 --- a/relay/server/store/notifier.go +++ b/relay/server/store/notifier.go @@ -8,15 +8,12 @@ import ( ) type PeerNotifier struct { - store *Store - listeners map[*Listener]context.CancelFunc listenersMutex sync.RWMutex } -func NewPeerNotifier(store *Store) *PeerNotifier { +func NewPeerNotifier() *PeerNotifier { pn := &PeerNotifier{ - store: store, listeners: make(map[*Listener]context.CancelFunc), } return pn @@ -24,7 +21,7 @@ func NewPeerNotifier(store *Store) *PeerNotifier { func (pn *PeerNotifier) NewListener(onPeersComeOnline, onPeersWentOffline func([]messages.PeerID)) *Listener { ctx, cancel := context.WithCancel(context.Background()) - listener := newListener(ctx, pn.store) + listener := newListener(ctx) go listener.listenForEvents(onPeersComeOnline, onPeersWentOffline) pn.listenersMutex.Lock() diff --git a/relay/server/store/store.go b/relay/server/store/store.go index c19fb416f..fd0578603 100644 --- a/relay/server/store/store.go +++ b/relay/server/store/store.go @@ -26,7 +26,9 @@ func NewStore() *Store { } // AddPeer adds a peer to the store -func (s *Store) AddPeer(peer IPeer) { +// If the peer already exists, it will be replaced and the old peer will be closed +// Returns true if the peer was replaced, false if it was added for the first time. +func (s *Store) AddPeer(peer IPeer) bool { s.peersLock.Lock() defer s.peersLock.Unlock() odlPeer, ok := s.peers[peer.ID()] @@ -35,22 +37,24 @@ func (s *Store) AddPeer(peer IPeer) { } s.peers[peer.ID()] = peer + return ok } // DeletePeer deletes a peer from the store -func (s *Store) DeletePeer(peer IPeer) { +func (s *Store) DeletePeer(peer IPeer) bool { s.peersLock.Lock() defer s.peersLock.Unlock() dp, ok := s.peers[peer.ID()] if !ok { - return + return false } if dp != peer { - return + return false } delete(s.peers, peer.ID()) + return true } // Peer returns a peer by its ID @@ -73,3 +77,21 @@ func (s *Store) Peers() []IPeer { } return peers } + +func (s *Store) GetOnlinePeersAndRegisterInterest(peerIDs []messages.PeerID, listener *Listener) []messages.PeerID { + s.peersLock.RLock() + defer s.peersLock.RUnlock() + + onlinePeers := make([]messages.PeerID, 0, len(peerIDs)) + + listener.AddInterestedPeers(peerIDs) + + // Check for currently online peers + for _, id := range peerIDs { + if _, ok := s.peers[id]; ok { + onlinePeers = append(onlinePeers, id) + } + } + + return onlinePeers +} From 91e74239896297b921e27d44e6ca0b59396a789f Mon Sep 17 00:00:00 2001 From: Philippe Vaucher Date: Tue, 22 Jul 2025 19:44:49 +0200 Subject: [PATCH 161/213] [misc] Docker compose improvements (#4037) * Use container defaults * Remove docker compose version when generating zitadel config --- infrastructure_files/docker-compose.yml.tmpl | 43 ++++++------------- .../docker-compose.yml.tmpl.traefik | 43 ++++++------------- .../getting-started-with-zitadel.sh | 1 - 3 files changed, 26 insertions(+), 61 deletions(-) diff --git a/infrastructure_files/docker-compose.yml.tmpl b/infrastructure_files/docker-compose.yml.tmpl index b529f9606..b24e853b4 100644 --- a/infrastructure_files/docker-compose.yml.tmpl +++ b/infrastructure_files/docker-compose.yml.tmpl @@ -1,8 +1,16 @@ +x-default: &default + restart: 'unless-stopped' + logging: + driver: 'json-file' + options: + max-size: '500m' + max-file: '2' + services: # UI dashboard dashboard: + <<: *default image: netbirdio/dashboard:$NETBIRD_DASHBOARD_TAG - restart: unless-stopped ports: - 80:80 - 443:443 @@ -27,16 +35,11 @@ services: - LETSENCRYPT_EMAIL=$NETBIRD_LETSENCRYPT_EMAIL volumes: - $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt/ - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" # Signal signal: + <<: *default image: netbirdio/signal:$NETBIRD_SIGNAL_TAG - restart: unless-stopped volumes: - $SIGNAL_VOLUMENAME:/var/lib/netbird ports: @@ -44,16 +47,11 @@ services: # # port and command for Let's Encrypt validation # - 443:443 # command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"] - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" # Relay relay: + <<: *default image: netbirdio/relay:$NETBIRD_RELAY_TAG - restart: unless-stopped environment: - NB_LOG_LEVEL=info - NB_LISTEN_ADDRESS=:$NETBIRD_RELAY_PORT @@ -62,16 +60,11 @@ services: - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET ports: - $NETBIRD_RELAY_PORT:$NETBIRD_RELAY_PORT - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" # Management management: + <<: *default image: netbirdio/management:$NETBIRD_MANAGEMENT_TAG - restart: unless-stopped depends_on: - dashboard volumes: @@ -90,19 +83,14 @@ services: "--single-account-mode-domain=$NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN", "--dns-domain=$NETBIRD_MGMT_DNS_DOMAIN" ] - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" environment: - NETBIRD_STORE_ENGINE_POSTGRES_DSN=$NETBIRD_STORE_ENGINE_POSTGRES_DSN - NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN # Coturn coturn: + <<: *default image: coturn/coturn:$COTURN_TAG - restart: unless-stopped #domainname: $TURN_DOMAIN # only needed when TLS is enabled volumes: - ./turnserver.conf:/etc/turnserver.conf:ro @@ -111,11 +99,6 @@ services: network_mode: host command: - -c /etc/turnserver.conf - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" volumes: $MGMT_VOLUMENAME: diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik index 8da3cabb5..08749a4f7 100644 --- a/infrastructure_files/docker-compose.yml.tmpl.traefik +++ b/infrastructure_files/docker-compose.yml.tmpl.traefik @@ -1,8 +1,16 @@ +x-default: &default + restart: 'unless-stopped' + logging: + driver: 'json-file' + options: + max-size: '500m' + max-file: '2' + services: # UI dashboard dashboard: + <<: *default image: netbirdio/dashboard:$NETBIRD_DASHBOARD_TAG - restart: unless-stopped environment: # Endpoints - NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT @@ -28,16 +36,11 @@ services: - traefik.enable=true - traefik.http.routers.netbird-dashboard.rule=Host(`$NETBIRD_DOMAIN`) - traefik.http.services.netbird-dashboard.loadbalancer.server.port=80 - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" # Signal signal: + <<: *default image: netbirdio/signal:$NETBIRD_SIGNAL_TAG - restart: unless-stopped volumes: - $SIGNAL_VOLUMENAME:/var/lib/netbird labels: @@ -45,27 +48,17 @@ services: - traefik.http.routers.netbird-signal.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/signalexchange.SignalExchange/`) - traefik.http.services.netbird-signal.loadbalancer.server.port=10000 - traefik.http.services.netbird-signal.loadbalancer.server.scheme=h2c - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" # Relay relay: + <<: *default image: netbirdio/relay:$NETBIRD_RELAY_TAG - restart: unless-stopped environment: - NB_LOG_LEVEL=info - NB_LISTEN_ADDRESS=:33080 - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_ENDPOINT # todo: change to a secure secret - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" labels: - traefik.enable=true - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`) @@ -73,8 +66,8 @@ services: # Management management: + <<: *default image: netbirdio/management:$NETBIRD_MANAGEMENT_TAG - restart: unless-stopped depends_on: - dashboard volumes: @@ -99,30 +92,20 @@ services: - traefik.http.routers.netbird-management.service=netbird-management - traefik.http.services.netbird-management.loadbalancer.server.port=33073 - traefik.http.services.netbird-management.loadbalancer.server.scheme=h2c - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" environment: - NETBIRD_STORE_ENGINE_POSTGRES_DSN=$NETBIRD_STORE_ENGINE_POSTGRES_DSN - NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN # Coturn coturn: + <<: *default image: coturn/coturn:$COTURN_TAG - restart: unless-stopped domainname: $TURN_DOMAIN volumes: - ./turnserver.conf:/etc/turnserver.conf:ro network_mode: host command: - -c /etc/turnserver.conf - logging: - driver: "json-file" - options: - max-size: "500m" - max-file: "2" volumes: $MGMT_VOLUMENAME: diff --git a/infrastructure_files/getting-started-with-zitadel.sh b/infrastructure_files/getting-started-with-zitadel.sh index 2118ef480..2d7c65cbe 100644 --- a/infrastructure_files/getting-started-with-zitadel.sh +++ b/infrastructure_files/getting-started-with-zitadel.sh @@ -780,7 +780,6 @@ EOF renderDockerCompose() { cat < Date: Wed, 23 Jul 2025 21:03:29 +0200 Subject: [PATCH 162/213] [client] Fix race issues in lazy tests (#4181) * Fix race issues in lazy tests * Fix test failure due to incorrect peer listener identification --- .../lazyconn/activity/manager_test.go | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/client/internal/lazyconn/activity/manager_test.go b/client/internal/lazyconn/activity/manager_test.go index c7c6c878a..ae6c31da4 100644 --- a/client/internal/lazyconn/activity/manager_test.go +++ b/client/internal/lazyconn/activity/manager_test.go @@ -33,6 +33,15 @@ func (m MocWGIface) UpdatePeer(string, []netip.Prefix, time.Duration, *net.UDPAd } +// Add this method to the Manager struct +func (m *Manager) GetPeerListener(peerConnID peerid.ConnID) (*Listener, bool) { + m.mu.Lock() + defer m.mu.Unlock() + + listener, exists := m.peers[peerConnID] + return listener, exists +} + func TestManager_MonitorPeerActivity(t *testing.T) { mocWgInterface := &MocWGIface{} @@ -51,7 +60,12 @@ func TestManager_MonitorPeerActivity(t *testing.T) { t.Fatalf("failed to monitor peer activity: %v", err) } - if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil { + listener, exists := mgr.GetPeerListener(peerCfg1.PeerConnID) + if !exists { + t.Fatalf("peer listener not found") + } + + if err := trigger(listener.conn.LocalAddr().String()); err != nil { t.Fatalf("failed to trigger activity: %v", err) } @@ -128,11 +142,21 @@ func TestManager_MultiPeerActivity(t *testing.T) { t.Fatalf("failed to monitor peer activity: %v", err) } - if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil { + listener, exists := mgr.GetPeerListener(peerCfg1.PeerConnID) + if !exists { + t.Fatalf("peer listener for peer1 not found") + } + + if err := trigger(listener.conn.LocalAddr().String()); err != nil { t.Fatalf("failed to trigger activity: %v", err) } - if err := trigger(mgr.peers[peerCfg2.PeerConnID].conn.LocalAddr().String()); err != nil { + listener, exists = mgr.GetPeerListener(peerCfg2.PeerConnID) + if !exists { + t.Fatalf("peer listener for peer2 not found") + } + + if err := trigger(listener.conn.LocalAddr().String()); err != nil { t.Fatalf("failed to trigger activity: %v", err) } From d311f57559a069fdc0b295d5004539b5c03f73e5 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 24 Jul 2025 13:14:49 +0200 Subject: [PATCH 163/213] [ci] Temporarily disable race detection in Relay (#4210) --- .github/workflows/golang-test-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 0c3862e33..1fa8b406f 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -215,7 +215,7 @@ jobs: - arch: "386" raceFlag: "" - arch: "amd64" - raceFlag: "-race" + raceFlag: "" runs-on: ubuntu-22.04 steps: - name: Install Go From e5e275c87a90680ca9abaaf51596ac1ed0827013 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:34:36 +0200 Subject: [PATCH 164/213] [client] Fix legacy routing exclusion routes in kernel mode (#4167) --- client/iface/bind/ice_bind.go | 2 +- client/iface/bind/udp_mux_generic.go | 15 ++++++++------- client/iface/device/device_kernel_unix.go | 9 ++++++++- util/net/listener_listen.go | 21 +++++++++++---------- util/net/listener_listen_ios.go | 4 ++-- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/client/iface/bind/ice_bind.go b/client/iface/bind/ice_bind.go index c3d5ef377..41f4aec6d 100644 --- a/client/iface/bind/ice_bind.go +++ b/client/iface/bind/ice_bind.go @@ -154,7 +154,7 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r s.udpMux = NewUniversalUDPMuxDefault( UniversalUDPMuxParams{ - UDPConn: nbnet.WrapUDPConn(conn), + UDPConn: nbnet.WrapPacketConn(conn), Net: s.transportNet, FilterFn: s.filterFn, WGAddress: s.address, diff --git a/client/iface/bind/udp_mux_generic.go b/client/iface/bind/udp_mux_generic.go index e42d25462..63f786d2b 100644 --- a/client/iface/bind/udp_mux_generic.go +++ b/client/iface/bind/udp_mux_generic.go @@ -7,15 +7,16 @@ import ( ) func (m *UDPMuxDefault) notifyAddressRemoval(addr string) { - wrapped, ok := m.params.UDPConn.(*UDPConn) - if !ok { + // Kernel mode: direct nbnet.PacketConn (SharedSocket wrapped with nbnet) + if conn, ok := m.params.UDPConn.(*nbnet.PacketConn); ok { + conn.RemoveAddress(addr) return } - nbnetConn, ok := wrapped.GetPacketConn().(*nbnet.UDPConn) - if !ok { - return + // Userspace mode: UDPConn wrapper around nbnet.PacketConn + if wrapped, ok := m.params.UDPConn.(*UDPConn); ok { + if conn, ok := wrapped.GetPacketConn().(*nbnet.PacketConn); ok { + conn.RemoveAddress(addr) + } } - - nbnetConn.RemoveAddress(addr) } diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go index 988ed1b39..7136be0bc 100644 --- a/client/iface/device/device_kernel_unix.go +++ b/client/iface/device/device_kernel_unix.go @@ -16,6 +16,7 @@ import ( "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/sharedsock" + nbnet "github.com/netbirdio/netbird/util/net" ) type TunKernelDevice struct { @@ -99,8 +100,14 @@ func (t *TunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) { if err != nil { return nil, err } + + var udpConn net.PacketConn = rawSock + if !nbnet.AdvancedRouting() { + udpConn = nbnet.WrapPacketConn(rawSock) + } + bindParams := bind.UniversalUDPMuxParams{ - UDPConn: rawSock, + UDPConn: udpConn, Net: t.transportNet, FilterFn: t.filterFn, WGAddress: t.address, diff --git a/util/net/listener_listen.go b/util/net/listener_listen.go index dc99fbd68..4060ab49a 100644 --- a/util/net/listener_listen.go +++ b/util/net/listener_listen.go @@ -120,17 +120,8 @@ func (c *UDPConn) Close() error { return closeConn(c.ID, c.UDPConn) } -// WrapUDPConn wraps an existing *net.UDPConn with nbnet functionality -func WrapUDPConn(conn *net.UDPConn) *UDPConn { - return &UDPConn{ - UDPConn: conn, - ID: GenerateConnID(), - seenAddrs: &sync.Map{}, - } -} - // RemoveAddress removes an address from the seen cache and triggers removal hooks. -func (c *UDPConn) RemoveAddress(addr string) { +func (c *PacketConn) RemoveAddress(addr string) { if _, exists := c.seenAddrs.LoadAndDelete(addr); !exists { return } @@ -159,6 +150,16 @@ func (c *UDPConn) RemoveAddress(addr string) { } } + +// WrapPacketConn wraps an existing net.PacketConn with nbnet functionality +func WrapPacketConn(conn net.PacketConn) *PacketConn { + return &PacketConn{ + PacketConn: conn, + ID: GenerateConnID(), + seenAddrs: &sync.Map{}, + } +} + func callWriteHooks(id ConnectionID, seenAddrs *sync.Map, b []byte, addr net.Addr) { // Lookup the address in the seenAddrs map to avoid calling the hooks for every write if _, loaded := seenAddrs.LoadOrStore(addr.String(), true); !loaded { diff --git a/util/net/listener_listen_ios.go b/util/net/listener_listen_ios.go index 3cbd2cd71..c52aea583 100644 --- a/util/net/listener_listen_ios.go +++ b/util/net/listener_listen_ios.go @@ -4,7 +4,7 @@ import ( "net" ) -// WrapUDPConn on iOS just returns the original connection since iOS handles its own networking -func WrapUDPConn(conn *net.UDPConn) *net.UDPConn { +// WrapPacketConn on iOS just returns the original connection since iOS handles its own networking +func WrapPacketConn(conn *net.UDPConn) *net.UDPConn { return conn } From 459c9ef3173ab7fd7cc46ade1a582e08de20e96d Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:34:55 +0200 Subject: [PATCH 165/213] [client] Add env and status flags for netbird service command (#3975) --- .github/workflows/golang-test-linux.yml | 10 +- client/cmd/root.go | 16 +- client/cmd/service.go | 80 +++++-- client/cmd/service_controller.go | 162 +++++++-------- client/cmd/service_installer.go | 248 ++++++++++++++++------ client/cmd/service_test.go | 263 ++++++++++++++++++++++++ 6 files changed, 601 insertions(+), 178 deletions(-) create mode 100644 client/cmd/service_test.go diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 1fa8b406f..0d7233c3e 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-22.04 outputs: management: ${{ steps.filter.outputs.management }} - steps: + steps: - name: Checkout code uses: actions/checkout@v4 @@ -24,8 +24,8 @@ jobs: id: filter with: filters: | - management: - - 'management/**' + management: + - 'management/**' - name: Install Go uses: actions/setup-go@v5 @@ -148,7 +148,7 @@ jobs: test_client_on_docker: name: "Client (Docker) / Unit" - needs: [build-cache] + needs: [ build-cache ] runs-on: ubuntu-22.04 steps: - name: Install Go @@ -181,6 +181,7 @@ jobs: env: HOST_GOCACHE: ${{ steps.go-env.outputs.cache_dir }} HOST_GOMODCACHE: ${{ steps.go-env.outputs.modcache_dir }} + CONTAINER: "true" run: | CONTAINER_GOCACHE="/root/.cache/go-build" CONTAINER_GOMODCACHE="/go/pkg/mod" @@ -198,6 +199,7 @@ jobs: -e GOARCH=${GOARCH_TARGET} \ -e GOCACHE=${CONTAINER_GOCACHE} \ -e GOMODCACHE=${CONTAINER_GOMODCACHE} \ + -e CONTAINER=${CONTAINER} \ golang:1.23-alpine \ sh -c ' \ apk update; apk add --no-cache \ diff --git a/client/cmd/root.go b/client/cmd/root.go index fa4bd4d42..bfd0d06c5 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -67,7 +67,6 @@ var ( interfaceName string wireguardPort uint16 networkMonitor bool - serviceName string autoConnectDisabled bool extraIFaceBlackList []string anonymizeFlag bool @@ -116,15 +115,9 @@ func init() { defaultDaemonAddr = "tcp://127.0.0.1:41731" } - defaultServiceName := "netbird" - if runtime.GOOS == "windows" { - defaultServiceName = "Netbird" - } - rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL)) rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) - rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.") @@ -135,7 +128,6 @@ func init() { rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device") rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output") - rootCmd.AddCommand(serviceCmd) rootCmd.AddCommand(upCmd) rootCmd.AddCommand(downCmd) rootCmd.AddCommand(statusCmd) @@ -146,9 +138,6 @@ func init() { rootCmd.AddCommand(forwardingRulesCmd) rootCmd.AddCommand(debugCmd) - serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service - serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service - networksCMD.AddCommand(routesListCmd) networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd) @@ -186,14 +175,13 @@ func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) { termCh := make(chan os.Signal, 1) signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) go func() { - done := ctx.Done() + defer cancel() select { - case <-done: + case <-ctx.Done(): case <-termCh: } log.Info("shutdown signal received") - cancel() }() } diff --git a/client/cmd/service.go b/client/cmd/service.go index 156e67d6d..178f4bf0e 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -1,12 +1,15 @@ +//go:build !ios && !android + package cmd import ( "context" + "fmt" "runtime" + "strings" "sync" "github.com/kardianos/service" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "google.golang.org/grpc" @@ -14,6 +17,16 @@ import ( "github.com/netbirdio/netbird/client/server" ) +var serviceCmd = &cobra.Command{ + Use: "service", + Short: "manages Netbird service", +} + +var ( + serviceName string + serviceEnvVars []string +) + type program struct { ctx context.Context cancel context.CancelFunc @@ -22,12 +35,31 @@ type program struct { serverInstanceMu sync.Mutex } +func init() { + defaultServiceName := "netbird" + if runtime.GOOS == "windows" { + defaultServiceName = "Netbird" + } + + serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd) + + rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") + serviceEnvDesc := `Sets extra environment variables for the service. ` + + `You can specify a comma-separated list of KEY=VALUE pairs. ` + + `E.g. --service-env LOG_LEVEL=debug,CUSTOM_VAR=value` + + installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc) + reconfigureCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc) + + rootCmd.AddCommand(serviceCmd) +} + func newProgram(ctx context.Context, cancel context.CancelFunc) *program { ctx = internal.CtxInitState(ctx) return &program{ctx: ctx, cancel: cancel} } -func newSVCConfig() *service.Config { +func newSVCConfig() (*service.Config, error) { config := &service.Config{ Name: serviceName, DisplayName: "Netbird", @@ -36,23 +68,47 @@ func newSVCConfig() *service.Config { EnvVars: make(map[string]string), } + if len(serviceEnvVars) > 0 { + extraEnvs, err := parseServiceEnvVars(serviceEnvVars) + if err != nil { + return nil, fmt.Errorf("parse service environment variables: %w", err) + } + config.EnvVars = extraEnvs + } + if runtime.GOOS == "linux" { config.EnvVars["SYSTEMD_UNIT"] = serviceName } - return config + return config, nil } func newSVC(prg *program, conf *service.Config) (service.Service, error) { - s, err := service.New(prg, conf) - if err != nil { - log.Fatal(err) - return nil, err - } - return s, nil + return service.New(prg, conf) } -var serviceCmd = &cobra.Command{ - Use: "service", - Short: "manages Netbird service", +func parseServiceEnvVars(envVars []string) (map[string]string, error) { + envMap := make(map[string]string) + + for _, env := range envVars { + if env == "" { + continue + } + + parts := strings.SplitN(env, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid environment variable format: %s (expected KEY=VALUE)", env) + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + if key == "" { + return nil, fmt.Errorf("empty environment variable key in: %s", env) + } + + envMap[key] = value + } + + return envMap, nil } diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 5e3c63e57..2545623ec 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -1,3 +1,5 @@ +//go:build !ios && !android + package cmd import ( @@ -47,14 +49,13 @@ func (p *program) Start(svc service.Service) error { listen, err := net.Listen(split[0], split[1]) if err != nil { - return fmt.Errorf("failed to listen daemon interface: %w", err) + return fmt.Errorf("listen daemon interface: %w", err) } go func() { defer listen.Close() if split[0] == "unix" { - err = os.Chmod(split[1], 0666) - if err != nil { + if err := os.Chmod(split[1], 0666); err != nil { log.Errorf("failed setting daemon permissions: %v", split[1]) return } @@ -100,37 +101,49 @@ func (p *program) Stop(srv service.Service) error { return nil } +// Common setup for service control commands +func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc) (service.Service, error) { + SetFlagsFromEnvVars(rootCmd) + SetFlagsFromEnvVars(serviceCmd) + + cmd.SetOut(cmd.OutOrStdout()) + + if err := handleRebrand(cmd); err != nil { + return nil, err + } + + if err := util.InitLog(logLevel, logFile); err != nil { + return nil, fmt.Errorf("init log: %w", err) + } + + cfg, err := newSVCConfig() + if err != nil { + return nil, fmt.Errorf("create service config: %w", err) + } + + s, err := newSVC(newProgram(ctx, cancel), cfg) + if err != nil { + return nil, err + } + + return s, nil +} + var runCmd = &cobra.Command{ Use: "run", Short: "runs Netbird as service", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := handleRebrand(cmd) - if err != nil { - return err - } - - err = util.InitLog(logLevel, logFile) - if err != nil { - return fmt.Errorf("failed initializing log %v", err) - } - ctx, cancel := context.WithCancel(cmd.Context()) + SetupCloseHandler(ctx, cancel) SetupDebugHandler(ctx, nil, nil, nil, logFile) - s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) + s, err := setupServiceControlCommand(cmd, ctx, cancel) if err != nil { return err } - err = s.Run() - if err != nil { - return err - } - return nil + + return s.Run() }, } @@ -138,31 +151,14 @@ var startCmd = &cobra.Command{ Use: "start", Short: "starts Netbird service", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := handleRebrand(cmd) - if err != nil { - return err - } - - err = util.InitLog(logLevel, logFile) - if err != nil { - return err - } - ctx, cancel := context.WithCancel(cmd.Context()) - - s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) + s, err := setupServiceControlCommand(cmd, ctx, cancel) if err != nil { - cmd.PrintErrln(err) return err } - err = s.Start() - if err != nil { - cmd.PrintErrln(err) - return err + + if err := s.Start(); err != nil { + return fmt.Errorf("start service: %w", err) } cmd.Println("Netbird service has been started") return nil @@ -173,29 +169,14 @@ var stopCmd = &cobra.Command{ Use: "stop", Short: "stops Netbird service", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := handleRebrand(cmd) - if err != nil { - return err - } - - err = util.InitLog(logLevel, logFile) - if err != nil { - return fmt.Errorf("failed initializing log %v", err) - } - ctx, cancel := context.WithCancel(cmd.Context()) - - s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) + s, err := setupServiceControlCommand(cmd, ctx, cancel) if err != nil { return err } - err = s.Stop() - if err != nil { - return err + + if err := s.Stop(); err != nil { + return fmt.Errorf("stop service: %w", err) } cmd.Println("Netbird service has been stopped") return nil @@ -206,31 +187,48 @@ var restartCmd = &cobra.Command{ Use: "restart", Short: "restarts Netbird service", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := handleRebrand(cmd) - if err != nil { - return err - } - - err = util.InitLog(logLevel, logFile) - if err != nil { - return fmt.Errorf("failed initializing log %v", err) - } - ctx, cancel := context.WithCancel(cmd.Context()) - - s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) + s, err := setupServiceControlCommand(cmd, ctx, cancel) if err != nil { return err } - err = s.Restart() - if err != nil { - return err + + if err := s.Restart(); err != nil { + return fmt.Errorf("restart service: %w", err) } cmd.Println("Netbird service has been restarted") return nil }, } + +var svcStatusCmd = &cobra.Command{ + Use: "status", + Short: "shows Netbird service status", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithCancel(cmd.Context()) + s, err := setupServiceControlCommand(cmd, ctx, cancel) + if err != nil { + return err + } + + status, err := s.Status() + if err != nil { + return fmt.Errorf("get service status: %w", err) + } + + var statusText string + switch status { + case service.StatusRunning: + statusText = "Running" + case service.StatusStopped: + statusText = "Stopped" + case service.StatusUnknown: + statusText = "Unknown" + default: + statusText = fmt.Sprintf("Unknown (%d)", status) + } + + cmd.Printf("Netbird service status: %s\n", statusText) + return nil + }, +} diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index c1d6308c6..951efcc73 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -1,87 +1,121 @@ +//go:build !ios && !android + package cmd import ( "context" + "errors" + "fmt" "os" "path/filepath" "runtime" + "github.com/kardianos/service" "github.com/spf13/cobra" ) +var ErrGetServiceStatus = fmt.Errorf("failed to get service status") + +// Common service command setup +func setupServiceCommand(cmd *cobra.Command) error { + SetFlagsFromEnvVars(rootCmd) + SetFlagsFromEnvVars(serviceCmd) + cmd.SetOut(cmd.OutOrStdout()) + return handleRebrand(cmd) +} + +// Build service arguments for install/reconfigure +func buildServiceArguments() []string { + args := []string{ + "service", + "run", + "--config", + configPath, + "--log-level", + logLevel, + "--daemon-addr", + daemonAddr, + } + + if managementURL != "" { + args = append(args, "--management-url", managementURL) + } + + if logFile != "" { + args = append(args, "--log-file", logFile) + } + + return args +} + +// Configure platform-specific service settings +func configurePlatformSpecificSettings(svcConfig *service.Config) error { + if runtime.GOOS == "linux" { + // Respected only by systemd systems + svcConfig.Dependencies = []string{"After=network.target syslog.target"} + + if logFile != "console" { + setStdLogPath := true + dir := filepath.Dir(logFile) + + if _, err := os.Stat(dir); err != nil { + if err = os.MkdirAll(dir, 0750); err != nil { + setStdLogPath = false + } + } + + if setStdLogPath { + svcConfig.Option["LogOutput"] = true + svcConfig.Option["LogDirectory"] = dir + } + } + } + + if runtime.GOOS == "windows" { + svcConfig.Option["OnFailure"] = "restart" + } + + return nil +} + +// Create fully configured service config for install/reconfigure +func createServiceConfigForInstall() (*service.Config, error) { + svcConfig, err := newSVCConfig() + if err != nil { + return nil, fmt.Errorf("create service config: %w", err) + } + + svcConfig.Arguments = buildServiceArguments() + if err = configurePlatformSpecificSettings(svcConfig); err != nil { + return nil, fmt.Errorf("configure platform-specific settings: %w", err) + } + + return svcConfig, nil +} + var installCmd = &cobra.Command{ Use: "install", Short: "installs Netbird service", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := handleRebrand(cmd) - if err != nil { + if err := setupServiceCommand(cmd); err != nil { return err } - svcConfig := newSVCConfig() - - svcConfig.Arguments = []string{ - "service", - "run", - "--config", - configPath, - "--log-level", - logLevel, - "--daemon-addr", - daemonAddr, - } - - if managementURL != "" { - svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL) - } - - if logFile != "" { - svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile) - } - - if runtime.GOOS == "linux" { - // Respected only by systemd systems - svcConfig.Dependencies = []string{"After=network.target syslog.target"} - - if logFile != "console" { - setStdLogPath := true - dir := filepath.Dir(logFile) - - _, err := os.Stat(dir) - if err != nil { - err = os.MkdirAll(dir, 0750) - if err != nil { - setStdLogPath = false - } - } - - if setStdLogPath { - svcConfig.Option["LogOutput"] = true - svcConfig.Option["LogDirectory"] = dir - } - } - } - - if runtime.GOOS == "windows" { - svcConfig.Option["OnFailure"] = "restart" + svcConfig, err := createServiceConfigForInstall() + if err != nil { + return err } ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() s, err := newSVC(newProgram(ctx, cancel), svcConfig) if err != nil { - cmd.PrintErrln(err) return err } - err = s.Install() - if err != nil { - cmd.PrintErrln(err) - return err + if err := s.Install(); err != nil { + return fmt.Errorf("install service: %w", err) } cmd.Println("Netbird service has been installed") @@ -93,27 +127,109 @@ var uninstallCmd = &cobra.Command{ Use: "uninstall", Short: "uninstalls Netbird service from system", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) + if err := setupServiceCommand(cmd); err != nil { + return err + } - cmd.SetOut(cmd.OutOrStdout()) + cfg, err := newSVCConfig() + if err != nil { + return fmt.Errorf("create service config: %w", err) + } - err := handleRebrand(cmd) + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + s, err := newSVC(newProgram(ctx, cancel), cfg) + if err != nil { + return err + } + + if err := s.Uninstall(); err != nil { + return fmt.Errorf("uninstall service: %w", err) + } + + cmd.Println("Netbird service has been uninstalled") + return nil + }, +} + +var reconfigureCmd = &cobra.Command{ + Use: "reconfigure", + Short: "reconfigures Netbird service with new settings", + Long: `Reconfigures the Netbird service with new settings without manual uninstall/install. +This command will temporarily stop the service, update its configuration, and restart it if it was running.`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := setupServiceCommand(cmd); err != nil { + return err + } + + wasRunning, err := isServiceRunning() + if err != nil && !errors.Is(err, ErrGetServiceStatus) { + return fmt.Errorf("check service status: %w", err) + } + + svcConfig, err := createServiceConfigForInstall() if err != nil { return err } ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() - s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) + s, err := newSVC(newProgram(ctx, cancel), svcConfig) if err != nil { - return err + return fmt.Errorf("create service: %w", err) } - err = s.Uninstall() - if err != nil { - return err + if wasRunning { + cmd.Println("Stopping Netbird service...") + if err := s.Stop(); err != nil { + cmd.Printf("Warning: failed to stop service: %v\n", err) + } } - cmd.Println("Netbird service has been uninstalled") + + cmd.Println("Removing existing service configuration...") + if err := s.Uninstall(); err != nil { + return fmt.Errorf("uninstall existing service: %w", err) + } + + cmd.Println("Installing service with new configuration...") + if err := s.Install(); err != nil { + return fmt.Errorf("install service with new config: %w", err) + } + + if wasRunning { + cmd.Println("Starting Netbird service...") + if err := s.Start(); err != nil { + return fmt.Errorf("start service after reconfigure: %w", err) + } + cmd.Println("Netbird service has been reconfigured and started") + } else { + cmd.Println("Netbird service has been reconfigured") + } + return nil }, } + +func isServiceRunning() (bool, error) { + cfg, err := newSVCConfig() + if err != nil { + return false, err + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + s, err := newSVC(newProgram(ctx, cancel), cfg) + if err != nil { + return false, err + } + + status, err := s.Status() + if err != nil { + return false, fmt.Errorf("%w: %w", ErrGetServiceStatus, err) + } + + return status == service.StatusRunning, nil +} diff --git a/client/cmd/service_test.go b/client/cmd/service_test.go new file mode 100644 index 000000000..6d75ca524 --- /dev/null +++ b/client/cmd/service_test.go @@ -0,0 +1,263 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "runtime" + "testing" + "time" + + "github.com/kardianos/service" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + serviceStartTimeout = 10 * time.Second + serviceStopTimeout = 5 * time.Second + statusPollInterval = 500 * time.Millisecond +) + +// waitForServiceStatus waits for service to reach expected status with timeout +func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) { + cfg, err := newSVCConfig() + if err != nil { + return false, err + } + + ctxSvc, cancel := context.WithCancel(context.Background()) + defer cancel() + + s, err := newSVC(newProgram(ctxSvc, cancel), cfg) + if err != nil { + return false, err + } + + ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout) + defer timeoutCancel() + + ticker := time.NewTicker(statusPollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus) + case <-ticker.C: + status, err := s.Status() + if err != nil { + // Continue polling on transient errors + continue + } + if status == expectedStatus { + return true, nil + } + } + } +} + +// TestServiceLifecycle tests the complete service lifecycle +func TestServiceLifecycle(t *testing.T) { + // TODO: Add support for Windows and macOS + if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" { + t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS) + } + + if os.Getenv("CONTAINER") == "true" { + t.Skip("Skipping service lifecycle test in container environment") + } + + originalServiceName := serviceName + serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix()) + defer func() { + serviceName = originalServiceName + }() + + tempDir := t.TempDir() + configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir) + logLevel = "info" + daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir) + + ctx := context.Background() + + t.Run("Install", func(t *testing.T) { + installCmd.SetContext(ctx) + err := installCmd.RunE(installCmd, []string{}) + require.NoError(t, err) + + cfg, err := newSVCConfig() + require.NoError(t, err) + + ctxSvc, cancel := context.WithCancel(context.Background()) + defer cancel() + + s, err := newSVC(newProgram(ctxSvc, cancel), cfg) + require.NoError(t, err) + + status, err := s.Status() + assert.NoError(t, err) + assert.NotEqual(t, service.StatusUnknown, status) + }) + + t.Run("Start", func(t *testing.T) { + startCmd.SetContext(ctx) + err := startCmd.RunE(startCmd, []string{}) + require.NoError(t, err) + + running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout) + require.NoError(t, err) + assert.True(t, running) + }) + + t.Run("Restart", func(t *testing.T) { + restartCmd.SetContext(ctx) + err := restartCmd.RunE(restartCmd, []string{}) + require.NoError(t, err) + + running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout) + require.NoError(t, err) + assert.True(t, running) + }) + + t.Run("Reconfigure", func(t *testing.T) { + originalLogLevel := logLevel + logLevel = "debug" + defer func() { + logLevel = originalLogLevel + }() + + reconfigureCmd.SetContext(ctx) + err := reconfigureCmd.RunE(reconfigureCmd, []string{}) + require.NoError(t, err) + + running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout) + require.NoError(t, err) + assert.True(t, running) + }) + + t.Run("Stop", func(t *testing.T) { + stopCmd.SetContext(ctx) + err := stopCmd.RunE(stopCmd, []string{}) + require.NoError(t, err) + + stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout) + require.NoError(t, err) + assert.True(t, stopped) + }) + + t.Run("Uninstall", func(t *testing.T) { + uninstallCmd.SetContext(ctx) + err := uninstallCmd.RunE(uninstallCmd, []string{}) + require.NoError(t, err) + + cfg, err := newSVCConfig() + require.NoError(t, err) + + ctxSvc, cancel := context.WithCancel(context.Background()) + defer cancel() + + s, err := newSVC(newProgram(ctxSvc, cancel), cfg) + require.NoError(t, err) + + _, err = s.Status() + assert.Error(t, err) + }) +} + +// TestServiceEnvVars tests environment variable parsing +func TestServiceEnvVars(t *testing.T) { + tests := []struct { + name string + envVars []string + expected map[string]string + expectErr bool + }{ + { + name: "Valid single env var", + envVars: []string{"LOG_LEVEL=debug"}, + expected: map[string]string{ + "LOG_LEVEL": "debug", + }, + }, + { + name: "Valid multiple env vars", + envVars: []string{"LOG_LEVEL=debug", "CUSTOM_VAR=value"}, + expected: map[string]string{ + "LOG_LEVEL": "debug", + "CUSTOM_VAR": "value", + }, + }, + { + name: "Env var with spaces", + envVars: []string{" KEY = value "}, + expected: map[string]string{ + "KEY": "value", + }, + }, + { + name: "Invalid format - no equals", + envVars: []string{"INVALID"}, + expectErr: true, + }, + { + name: "Invalid format - empty key", + envVars: []string{"=value"}, + expectErr: true, + }, + { + name: "Empty value is valid", + envVars: []string{"KEY="}, + expected: map[string]string{ + "KEY": "", + }, + }, + { + name: "Empty slice", + envVars: []string{}, + expected: map[string]string{}, + }, + { + name: "Empty string in slice", + envVars: []string{"", "KEY=value", ""}, + expected: map[string]string{"KEY": "value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseServiceEnvVars(tt.envVars) + + if tt.expectErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +// TestServiceConfigWithEnvVars tests service config creation with env vars +func TestServiceConfigWithEnvVars(t *testing.T) { + originalServiceName := serviceName + originalServiceEnvVars := serviceEnvVars + defer func() { + serviceName = originalServiceName + serviceEnvVars = originalServiceEnvVars + }() + + serviceName = "test-service" + serviceEnvVars = []string{"TEST_VAR=test_value", "ANOTHER_VAR=another_value"} + + cfg, err := newSVCConfig() + require.NoError(t, err) + + assert.Equal(t, "test-service", cfg.Name) + assert.Equal(t, "test_value", cfg.EnvVars["TEST_VAR"]) + assert.Equal(t, "another_value", cfg.EnvVars["ANOTHER_VAR"]) + + if runtime.GOOS == "linux" { + assert.Equal(t, "test-service", cfg.EnvVars["SYSTEMD_UNIT"]) + } +} From 0ea5d020a34b13a80ad0c3c2a96b556b39ffc1ae Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:12:29 +0100 Subject: [PATCH 166/213] [management] extra settings integrated validator (#4136) --- go.mod | 2 +- go.sum | 4 +- management/server/account/manager.go | 2 +- management/server/integrated_validator.go | 39 ++++++++++++------- .../integrated_validator/interface.go | 4 +- management/server/mock_server/account_mock.go | 10 ++--- management/server/peer.go | 14 +++---- management/server/types/settings.go | 7 ++++ 8 files changed, 51 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index cf2a23758..8120efe54 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5 + github.com/netbirdio/management-integrations/integrations v0.0.0-20250718161635-83fb99b09b5a github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index 699a832dd..c9938908e 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5 h1:Zfn8d83OVyELCdxgprcyXR3D8uqoxHtXE9PUxVXDx/w= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250718161635-83fb99b09b5a h1:Kmq74+axAiJrD98+uAr53sIuj/zwMrak05Ofoy4SWYU= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250718161635-83fb99b09b5a/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/server/account/manager.go b/management/server/account/manager.go index f8aa2756a..8c7e95e3d 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -101,7 +101,7 @@ type Manager interface { DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error ListPostureChecks(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) GetIdpManager() idp.Manager - UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error + UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error) GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index e3e474411..b89739be9 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -3,6 +3,7 @@ package server import ( "context" "errors" + "fmt" log "github.com/sirupsen/logrus" @@ -12,34 +13,44 @@ import ( "github.com/netbirdio/netbird/management/server/types" ) -// UpdateIntegratedValidatorGroups updates the integrated validator groups for a specified account. +// UpdateIntegratedValidator updates the integrated validator groups for a specified account. // It retrieves the account associated with the provided userID, then updates the integrated validator groups // with the provided list of group ids. The updated account is then saved. // // Parameters: // - accountID: The ID of the account for which integrated validator groups are to be updated. // - userID: The ID of the user whose account is being updated. +// - validator: The validator type to use, or empty to remove. // - groups: A slice of strings representing the ids of integrated validator groups to be updated. // // Returns: // - error: An error if any occurred during the process, otherwise returns nil -func (am *DefaultAccountManager) UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error { - ok, err := am.GroupValidation(ctx, accountID, groups) - if err != nil { - log.WithContext(ctx).Debugf("error validating groups: %s", err.Error()) - return err +func (am *DefaultAccountManager) UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error { + if validator != "" && len(groups) == 0 { + return fmt.Errorf("at least one group must be specified for validator") } - if !ok { - log.WithContext(ctx).Debugf("invalid groups") - return errors.New("invalid groups") + if validator != "" { + ok, err := am.GroupValidation(ctx, accountID, groups) + if err != nil { + log.WithContext(ctx).Debugf("error validating groups: %s", err.Error()) + return err + } + + if !ok { + log.WithContext(ctx).Debugf("invalid groups") + return errors.New("invalid groups") + } + } else { + // ensure groups is empty + groups = []string{} } unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() return am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - a, err := transaction.GetAccountByUser(ctx, userID) + a, err := transaction.GetAccount(ctx, accountID) if err != nil { return err } @@ -52,6 +63,8 @@ func (am *DefaultAccountManager) UpdateIntegratedValidatorGroups(ctx context.Con extra = &types.ExtraSettings{} a.Settings.Extra = extra } + + extra.IntegratedValidator = validator extra.IntegratedValidatorGroups = groups return transaction.SaveAccount(ctx, a) }) @@ -99,7 +112,7 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI return nil, err } - return am.integratedPeerValidator.GetValidatedPeers(accountID, groups, peers, settings.Extra) + return am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, groups, peers, settings.Extra) } type MockIntegratedValidator struct { @@ -118,7 +131,7 @@ func (a MockIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer. return update, false, nil } -func (a MockIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) { +func (a MockIntegratedValidator) GetValidatedPeers(_ context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) { validatedPeers := make(map[string]struct{}) for _, peer := range peers { validatedPeers[peer.ID] = struct{}{} @@ -134,7 +147,7 @@ func (MockIntegratedValidator) IsNotValidPeer(_ context.Context, accountID strin return false, false, nil } -func (MockIntegratedValidator) PeerDeleted(_ context.Context, _, _ string) error { +func (MockIntegratedValidator) PeerDeleted(_ context.Context, _, _ string, extraSettings *types.ExtraSettings) error { return nil } diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go index 245c0168f..4d4a8cdf6 100644 --- a/management/server/integrations/integrated_validator/interface.go +++ b/management/server/integrations/integrated_validator/interface.go @@ -14,8 +14,8 @@ type IntegratedValidator interface { ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) *nbpeer.Peer IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) - GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) - PeerDeleted(ctx context.Context, accountID, peerID string) error + GetValidatedPeers(ctx context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) + PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error SetPeerInvalidationListener(fn func(accountID string)) Stop(ctx context.Context) ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index b1ec66286..a16e3652c 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -102,7 +102,7 @@ type MockAccountManager struct { DeletePostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) error ListPostureChecksFunc func(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) GetIdpManagerFunc func() idp.Manager - UpdateIntegratedValidatorGroupsFunc func(ctx context.Context, accountID string, userID string, groups []string) error + UpdateIntegratedValidatorFunc func(ctx context.Context, accountID, userID, validator string, groups []string) error GroupValidationFunc func(ctx context.Context, accountId string, groups []string) (bool, error) SyncPeerMetaFunc func(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error FindExistingPostureCheckFunc func(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) @@ -769,10 +769,10 @@ func (am *MockAccountManager) GetIdpManager() idp.Manager { return nil } -// UpdateIntegratedValidatorGroups mocks UpdateIntegratedApprovalGroups of the AccountManager interface -func (am *MockAccountManager) UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error { - if am.UpdateIntegratedValidatorGroupsFunc != nil { - return am.UpdateIntegratedValidatorGroupsFunc(ctx, accountID, userID, groups) +// UpdateIntegratedValidator mocks UpdateIntegratedApprovalGroups of the AccountManager interface +func (am *MockAccountManager) UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error { + if am.UpdateIntegratedValidatorFunc != nil { + return am.UpdateIntegratedValidatorFunc(ctx, accountID, userID, validator, groups) } return status.Errorf(codes.Unimplemented, "method UpdateIntegratedValidatorGroups is not implemented") } diff --git a/management/server/peer.go b/management/server/peer.go index c6ade83c0..8f3eb2331 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -87,7 +87,7 @@ func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, acc return nil, err } - approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { return nil, err } @@ -412,7 +412,7 @@ func (am *DefaultAccountManager) GetNetworkMap(ctx context.Context, peerID strin groups[groupID] = group.Peers } - validatedPeers, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + validatedPeers, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { return nil, err } @@ -1036,7 +1036,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is return nil, nil, nil, err } - approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { return nil, nil, nil, err } @@ -1156,7 +1156,7 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun return nil, err } - approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { return nil, err } @@ -1204,7 +1204,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account return } - approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { log.WithContext(ctx).Errorf("failed to send out updates to peers, failed to get validate peers: %v", err) return @@ -1337,7 +1337,7 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI return } - approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { log.WithContext(ctx).Errorf("failed to send update to peer %s, failed to validate peers: %v", peerId, err) return @@ -1571,7 +1571,7 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto } } - if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil { + if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID, settings.Extra); err != nil { return nil, err } diff --git a/management/server/types/settings.go b/management/server/types/settings.go index a22a36b03..37c728bf8 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -77,6 +77,8 @@ type ExtraSettings struct { // PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator PeerApprovalEnabled bool + // IntegratedValidator is the string enum for the integrated validator type + IntegratedValidator string // IntegratedValidatorGroups list of group IDs to be used with integrated approval configurations IntegratedValidatorGroups []string `gorm:"serializer:json"` @@ -93,5 +95,10 @@ func (e *ExtraSettings) Copy() *ExtraSettings { return &ExtraSettings{ PeerApprovalEnabled: e.PeerApprovalEnabled, IntegratedValidatorGroups: append(cpGroup, e.IntegratedValidatorGroups...), + IntegratedValidator: e.IntegratedValidator, + FlowEnabled: e.FlowEnabled, + FlowPacketCounterEnabled: e.FlowPacketCounterEnabled, + FlowENCollectionEnabled: e.FlowENCollectionEnabled, + FlowDnsCollectionEnabled: e.FlowDnsCollectionEnabled, } } From 1a9ea32c2138f363f2d67af9c5d2afcbc74085f1 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:25:21 +0100 Subject: [PATCH 167/213] [management] scheduler cancel all jobs (#4158) --- go.mod | 2 +- go.sum | 4 ++-- management/server/scheduler.go | 29 +++++++++++++++++++++++--- management/server/scheduler_test.go | 32 +++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8120efe54..4d9191d04 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250718161635-83fb99b09b5a + github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index c9938908e..84dfe2403 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250718161635-83fb99b09b5a h1:Kmq74+axAiJrD98+uAr53sIuj/zwMrak05Ofoy4SWYU= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250718161635-83fb99b09b5a/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c h1:OtX903X0FKEE+fcsp/P2701md7X/xbi/W/ojWIJNKSk= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/server/scheduler.go b/management/server/scheduler.go index df73c9a1d..b61643295 100644 --- a/management/server/scheduler.go +++ b/management/server/scheduler.go @@ -11,14 +11,17 @@ import ( // Scheduler is an interface which implementations can schedule and cancel jobs type Scheduler interface { Cancel(ctx context.Context, IDs []string) + CancelAll(ctx context.Context) Schedule(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) IsSchedulerRunning(ID string) bool } // MockScheduler is a mock implementation of Scheduler type MockScheduler struct { - CancelFunc func(ctx context.Context, IDs []string) - ScheduleFunc func(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) + CancelFunc func(ctx context.Context, IDs []string) + CancelAllFunc func(ctx context.Context) + ScheduleFunc func(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) + IsSchedulerRunningFunc func(ID string) bool } // Cancel mocks the Cancel function of the Scheduler interface @@ -30,6 +33,15 @@ func (mock *MockScheduler) Cancel(ctx context.Context, IDs []string) { log.WithContext(ctx).Warnf("MockScheduler doesn't have Cancel function defined ") } +// CancelAll mocks the CancelAll function of the Scheduler interface +func (mock *MockScheduler) CancelAll(ctx context.Context) { + if mock.CancelAllFunc != nil { + mock.CancelAllFunc(ctx) + return + } + log.WithContext(ctx).Warnf("MockScheduler doesn't have CancelAll function defined ") +} + // Schedule mocks the Schedule function of the Scheduler interface func (mock *MockScheduler) Schedule(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) { if mock.ScheduleFunc != nil { @@ -40,7 +52,9 @@ func (mock *MockScheduler) Schedule(ctx context.Context, in time.Duration, ID st } func (mock *MockScheduler) IsSchedulerRunning(ID string) bool { - // MockScheduler does not implement IsSchedulerRunning, so we return false + if mock.IsSchedulerRunningFunc != nil { + return mock.IsSchedulerRunningFunc(ID) + } log.Warnf("MockScheduler doesn't have IsSchedulerRunning function defined") return false } @@ -52,6 +66,15 @@ type DefaultScheduler struct { mu *sync.Mutex } +func (wm *DefaultScheduler) CancelAll(ctx context.Context) { + wm.mu.Lock() + defer wm.mu.Unlock() + + for id := range wm.jobs { + wm.cancel(ctx, id) + } +} + // NewDefaultScheduler creates an instance of a DefaultScheduler func NewDefaultScheduler() *DefaultScheduler { return &DefaultScheduler{ diff --git a/management/server/scheduler_test.go b/management/server/scheduler_test.go index fa279d4db..e3af551ad 100644 --- a/management/server/scheduler_test.go +++ b/management/server/scheduler_test.go @@ -75,6 +75,38 @@ func TestScheduler_Cancel(t *testing.T) { assert.NotNil(t, scheduler.jobs[jobID2]) } +func TestScheduler_CancelAll(t *testing.T) { + jobID1 := "test-scheduler-job-1" + jobID2 := "test-scheduler-job-2" + scheduler := NewDefaultScheduler() + tChan := make(chan struct{}) + p := []string{jobID1, jobID2} + scheduletime := 2 * time.Millisecond + sleepTime := 4 * time.Millisecond + if runtime.GOOS == "windows" { + // sleep and ticker are slower on windows see https://github.com/golang/go/issues/44343 + sleepTime = 20 * time.Millisecond + } + + scheduler.Schedule(context.Background(), scheduletime, jobID1, func() (nextRunIn time.Duration, reschedule bool) { + tt := p[0] + <-tChan + t.Logf("job %s", tt) + return scheduletime, true + }) + scheduler.Schedule(context.Background(), scheduletime, jobID2, func() (nextRunIn time.Duration, reschedule bool) { + return scheduletime, true + }) + + time.Sleep(sleepTime) + assert.Len(t, scheduler.jobs, 2) + scheduler.CancelAll(context.Background()) + close(tChan) + p = []string{} + time.Sleep(sleepTime) + assert.Len(t, scheduler.jobs, 0) +} + func TestScheduler_Schedule(t *testing.T) { jobID := "test-scheduler-job-1" scheduler := NewDefaultScheduler() From 04fae00a6c1cdfa47d937172d369eaa6e0963157 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:44:48 +0200 Subject: [PATCH 168/213] [management] Log UpdateAccountPeers caller (#4216) --- management/server/peer.go | 3 +++ util/runtime.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 util/runtime.go diff --git a/management/server/peer.go b/management/server/peer.go index 8f3eb2331..b82a9cb80 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -23,6 +23,7 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" + "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" @@ -1183,6 +1184,8 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun // UpdateAccountPeers updates all peers that belong to an account. // Should be called when changes have to be synced to peers. func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { + log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName()) + account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) if err != nil { log.WithContext(ctx).Errorf("failed to send out updates to peers. failed to get account: %v", err) diff --git a/util/runtime.go b/util/runtime.go new file mode 100644 index 000000000..3b420e15b --- /dev/null +++ b/util/runtime.go @@ -0,0 +1,15 @@ +package util + +import "runtime" + +func GetCallerName() string { + pc, _, _, ok := runtime.Caller(2) + if !ok { + return "unknown" + } + fn := runtime.FuncForPC(pc) + if fn == nil { + return "unknown" + } + return fn.Name() +} From 643730f770fd82927d28496759606872ed937065 Mon Sep 17 00:00:00 2001 From: Ali Amer <76897266+aliamerj@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:51:27 +0300 Subject: [PATCH 169/213] [client] Correct minor issues in --filter-by-connection-type flag implementation for status command (#4214) Signed-off-by: aliamerj --- client/proto/daemon.pb.go | 7 ------- client/status/status.go | 14 +++++++++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 753aa62d1..26e58d183 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1330,13 +1330,6 @@ func (x *PeerState) GetRelayAddress() string { return "" } -func (x *PeerState) GetConnectionType() string { - if x.Relayed { - return "Relayed" - } - return "P2P" -} - // LocalPeerState contains the latest state of the local peer type LocalPeerState struct { state protoimpl.MessageState `protogen:"open.v1"` diff --git a/client/status/status.go b/client/status/status.go index 507c7ea80..d28485bc0 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -203,13 +203,18 @@ func mapPeers( localICEEndpoint := "" remoteICEEndpoint := "" relayServerAddress := "" - connType := "" + connType := "P2P" lastHandshake := time.Time{} transferReceived := int64(0) transferSent := int64(0) isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() - if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter, connectionTypeFilter) { + + if pbPeerState.Relayed { + connType = "Relayed" + } + + if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter, connectionTypeFilter, connType) { continue } if isPeerConnected { @@ -219,7 +224,6 @@ func mapPeers( remoteICE = pbPeerState.GetRemoteIceCandidateType() localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() - connType = pbPeerState.GetConnectionType() relayServerAddress = pbPeerState.GetRelayAddress() lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() transferReceived = pbPeerState.GetBytesRx() @@ -540,7 +544,7 @@ func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bo return peersString } -func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter string) bool { +func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter, connType string) bool { statusEval := false ipEval := false nameEval := true @@ -569,7 +573,7 @@ func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFi } else { nameEval = false } - if connectionTypeFilter != "" && !strings.EqualFold(peerState.GetConnectionType(), connectionTypeFilter) { + if connectionTypeFilter != "" && !strings.EqualFold(connType, connectionTypeFilter) { connectionTypeEval = true } From c435c2727fb2bd99c080afdadc4a7d63690e7626 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:33:58 +0200 Subject: [PATCH 170/213] [management] Log BufferUpdateAccountPeers caller (#4217) --- management/server/peer.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/management/server/peer.go b/management/server/peer.go index b82a9cb80..3c40c6bb6 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1291,6 +1291,8 @@ type bufferUpdate struct { } func (am *DefaultAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) { + log.WithContext(ctx).Tracef("buffer updating peers for account %s from %s", accountID, util.GetCallerName()) + bufUpd, _ := am.accountUpdateLocks.LoadOrStore(accountID, &bufferUpdate{}) b := bufUpd.(*bufferUpdate) From cb1e437785478f2ae160def29a1ccf3717725339 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:00:51 +0200 Subject: [PATCH 171/213] [client] handle order of check when checking order of files in isChecksEqual (#4219) --- client/internal/engine.go | 29 +++++++------ client/internal/engine_test.go | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index 1abb8163d..079adf7e8 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1968,21 +1968,24 @@ func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers } // isChecksEqual checks if two slices of checks are equal. -func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { - for _, check := range checks { - sort.Slice(check.Files, func(i, j int) bool { - return check.Files[i] < check.Files[j] - }) - } - for _, oCheck := range oChecks { - sort.Slice(oCheck.Files, func(i, j int) bool { - return oCheck.Files[i] < oCheck.Files[j] - }) +func isChecksEqual(checks1, checks2 []*mgmProto.Checks) bool { + normalize := func(checks []*mgmProto.Checks) []string { + normalized := make([]string, len(checks)) + + for i, check := range checks { + sortedFiles := slices.Clone(check.Files) + sort.Strings(sortedFiles) + normalized[i] = strings.Join(sortedFiles, "|") + } + + sort.Strings(normalized) + return normalized } - return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool { - return slices.Equal(checks.Files, oChecks.Files) - }) + n1 := normalize(checks1) + n2 := normalize(checks2) + + return slices.Equal(n1, n2) } func getInterfacePrefixes() ([]netip.Prefix, error) { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index f02138686..fffbed533 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1270,6 +1270,82 @@ func Test_CheckFilesEqual(t *testing.T) { }, expectedBool: false, }, + { + name: "Compared Slices with same files but different order should return true", + inputChecks1: []*mgmtProto.Checks{ + { + Files: []string{ + "testfile1", + "testfile2", + }, + }, + { + Files: []string{ + "testfile4", + "testfile3", + }, + }, + }, + inputChecks2: []*mgmtProto.Checks{ + { + Files: []string{ + "testfile3", + "testfile4", + }, + }, + { + Files: []string{ + "testfile2", + "testfile1", + }, + }, + }, + expectedBool: true, + }, + { + name: "Compared Slices with same files but different order while first is equal should return true", + inputChecks1: []*mgmtProto.Checks{ + { + Files: []string{ + "testfile0", + "testfile1", + }, + }, + { + Files: []string{ + "testfile0", + "testfile2", + }, + }, + { + Files: []string{ + "testfile0", + "testfile3", + }, + }, + }, + inputChecks2: []*mgmtProto.Checks{ + { + Files: []string{ + "testfile0", + "testfile1", + }, + }, + { + Files: []string{ + "testfile0", + "testfile3", + }, + }, + { + Files: []string{ + "testfile0", + "testfile2", + }, + }, + }, + expectedBool: true, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { From 3f8269808943c3bf84183d8c9ba544159cc7e213 Mon Sep 17 00:00:00 2001 From: Louis Li <32395144+gamerslouis@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:36:11 +0800 Subject: [PATCH 172/213] [client] make ICE failed timeout configurable (#4211) --- client/internal/peer/ice/agent.go | 8 +++----- client/internal/peer/ice/env.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/client/internal/peer/ice/agent.go b/client/internal/peer/ice/agent.go index 9b63cebf0..4a0228405 100644 --- a/client/internal/peer/ice/agent.go +++ b/client/internal/peer/ice/agent.go @@ -18,17 +18,15 @@ const ( iceKeepAliveDefault = 4 * time.Second iceDisconnectedTimeoutDefault = 6 * time.Second + iceFailedTimeoutDefault = 6 * time.Second // iceRelayAcceptanceMinWaitDefault is the same as in the Pion ICE package iceRelayAcceptanceMinWaitDefault = 2 * time.Second ) -var ( - failedTimeout = 6 * time.Second -) - func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candidateTypes []ice.CandidateType, ufrag string, pwd string) (*ice.Agent, error) { iceKeepAlive := iceKeepAlive() iceDisconnectedTimeout := iceDisconnectedTimeout() + iceFailedTimeout := iceFailedTimeout() iceRelayAcceptanceMinWait := iceRelayAcceptanceMinWait() transportNet, err := newStdNet(iFaceDiscover, config.InterfaceBlackList) @@ -50,7 +48,7 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida UDPMuxSrflx: config.UDPMuxSrflx, NAT1To1IPs: config.NATExternalIPs, Net: transportNet, - FailedTimeout: &failedTimeout, + FailedTimeout: &iceFailedTimeout, DisconnectedTimeout: &iceDisconnectedTimeout, KeepaliveInterval: &iceKeepAlive, RelayAcceptanceMinWait: &iceRelayAcceptanceMinWait, diff --git a/client/internal/peer/ice/env.go b/client/internal/peer/ice/env.go index 3b0cb74ad..c11c35441 100644 --- a/client/internal/peer/ice/env.go +++ b/client/internal/peer/ice/env.go @@ -13,6 +13,7 @@ const ( envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN" envICEKeepAliveIntervalSec = "NB_ICE_KEEP_ALIVE_INTERVAL_SEC" envICEDisconnectedTimeoutSec = "NB_ICE_DISCONNECTED_TIMEOUT_SEC" + envICEFailedTimeoutSec = "NB_ICE_FAILED_TIMEOUT_SEC" envICERelayAcceptanceMinWaitSec = "NB_ICE_RELAY_ACCEPTANCE_MIN_WAIT_SEC" msgWarnInvalidValue = "invalid value %s set for %s, using default %v" @@ -55,6 +56,22 @@ func iceDisconnectedTimeout() time.Duration { return time.Duration(disconnectedTimeoutSec) * time.Second } +func iceFailedTimeout() time.Duration { + failedTimeoutEnv := os.Getenv(envICEFailedTimeoutSec) + if failedTimeoutEnv == "" { + return iceFailedTimeoutDefault + } + + log.Infof("setting ICE failed timeout to %s seconds", failedTimeoutEnv) + failedTimeoutSec, err := strconv.Atoi(failedTimeoutEnv) + if err != nil { + log.Warnf(msgWarnInvalidValue, failedTimeoutEnv, envICEFailedTimeoutSec, iceFailedTimeoutDefault) + return iceFailedTimeoutDefault + } + + return time.Duration(failedTimeoutSec) * time.Second +} + func iceRelayAcceptanceMinWait() time.Duration { iceRelayAcceptanceMinWaitEnv := os.Getenv(envICERelayAcceptanceMinWaitSec) if iceRelayAcceptanceMinWaitEnv == "" { From af8687579b571592663371e237fa9a405af56493 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Fri, 25 Jul 2025 11:44:30 +0200 Subject: [PATCH 173/213] client: container: support CLI with entrypoint addition (#4126) This will allow running netbird commands (including debugging) against the daemon and provide a flow similar to non-container usages. It will by default both log to file and stderr so it can be handled more uniformly in container-native environments. --- .dockerignore-client | 3 + .gitignore | 1 + .goreleaser.yaml | 14 ++- client/Dockerfile | 30 ++++-- client/Dockerfile-rootless | 37 +++++-- client/cmd/down.go | 2 +- client/cmd/login.go | 4 +- client/cmd/login_test.go | 2 +- client/cmd/root.go | 7 +- client/cmd/service_controller.go | 6 +- client/cmd/service_installer.go | 6 +- client/cmd/ssh.go | 2 +- client/cmd/status.go | 2 +- client/cmd/up.go | 4 +- client/iface/wgproxy/proxy_test.go | 2 +- client/internal/debug/debug.go | 4 +- client/internal/dns/file_repair_unix_test.go | 2 +- client/internal/engine_test.go | 2 +- client/internal/peer/conn_test.go | 2 +- client/netbird-entrypoint.sh | 105 +++++++++++++++++++ client/server/server_test.go | 5 +- client/ui/client_ui.go | 2 +- management/client/client_test.go | 2 +- relay/client/client_test.go | 2 +- relay/cmd/root.go | 2 +- relay/test/benchmark_test.go | 2 +- relay/testec2/main.go | 2 +- upload-server/main.go | 2 +- util/log.go | 77 ++++++++++---- 29 files changed, 267 insertions(+), 66 deletions(-) create mode 100644 .dockerignore-client create mode 100755 client/netbird-entrypoint.sh diff --git a/.dockerignore-client b/.dockerignore-client new file mode 100644 index 000000000..a93ef97c0 --- /dev/null +++ b/.dockerignore-client @@ -0,0 +1,3 @@ +* +!client/netbird-entrypoint.sh +!netbird diff --git a/.gitignore b/.gitignore index abb728b19..e6c0c0aca 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ infrastructure_files/setup-*.env .vscode .DS_Store vendor/ +/netbird diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ca5eafa62..d4a97b447 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -155,13 +155,15 @@ dockers: goarch: amd64 use: buildx dockerfile: client/Dockerfile + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-arm64v8 @@ -171,6 +173,8 @@ dockers: goarch: arm64 use: buildx dockerfile: client/Dockerfile + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm64" - "--label=org.opencontainers.image.created={{.Date}}" @@ -188,6 +192,8 @@ dockers: goarm: 6 use: buildx dockerfile: client/Dockerfile + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm" - "--label=org.opencontainers.image.created={{.Date}}" @@ -205,6 +211,8 @@ dockers: goarch: amd64 use: buildx dockerfile: client/Dockerfile-rootless + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" @@ -221,6 +229,8 @@ dockers: goarch: arm64 use: buildx dockerfile: client/Dockerfile-rootless + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm64" - "--label=org.opencontainers.image.created={{.Date}}" @@ -238,6 +248,8 @@ dockers: goarm: 6 use: buildx dockerfile: client/Dockerfile-rootless + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm" - "--label=org.opencontainers.image.created={{.Date}}" diff --git a/client/Dockerfile b/client/Dockerfile index 5f1f70040..e19a09909 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,9 +1,27 @@ -FROM alpine:3.21.3 +# build & run locally with: +# cd "$(git rev-parse --show-toplevel)" +# CGO_ENABLED=0 go build -o netbird ./client +# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client . +# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest + +FROM alpine:3.22.0 # iproute2: busybox doesn't display ip rules properly -RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables +RUN apk add --no-cache \ + bash \ + ca-certificates \ + ip6tables \ + iproute2 \ + iptables + +ENV \ + NETBIRD_BIN="/usr/local/bin/netbird" \ + NB_LOG_FILE="console,/var/log/netbird/client.log" \ + NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \ + NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \ + NB_ENTRYPOINT_LOGIN_TIMEOUT="1" + +ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ] ARG NETBIRD_BINARY=netbird -COPY ${NETBIRD_BINARY} /usr/local/bin/netbird - -ENV NB_FOREGROUND_MODE=true -ENTRYPOINT [ "/usr/local/bin/netbird","up"] +COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh +COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless index 5055cb20d..5fa8de0a5 100644 --- a/client/Dockerfile-rootless +++ b/client/Dockerfile-rootless @@ -1,18 +1,33 @@ -FROM alpine:3.21.0 +# build & run locally with: +# cd "$(git rev-parse --show-toplevel)" +# CGO_ENABLED=0 go build -o netbird ./client +# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client . +# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest -ARG NETBIRD_BINARY=netbird -COPY ${NETBIRD_BINARY} /usr/local/bin/netbird +FROM alpine:3.22.0 -RUN apk add --no-cache ca-certificates \ +RUN apk add --no-cache \ + bash \ + ca-certificates \ && adduser -D -h /var/lib/netbird netbird + WORKDIR /var/lib/netbird USER netbird:netbird -ENV NB_FOREGROUND_MODE=true -ENV NB_USE_NETSTACK_MODE=true -ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true -ENV NB_CONFIG=config.json -ENV NB_DAEMON_ADDR=unix://netbird.sock -ENV NB_DISABLE_DNS=true +ENV \ + NETBIRD_BIN="/usr/local/bin/netbird" \ + NB_USE_NETSTACK_MODE="true" \ + NB_ENABLE_NETSTACK_LOCAL_FORWARDING="true" \ + NB_CONFIG="/var/lib/netbird/config.json" \ + NB_STATE_DIR="/var/lib/netbird" \ + NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \ + NB_LOG_FILE="console,/var/lib/netbird/client.log" \ + NB_DISABLE_DNS="true" \ + NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \ + NB_ENTRYPOINT_LOGIN_TIMEOUT="1" -ENTRYPOINT [ "/usr/local/bin/netbird", "up" ] +ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ] + +ARG NETBIRD_BINARY=netbird +COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh +COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird diff --git a/client/cmd/down.go b/client/cmd/down.go index 3a324cc19..cfa69bce2 100644 --- a/client/cmd/down.go +++ b/client/cmd/down.go @@ -20,7 +20,7 @@ var downCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { log.Errorf("failed initializing log %v", err) return err diff --git a/client/cmd/login.go b/client/cmd/login.go index 14abcd034..8ac7086b8 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -32,7 +32,7 @@ var loginCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } @@ -50,7 +50,7 @@ var loginCmd = &cobra.Command{ } // workaround to run without service - if logFile == "console" { + if util.FindFirstLogPath(logFiles) == "" { err = handleRebrand(cmd) if err != nil { return err diff --git a/client/cmd/login_test.go b/client/cmd/login_test.go index fa20435ea..cf98a5854 100644 --- a/client/cmd/login_test.go +++ b/client/cmd/login_test.go @@ -21,7 +21,7 @@ func TestLogin(t *testing.T) { "--config", confPath, "--log-file", - "console", + util.LogConsole, "--setup-key", strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"), "--management-url", diff --git a/client/cmd/root.go b/client/cmd/root.go index bfd0d06c5..1774602c4 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -10,6 +10,7 @@ import ( "os/signal" "path" "runtime" + "slices" "strings" "syscall" "time" @@ -51,7 +52,7 @@ var ( defaultLogFile string oldDefaultLogFileDir string oldDefaultLogFile string - logFile string + logFiles []string daemonAddr string managementURL string adminURL string @@ -120,7 +121,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") - rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.") + rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character") rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") rootCmd.PersistentFlags().StringVar(&setupKeyPath, "setup-key-file", "", "The path to a setup key obtained from the Management Service Dashboard (used to register peer) This is ignored if the setup-key flag is provided.") rootCmd.MarkFlagsMutuallyExclusive("setup-key", "setup-key-file") @@ -265,7 +266,7 @@ func getSetupKeyFromFile(setupKeyPath string) (string, error) { func handleRebrand(cmd *cobra.Command) error { var err error - if logFile == defaultLogFile { + if slices.Contains(logFiles, defaultLogFile) { if migrateToNetbird(oldDefaultLogFile, defaultLogFile) { cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir) err = cpDir(oldDefaultLogFileDir, defaultLogFileDir) diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 2545623ec..df84342c9 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, configPath, logFile) + serverInstance := server.New(p.ctx, configPath, util.FindFirstLogPath(logFiles)) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } @@ -112,7 +112,7 @@ func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel return nil, err } - if err := util.InitLog(logLevel, logFile); err != nil { + if err := util.InitLog(logLevel, logFiles...); err != nil { return nil, fmt.Errorf("init log: %w", err) } @@ -136,7 +136,7 @@ var runCmd = &cobra.Command{ ctx, cancel := context.WithCancel(cmd.Context()) SetupCloseHandler(ctx, cancel) - SetupDebugHandler(ctx, nil, nil, nil, logFile) + SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles)) s, err := setupServiceControlCommand(cmd, ctx, cancel) if err != nil { diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index 951efcc73..c994801a6 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -12,6 +12,8 @@ import ( "github.com/kardianos/service" "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/util" ) var ErrGetServiceStatus = fmt.Errorf("failed to get service status") @@ -41,7 +43,7 @@ func buildServiceArguments() []string { args = append(args, "--management-url", managementURL) } - if logFile != "" { + for _, logFile := range logFiles { args = append(args, "--log-file", logFile) } @@ -54,7 +56,7 @@ func configurePlatformSpecificSettings(svcConfig *service.Config) error { // Respected only by systemd systems svcConfig.Dependencies = []string{"After=network.target syslog.target"} - if logFile != "console" { + if logFile := util.FindFirstLogPath(logFiles); logFile != "" { setStdLogPath := true dir := filepath.Dir(logFile) diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index f9dbc26fc..264f643ee 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -46,7 +46,7 @@ var sshCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } diff --git a/client/cmd/status.go b/client/cmd/status.go index 2d6e41bc2..e50156ac9 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -59,7 +59,7 @@ func statusFunc(cmd *cobra.Command, args []string) error { return err } - err = util.InitLog(logLevel, "console") + err = util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } diff --git a/client/cmd/up.go b/client/cmd/up.go index b9781c0df..529beeac7 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -79,7 +79,7 @@ func upFunc(cmd *cobra.Command, args []string) error { cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } @@ -484,7 +484,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) { if !isValidAddrPort(customDNSAddress) { return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress) } - if customDNSAddress == "" && logFile != "console" { + if customDNSAddress == "" && util.FindFirstLogPath(logFiles) != "" { parsed = []byte("empty") } else { parsed = []byte(customDNSAddress) diff --git a/client/iface/wgproxy/proxy_test.go b/client/iface/wgproxy/proxy_test.go index 2165b8aba..6882f9ea2 100644 --- a/client/iface/wgproxy/proxy_test.go +++ b/client/iface/wgproxy/proxy_test.go @@ -17,7 +17,7 @@ import ( ) func TestMain(m *testing.M) { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 6455b3aaf..220e6854d 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -16,6 +16,7 @@ import ( "path/filepath" "runtime" "runtime/pprof" + "slices" "sort" "strings" "time" @@ -28,6 +29,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" mgmProto "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/util" ) const readmeContent = `Netbird debug bundle @@ -283,7 +285,7 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("Failed to add wg show output: %v", err) } - if g.logFile != "console" && g.logFile != "" { + if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) { if err := g.addLogfile(); err != nil { log.Errorf("Failed to add log file to debug bundle: %v", err) if err := g.trySystemdLogFallback(); err != nil { diff --git a/client/internal/dns/file_repair_unix_test.go b/client/internal/dns/file_repair_unix_test.go index e948557b6..3aa0b859e 100644 --- a/client/internal/dns/file_repair_unix_test.go +++ b/client/internal/dns/file_repair_unix_test.go @@ -14,7 +14,7 @@ import ( ) func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index fffbed533..69586b47a 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -196,7 +196,7 @@ func (m *MockWGIface) LastActivities() map[string]monotime.Time { } func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index c5055e646..7cad45953 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -31,7 +31,7 @@ var connConf = ConnConfig{ } func TestMain(m *testing.M) { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/netbird-entrypoint.sh b/client/netbird-entrypoint.sh new file mode 100755 index 000000000..2422d2683 --- /dev/null +++ b/client/netbird-entrypoint.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +: ${NB_ENTRYPOINT_SERVICE_TIMEOUT:="5"} +: ${NB_ENTRYPOINT_LOGIN_TIMEOUT:="1"} +NETBIRD_BIN="${NETBIRD_BIN:-"netbird"}" +export NB_LOG_FILE="${NB_LOG_FILE:-"console,/var/log/netbird/client.log"}" +service_pids=() +log_file_path="" + +_log() { + # mimic Go logger's output for easier parsing + # 2025-04-15T21:32:00+08:00 INFO client/internal/config.go:495: setting notifications to disabled by default + printf "$(date -Isec) ${1} ${BASH_SOURCE[1]}:${BASH_LINENO[1]}: ${2}\n" "${@:3}" >&2 +} + +info() { + _log INFO "$@" +} + +warn() { + _log WARN "$@" +} + +on_exit() { + info "Shutting down NetBird daemon..." + if test "${#service_pids[@]}" -gt 0; then + info "terminating service process IDs: ${service_pids[@]@Q}" + kill -TERM "${service_pids[@]}" 2>/dev/null || true + wait "${service_pids[@]}" 2>/dev/null || true + else + info "there are no service processes to terminate" + fi +} + +wait_for_message() { + local timeout="${1}" message="${2}" + if test "${timeout}" -eq 0; then + info "not waiting for log line ${message@Q} due to zero timeout." + elif test -n "${log_file_path}"; then + info "waiting for log line ${message@Q} for ${timeout} seconds..." + grep -q "${message}" <(timeout "${timeout}" tail -F "${log_file_path}" 2>/dev/null) + else + info "log file unsupported, sleeping for ${timeout} seconds..." + sleep "${timeout}" + fi +} + +locate_log_file() { + local log_files_string="${1}" + + while read -r log_file; do + case "${log_file}" in + console | syslog) ;; + *) + log_file_path="${log_file}" + return + ;; + esac + done < <(sed 's#,#\n#g' <<<"${log_files_string}") + + warn "log files parsing for ${log_files_string@Q} is not supported by debug bundles" + warn "please consider removing the \$NB_LOG_FILE or setting it to real file, before gathering debug bundles." +} + +wait_for_daemon_startup() { + local timeout="${1}" + + if test -n "${log_file_path}"; then + if ! wait_for_message "${timeout}" "started daemon server"; then + warn "log line containing 'started daemon server' not found after ${timeout} seconds" + warn "daemon failed to start, exiting..." + exit 1 + fi + else + warn "daemon service startup not discovered, sleeping ${timeout} instead" + sleep "${timeout}" + fi +} + +login_if_needed() { + local timeout="${1}" + + if test -n "${log_file_path}" && wait_for_message "${timeout}" 'peer has been successfully registered'; then + info "already logged in, skipping 'netbird up'..." + else + info "logging in..." + "${NETBIRD_BIN}" up + fi +} + +main() { + trap 'on_exit' SIGTERM SIGINT EXIT + "${NETBIRD_BIN}" service run & + service_pids+=("$!") + info "registered new service process 'netbird service run', currently running: ${service_pids[@]@Q}" + + locate_log_file "${NB_LOG_FILE}" + wait_for_daemon_startup "${NB_ENTRYPOINT_SERVICE_TIMEOUT}" + login_if_needed "${NB_ENTRYPOINT_LOGIN_TIMEOUT}" + + wait "${service_pids[@]}" +} + +main "$@" diff --git a/client/server/server_test.go b/client/server/server_test.go index 7c46aac5d..11e4d3899 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -32,6 +32,7 @@ import ( "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" + "github.com/netbirdio/netbird/util" ) var ( @@ -92,7 +93,7 @@ func TestConnectWithRetryRuns(t *testing.T) { func TestServer_Up(t *testing.T) { ctx := internal.CtxInitState(context.Background()) - s := New(ctx, t.TempDir()+"/config.json", "console") + s := New(ctx, t.TempDir()+"/config.json", util.LogConsole) err := s.Start() require.NoError(t, err) @@ -130,7 +131,7 @@ func (m *mockSubscribeEventsServer) Context() context.Context { func TestServer_SubcribeEvents(t *testing.T) { ctx := internal.CtxInitState(context.Background()) - s := New(ctx, t.TempDir()+"/config.json", "console") + s := New(ctx, t.TempDir()+"/config.json", util.LogConsole) err := s.Start() require.NoError(t, err) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index ace5b71e4..4480adb51 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -66,7 +66,7 @@ func main() { } logFile = file } else { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) } // Create the Fyne application. diff --git a/management/client/client_test.go b/management/client/client_test.go index b59b7c982..5b2a87492 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -41,7 +41,7 @@ import ( const ValidKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/relay/client/client_test.go b/relay/client/client_test.go index c85ec9fd3..2ce8d7e34 100644 --- a/relay/client/client_test.go +++ b/relay/client/client_test.go @@ -30,7 +30,7 @@ var ( ) func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/relay/cmd/root.go b/relay/cmd/root.go index 15090024c..7b8e5bbeb 100644 --- a/relay/cmd/root.go +++ b/relay/cmd/root.go @@ -73,7 +73,7 @@ var ( ) func init() { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) cobraConfig = &Config{} rootCmd.PersistentFlags().StringVarP(&cobraConfig.ListenAddress, "listen-address", "l", ":443", "listen address") rootCmd.PersistentFlags().StringVarP(&cobraConfig.ExposedAddress, "exposed-address", "e", "", "instance domain address (or ip) and port, it will be distributes between peers") diff --git a/relay/test/benchmark_test.go b/relay/test/benchmark_test.go index 2e67ab803..afbb14b84 100644 --- a/relay/test/benchmark_test.go +++ b/relay/test/benchmark_test.go @@ -27,7 +27,7 @@ var ( ) func TestMain(m *testing.M) { - _ = util.InitLog("error", "console") + _ = util.InitLog("error", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/relay/testec2/main.go b/relay/testec2/main.go index 0c8099a5e..6954d6a50 100644 --- a/relay/testec2/main.go +++ b/relay/testec2/main.go @@ -233,7 +233,7 @@ func TURNReaderMain() []testResult { func main() { var mode string - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) flag.StringVar(&mode, "mode", "sender", "sender or receiver mode") flag.Parse() diff --git a/upload-server/main.go b/upload-server/main.go index dcfb35cdf..546c0f584 100644 --- a/upload-server/main.go +++ b/upload-server/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - err := util.InitLog("info", "console") + err := util.InitLog("info", util.LogConsole) if err != nil { log.Fatalf("Failed to initialize logger: %v", err) } diff --git a/util/log.go b/util/log.go index 53d2b0684..a951eab87 100644 --- a/util/log.go +++ b/util/log.go @@ -16,36 +16,54 @@ import ( const defaultLogSize = 15 +const ( + LogConsole = "console" + LogSyslog = "syslog" +) + +var ( + SpecialLogs = []string{ + LogSyslog, + LogConsole, + } +) + // InitLog parses and sets log-level input -func InitLog(logLevel string, logPath string) error { +func InitLog(logLevel string, logs ...string) error { level, err := log.ParseLevel(logLevel) if err != nil { log.Errorf("Failed parsing log-level %s: %s", logLevel, err) return err } - customOutputs := []string{"console", "syslog"} + var writers []io.Writer + logFmt := os.Getenv("NB_LOG_FORMAT") - if logPath != "" && !slices.Contains(customOutputs, logPath) { - maxLogSize := getLogMaxSize() - lumberjackLogger := &lumberjack.Logger{ - // Log file absolute path, os agnostic - Filename: filepath.ToSlash(logPath), - MaxSize: maxLogSize, // MB - MaxBackups: 10, - MaxAge: 30, // days - Compress: true, + for _, logPath := range logs { + switch logPath { + case LogSyslog: + AddSyslogHook() + logFmt = "syslog" + case LogConsole: + writers = append(writers, os.Stderr) + case "": + log.Warnf("empty log path received: %#v", logPath) + default: + writers = append(writers, newRotatedOutput(logPath)) } - log.SetOutput(io.Writer(lumberjackLogger)) - } else if logPath == "syslog" { - AddSyslogHook() } - //nolint:gocritic - if os.Getenv("NB_LOG_FORMAT") == "json" { + if len(writers) > 1 { + log.SetOutput(io.MultiWriter(writers...)) + } else if len(writers) == 1 { + log.SetOutput(writers[0]) + } + + switch logFmt { + case "json": formatter.SetJSONFormatter(log.StandardLogger()) - } else if logPath == "syslog" { + case "syslog": formatter.SetSyslogFormatter(log.StandardLogger()) - } else { + default: formatter.SetTextFormatter(log.StandardLogger()) } log.SetLevel(level) @@ -55,6 +73,29 @@ func InitLog(logLevel string, logPath string) error { return nil } +// FindFirstLogPath returns the first logs entry that could be a log path, that is neither empty, nor a special value +func FindFirstLogPath(logs []string) string { + for _, logFile := range logs { + if logFile != "" && !slices.Contains(SpecialLogs, logFile) { + return logFile + } + } + return "" +} + +func newRotatedOutput(logPath string) io.Writer { + maxLogSize := getLogMaxSize() + lumberjackLogger := &lumberjack.Logger{ + // Log file absolute path, os agnostic + Filename: filepath.ToSlash(logPath), + MaxSize: maxLogSize, // MB + MaxBackups: 10, + MaxAge: 30, // days + Compress: true, + } + return lumberjackLogger +} + func setGRPCLibLogger() { logOut := log.StandardLogger().Writer() if os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL") != "info" { From cb85d3f2fc8d6836544013fd1764d93d8a02351d Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:46:04 +0200 Subject: [PATCH 174/213] [client] Always register NetBird with plain Linux DNS and use original servers as upstream (#3967) --- client/internal/dns/file_parser_unix.go | 40 +------- client/internal/dns/file_parser_unix_test.go | 52 +--------- client/internal/dns/file_repair_unix.go | 9 +- client/internal/dns/file_repair_unix_test.go | 9 +- client/internal/dns/file_unix.go | 73 +++++--------- client/internal/dns/handler_chain.go | 3 +- client/internal/dns/host.go | 8 +- client/internal/dns/host_darwin.go | 22 ++-- client/internal/dns/host_windows.go | 11 +- client/internal/dns/mock_server.go | 5 +- client/internal/dns/network_manager_unix.go | 6 +- client/internal/dns/resolvconf_unix.go | 31 +++--- client/internal/dns/server.go | 101 ++++++++++++++----- client/internal/dns/server_test.go | 4 +- client/internal/dns/service.go | 4 +- client/internal/dns/service_listener.go | 34 ++++--- client/internal/dns/service_memory.go | 13 +-- client/internal/dns/systemd_linux.go | 21 ++-- client/internal/dns/unclean_shutdown_unix.go | 7 +- client/internal/engine.go | 2 +- 20 files changed, 196 insertions(+), 259 deletions(-) diff --git a/client/internal/dns/file_parser_unix.go b/client/internal/dns/file_parser_unix.go index 130c88214..6e123c94e 100644 --- a/client/internal/dns/file_parser_unix.go +++ b/client/internal/dns/file_parser_unix.go @@ -4,8 +4,8 @@ package dns import ( "fmt" + "net/netip" "os" - "regexp" "strings" log "github.com/sirupsen/logrus" @@ -15,9 +15,6 @@ const ( defaultResolvConfPath = "/etc/resolv.conf" ) -var timeoutRegex = regexp.MustCompile(`timeout:\d+`) -var attemptsRegex = regexp.MustCompile(`attempts:\d+`) - type resolvConf struct { nameServers []string searchDomains []string @@ -108,40 +105,9 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) { return rconf, nil } -// prepareOptionsWithTimeout appends timeout to existing options if it doesn't exist, -// otherwise it adds a new option with timeout and attempts. -func prepareOptionsWithTimeout(input []string, timeout int, attempts int) []string { - configs := make([]string, len(input)) - copy(configs, input) - - for i, config := range configs { - if strings.HasPrefix(config, "options") { - config = strings.ReplaceAll(config, "rotate", "") - config = strings.Join(strings.Fields(config), " ") - - if strings.Contains(config, "timeout:") { - config = timeoutRegex.ReplaceAllString(config, fmt.Sprintf("timeout:%d", timeout)) - } else { - config = strings.Replace(config, "options ", fmt.Sprintf("options timeout:%d ", timeout), 1) - } - - if strings.Contains(config, "attempts:") { - config = attemptsRegex.ReplaceAllString(config, fmt.Sprintf("attempts:%d", attempts)) - } else { - config = strings.Replace(config, "options ", fmt.Sprintf("options attempts:%d ", attempts), 1) - } - - configs[i] = config - return configs - } - } - - return append(configs, fmt.Sprintf("options timeout:%d attempts:%d", timeout, attempts)) -} - // removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position // and writes the file back to the original location -func removeFirstNbNameserver(filename, nameserverIP string) error { +func removeFirstNbNameserver(filename string, nameserverIP netip.Addr) error { resolvConf, err := parseResolvConfFile(filename) if err != nil { return fmt.Errorf("parse backup resolv.conf: %w", err) @@ -151,7 +117,7 @@ func removeFirstNbNameserver(filename, nameserverIP string) error { return fmt.Errorf("read %s: %w", filename, err) } - if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP { + if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP.String() { newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1) stat, err := os.Stat(filename) diff --git a/client/internal/dns/file_parser_unix_test.go b/client/internal/dns/file_parser_unix_test.go index 1d6e64683..228a708f1 100644 --- a/client/internal/dns/file_parser_unix_test.go +++ b/client/internal/dns/file_parser_unix_test.go @@ -3,11 +3,13 @@ package dns import ( + "net/netip" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_parseResolvConf(t *testing.T) { @@ -175,52 +177,6 @@ nameserver 192.168.0.1 } } -func TestPrepareOptionsWithTimeout(t *testing.T) { - tests := []struct { - name string - others []string - timeout int - attempts int - expected []string - }{ - { - name: "Append new options with timeout and attempts", - others: []string{"some config"}, - timeout: 2, - attempts: 2, - expected: []string{"some config", "options timeout:2 attempts:2"}, - }, - { - name: "Modify existing options to exclude rotate and include timeout and attempts", - others: []string{"some config", "options rotate someother"}, - timeout: 3, - attempts: 2, - expected: []string{"some config", "options attempts:2 timeout:3 someother"}, - }, - { - name: "Existing options with timeout and attempts are updated", - others: []string{"some config", "options timeout:4 attempts:3"}, - timeout: 5, - attempts: 4, - expected: []string{"some config", "options timeout:5 attempts:4"}, - }, - { - name: "Modify existing options, add missing attempts before timeout", - others: []string{"some config", "options timeout:4"}, - timeout: 4, - attempts: 3, - expected: []string{"some config", "options attempts:3 timeout:4"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := prepareOptionsWithTimeout(tc.others, tc.timeout, tc.attempts) - assert.Equal(t, tc.expected, result) - }) - } -} - func TestRemoveFirstNbNameserver(t *testing.T) { testCases := []struct { name string @@ -292,7 +248,9 @@ search localdomain`, err := os.WriteFile(tempFile, []byte(tc.content), 0644) assert.NoError(t, err) - err = removeFirstNbNameserver(tempFile, tc.ipToRemove) + ip, err := netip.ParseAddr(tc.ipToRemove) + require.NoError(t, err, "Failed to parse IP address") + err = removeFirstNbNameserver(tempFile, ip) assert.NoError(t, err) content, err := os.ReadFile(tempFile) diff --git a/client/internal/dns/file_repair_unix.go b/client/internal/dns/file_repair_unix.go index 9a9218fa1..75af411df 100644 --- a/client/internal/dns/file_repair_unix.go +++ b/client/internal/dns/file_repair_unix.go @@ -3,6 +3,7 @@ package dns import ( + "net/netip" "path" "path/filepath" "sync" @@ -22,7 +23,7 @@ var ( } ) -type repairConfFn func([]string, string, *resolvConf, *statemanager.Manager) error +type repairConfFn func([]string, netip.Addr, *resolvConf, *statemanager.Manager) error type repair struct { operationFile string @@ -42,7 +43,7 @@ func newRepair(operationFile string, updateFn repairConfFn) *repair { } } -func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP string, stateManager *statemanager.Manager) { +func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP netip.Addr, stateManager *statemanager.Manager) { if f.inotify != nil { return } @@ -136,7 +137,7 @@ func (f *repair) isEventRelevant(event fsnotify.Event) bool { // nbParamsAreMissing checks if the resolv.conf file contains all the parameters that NetBird needs // check the NetBird related nameserver IP at the first place // check the NetBird related search domains in the search domains list -func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP string, rConf *resolvConf) bool { +func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP netip.Addr, rConf *resolvConf) bool { if !isContains(nbSearchDomains, rConf.searchDomains) { return true } @@ -145,7 +146,7 @@ func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP string, rConf *r return true } - if rConf.nameServers[0] != nbNameserverIP { + if rConf.nameServers[0] != nbNameserverIP.String() { return true } diff --git a/client/internal/dns/file_repair_unix_test.go b/client/internal/dns/file_repair_unix_test.go index 3aa0b859e..f22081307 100644 --- a/client/internal/dns/file_repair_unix_test.go +++ b/client/internal/dns/file_repair_unix_test.go @@ -4,6 +4,7 @@ package dns import ( "context" + "net/netip" "os" "path/filepath" "testing" @@ -105,14 +106,14 @@ nameserver 8.8.8.8`, var changed bool ctx, cancel := context.WithTimeout(context.Background(), time.Second) - updateFn := func([]string, string, *resolvConf, *statemanager.Manager) error { + updateFn := func([]string, netip.Addr, *resolvConf, *statemanager.Manager) error { changed = true cancel() return nil } r := newRepair(operationFile, updateFn) - r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1", nil) + r.watchFileChanges([]string{"netbird.cloud"}, netip.MustParseAddr("10.0.0.1"), nil) err = os.WriteFile(operationFile, []byte(tt.touchedConfContent), 0755) if err != nil { @@ -152,14 +153,14 @@ searchdomain netbird.cloud something` var changed bool ctx, cancel := context.WithTimeout(context.Background(), time.Second) - updateFn := func([]string, string, *resolvConf, *statemanager.Manager) error { + updateFn := func([]string, netip.Addr, *resolvConf, *statemanager.Manager) error { changed = true cancel() return nil } r := newRepair(tmpLink, updateFn) - r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1", nil) + r.watchFileChanges([]string{"netbird.cloud"}, netip.MustParseAddr("10.0.0.1"), nil) err = os.WriteFile(tmpLink, []byte(modifyContent), 0755) if err != nil { diff --git a/client/internal/dns/file_unix.go b/client/internal/dns/file_unix.go index 3e338267f..423989f72 100644 --- a/client/internal/dns/file_unix.go +++ b/client/internal/dns/file_unix.go @@ -8,7 +8,6 @@ import ( "net/netip" "os" "strings" - "time" log "github.com/sirupsen/logrus" @@ -18,7 +17,7 @@ import ( const ( fileGeneratedResolvConfContentHeader = "# Generated by NetBird" fileGeneratedResolvConfContentHeaderNextLine = fileGeneratedResolvConfContentHeader + ` -# If needed you can restore the original file by copying back ` + fileDefaultResolvConfBackupLocation + "\n\n" +# The original file can be restored from ` + fileDefaultResolvConfBackupLocation + "\n\n" fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird" @@ -26,16 +25,11 @@ const ( fileMaxNumberOfSearchDomains = 6 ) -const ( - dnsFailoverTimeout = 4 * time.Second - dnsFailoverAttempts = 1 -) - type fileConfigurator struct { - repair *repair - - originalPerms os.FileMode - nbNameserverIP string + repair *repair + originalPerms os.FileMode + nbNameserverIP netip.Addr + originalNameservers []string } func newFileConfigurator() (*fileConfigurator, error) { @@ -49,22 +43,9 @@ func (f *fileConfigurator) supportCustomPort() bool { } func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { - backupFileExist := f.isBackupFileExist() - if !config.RouteAll { - if backupFileExist { - f.repair.stopWatchFileChanges() - err := f.restore() - if err != nil { - return fmt.Errorf("restoring the original resolv.conf file return err: %w", err) - } - } - return ErrRouteAllWithoutNameserverGroup - } - - if !backupFileExist { - err := f.backup() - if err != nil { - return fmt.Errorf("unable to backup the resolv.conf file: %w", err) + if !f.isBackupFileExist() { + if err := f.backup(); err != nil { + return fmt.Errorf("backup resolv.conf: %w", err) } } @@ -76,6 +57,8 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st log.Errorf("could not read original search domains from %s: %s", fileDefaultResolvConfBackupLocation, err) } + f.originalNameservers = resolvConf.nameServers + f.repair.stopWatchFileChanges() err = f.updateConfig(nbSearchDomains, f.nbNameserverIP, resolvConf, stateManager) @@ -86,15 +69,19 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st return nil } -func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP string, cfg *resolvConf, stateManager *statemanager.Manager) error { - searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains) - nameServers := generateNsList(nbNameserverIP, cfg) +// getOriginalNameservers returns the nameservers that were found in the original resolv.conf +func (f *fileConfigurator) getOriginalNameservers() []string { + return f.originalNameservers +} + +func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP netip.Addr, cfg *resolvConf, stateManager *statemanager.Manager) error { + searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains) - options := prepareOptionsWithTimeout(cfg.others, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts) buf := prepareResolvConfContent( searchDomainList, - nameServers, - options) + []string{nbNameserverIP.String()}, + cfg.others, + ) log.Debugf("creating managed file %s", defaultResolvConfPath) err := os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms) @@ -197,38 +184,28 @@ func restoreResolvConfFile() error { return nil } -// generateNsList generates a list of nameservers from the config and adds the primary nameserver to the beginning of the list -func generateNsList(nbNameserverIP string, cfg *resolvConf) []string { - ns := make([]string, 1, len(cfg.nameServers)+1) - ns[0] = nbNameserverIP - for _, cfgNs := range cfg.nameServers { - if nbNameserverIP != cfgNs { - ns = append(ns, cfgNs) - } - } - return ns -} - func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes.Buffer { var buf bytes.Buffer + buf.WriteString(fileGeneratedResolvConfContentHeaderNextLine) for _, cfgLine := range others { buf.WriteString(cfgLine) - buf.WriteString("\n") + buf.WriteByte('\n') } if len(searchDomains) > 0 { buf.WriteString("search ") buf.WriteString(strings.Join(searchDomains, " ")) - buf.WriteString("\n") + buf.WriteByte('\n') } for _, ns := range nameServers { buf.WriteString("nameserver ") buf.WriteString(ns) - buf.WriteString("\n") + buf.WriteByte('\n') } + return buf } diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 7e7e7cc2d..36da8fb78 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -15,6 +15,7 @@ const ( PriorityDNSRoute = 75 PriorityUpstream = 50 PriorityDefault = 1 + PriorityFallback = -100 ) type SubdomainMatcher interface { @@ -191,7 +192,7 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { // No handler matched or all handlers passed log.Tracef("no handler found for domain=%s", qname) resp := &dns.Msg{} - resp.SetRcode(r, dns.RcodeNameError) + resp.SetRcode(r, dns.RcodeRefused) if err := w.WriteMsg(resp); err != nil { log.Errorf("failed to write DNS response: %v", err) } diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index dbf0f2cfc..fa474afde 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -11,8 +11,6 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) -var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") - const ( ipv4ReverseZone = ".in-addr.arpa." ipv6ReverseZone = ".ip6.arpa." @@ -27,14 +25,14 @@ type hostManager interface { type SystemDNSSettings struct { Domains []string - ServerIP string + ServerIP netip.Addr ServerPort int } type HostDNSConfig struct { Domains []DomainConfig `json:"domains"` RouteAll bool `json:"routeAll"` - ServerIP string `json:"serverIP"` + ServerIP netip.Addr `json:"serverIP"` ServerPort int `json:"serverPort"` } @@ -89,7 +87,7 @@ func newNoopHostMocker() hostManager { } } -func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostDNSConfig { +func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip netip.Addr, port int) HostDNSConfig { config := HostDNSConfig{ RouteAll: false, ServerIP: ip, diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index a445bc6c4..820cf9029 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -7,7 +7,7 @@ import ( "bytes" "fmt" "io" - "net" + "net/netip" "os/exec" "strconv" "strings" @@ -165,13 +165,13 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error { } func (s *systemConfigurator) addLocalDNS() error { - if s.systemDNSSettings.ServerIP == "" || len(s.systemDNSSettings.Domains) == 0 { + if !s.systemDNSSettings.ServerIP.IsValid() || len(s.systemDNSSettings.Domains) == 0 { err := s.recordSystemDNSSettings(true) log.Errorf("Unable to get system DNS configuration") return err } localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix) - if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 { + if s.systemDNSSettings.ServerIP.IsValid() && len(s.systemDNSSettings.Domains) != 0 { err := s.addSearchDomains(localKey, strings.Join(s.systemDNSSettings.Domains, " "), s.systemDNSSettings.ServerIP, s.systemDNSSettings.ServerPort) if err != nil { return fmt.Errorf("couldn't add local network DNS conf: %w", err) @@ -184,7 +184,7 @@ func (s *systemConfigurator) addLocalDNS() error { } func (s *systemConfigurator) recordSystemDNSSettings(force bool) error { - if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 && !force { + if s.systemDNSSettings.ServerIP.IsValid() && len(s.systemDNSSettings.Domains) != 0 && !force { return nil } @@ -238,8 +238,8 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) { dnsSettings.Domains = append(dnsSettings.Domains, searchDomain) } else if inServerAddressesArray { address := strings.Split(line, " : ")[1] - if ip := net.ParseIP(address); ip != nil && ip.To4() != nil { - dnsSettings.ServerIP = address + if ip, err := netip.ParseAddr(address); err == nil && ip.Is4() { + dnsSettings.ServerIP = ip inServerAddressesArray = false // Stop reading after finding the first IPv4 address } } @@ -250,12 +250,12 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) { } // default to 53 port - dnsSettings.ServerPort = 53 + dnsSettings.ServerPort = defaultPort return dnsSettings, nil } -func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error { +func (s *systemConfigurator) addSearchDomains(key, domains string, ip netip.Addr, port int) error { err := s.addDNSState(key, domains, ip, port, true) if err != nil { return fmt.Errorf("add dns state: %w", err) @@ -268,7 +268,7 @@ func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, po return nil } -func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error { +func (s *systemConfigurator) addMatchDomains(key, domains string, dnsServer netip.Addr, port int) error { err := s.addDNSState(key, domains, dnsServer, port, false) if err != nil { return fmt.Errorf("add dns state: %w", err) @@ -281,14 +281,14 @@ func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, por return nil } -func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port int, enableSearch bool) error { +func (s *systemConfigurator) addDNSState(state, domains string, dnsServer netip.Addr, port int, enableSearch bool) error { noSearch := "1" if enableSearch { noSearch = "0" } lines := buildAddCommandLine(keySupplementalMatchDomains, arraySymbol+domains) lines += buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+noSearch) - lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer) + lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer.String()) lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port)) addDomainCommand := buildCreateStateWithOperation(state, lines) diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index f8939328a..648a58207 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "net/netip" "os/exec" "strings" "syscall" @@ -210,8 +211,8 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager return nil } -func (r *registryConfigurator) addDNSSetupForAll(ip string) error { - if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip); err != nil { +func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error { + if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip.String()); err != nil { return fmt.Errorf("adding dns setup for all failed: %w", err) } r.routingAll = true @@ -219,7 +220,7 @@ func (r *registryConfigurator) addDNSSetupForAll(ip string) error { return nil } -func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error { +func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) error { // if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored // see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745 if r.gpo { @@ -241,7 +242,7 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er } // configureDNSPolicy handles the actual configuration of a DNS policy at the specified path -func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip string) error { +func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error { if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil { return fmt.Errorf("remove existing dns policy: %w", err) } @@ -260,7 +261,7 @@ func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []s return fmt.Errorf("set %s: %w", dnsPolicyConfigNameKey, err) } - if err := regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip); err != nil { + if err := regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip.String()); err != nil { return fmt.Errorf("set %s: %w", dnsPolicyConfigGenericDNSServersKey, err) } diff --git a/client/internal/dns/mock_server.go b/client/internal/dns/mock_server.go index c5dd6e23f..40a2e7384 100644 --- a/client/internal/dns/mock_server.go +++ b/client/internal/dns/mock_server.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + "net/netip" "github.com/miekg/dns" @@ -45,8 +46,8 @@ func (m *MockServer) Stop() { } } -func (m *MockServer) DnsIP() string { - return "" +func (m *MockServer) DnsIP() netip.Addr { + return netip.MustParseAddr("100.10.254.255") } func (m *MockServer) OnUpdatedHostDNSServer(strings []string) { diff --git a/client/internal/dns/network_manager_unix.go b/client/internal/dns/network_manager_unix.go index caae63a24..5459bc2d7 100644 --- a/client/internal/dns/network_manager_unix.go +++ b/client/internal/dns/network_manager_unix.go @@ -110,11 +110,7 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig, st connSettings.cleanDeprecatedSettings() - dnsIP, err := netip.ParseAddr(config.ServerIP) - if err != nil { - return fmt.Errorf("unable to parse ip address, error: %w", err) - } - convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice()) + convDNSIP := binary.LittleEndian.Uint32(config.ServerIP.AsSlice()) connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP}) var ( searchDomains []string diff --git a/client/internal/dns/resolvconf_unix.go b/client/internal/dns/resolvconf_unix.go index 54c4c75bf..6080c1d2c 100644 --- a/client/internal/dns/resolvconf_unix.go +++ b/client/internal/dns/resolvconf_unix.go @@ -46,9 +46,9 @@ type resolvconf struct { func detectResolvconfType() (resolvconfType, error) { cmd := exec.Command(resolvconfCommand, "--version") - out, err := cmd.Output() + out, err := cmd.CombinedOutput() if err != nil { - return typeOpenresolv, fmt.Errorf("failed to determine resolvconf type: %w", err) + return typeOpenresolv, fmt.Errorf("determine resolvconf type: %w", err) } if strings.Contains(string(out), "openresolv") { @@ -66,7 +66,7 @@ func newResolvConfConfigurator(wgInterface string) (*resolvconf, error) { implType, err := detectResolvconfType() if err != nil { log.Warnf("failed to detect resolvconf type, defaulting to openresolv: %v", err) - implType = typeOpenresolv + implType = typeResolvconf } else { log.Infof("detected resolvconf type: %v", implType) } @@ -85,24 +85,14 @@ func (r *resolvconf) supportCustomPort() bool { } func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { - var err error - if !config.RouteAll { - err = r.restoreHostDNS() - if err != nil { - log.Errorf("restore host dns: %s", err) - } - return ErrRouteAllWithoutNameserverGroup - } - searchDomainList := searchDomains(config) searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains) - options := prepareOptionsWithTimeout(r.othersConfigs, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts) - buf := prepareResolvConfContent( searchDomainList, - append([]string{config.ServerIP}, r.originalNameServers...), - options) + []string{config.ServerIP.String()}, + r.othersConfigs, + ) state := &ShutdownState{ ManagerType: resolvConfManager, @@ -112,8 +102,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman log.Errorf("failed to update shutdown state: %s", err) } - err = r.applyConfig(buf) - if err != nil { + if err := r.applyConfig(buf); err != nil { return fmt.Errorf("apply config: %w", err) } @@ -121,6 +110,10 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman return nil } +func (r *resolvconf) getOriginalNameservers() []string { + return r.originalNameServers +} + func (r *resolvconf) restoreHostDNS() error { var cmd *exec.Cmd @@ -157,7 +150,7 @@ func (r *resolvconf) applyConfig(content bytes.Buffer) error { } cmd.Stdin = &content - out, err := cmd.Output() + out, err := cmd.CombinedOutput() log.Tracef("resolvconf output: %s", out) if err != nil { return fmt.Errorf("applying resolvconf configuration for %s interface: %w", r.ifaceName, err) diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index e81aebf98..f933c1de0 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -2,7 +2,6 @@ package dns import ( "context" - "errors" "fmt" "net/netip" "runtime" @@ -20,7 +19,6 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" - cProto "github.com/netbirdio/netbird/client/proto" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/domain" ) @@ -41,7 +39,7 @@ type Server interface { DeregisterHandler(domains domain.List, priority int) Initialize() error Stop() - DnsIP() string + DnsIP() netip.Addr UpdateDNSServer(serial uint64, update nbdns.Config) error OnUpdatedHostDNSServer(strings []string) SearchDomains() []string @@ -53,6 +51,12 @@ type nsGroupsByDomain struct { groups []*nbdns.NameServerGroup } +// hostManagerWithOriginalNS extends the basic hostManager interface +type hostManagerWithOriginalNS interface { + hostManager + getOriginalNameservers() []string +} + // DefaultServer dns server object type DefaultServer struct { ctx context.Context @@ -215,6 +219,7 @@ func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, p log.Warn("skipping empty domain") continue } + s.handlerChain.AddHandler(domain, handler, priority) } } @@ -286,7 +291,7 @@ func (s *DefaultServer) Initialize() (err error) { // // When kernel space interface used it return real DNS server listener IP address // For bind interface, fake DNS resolver address returned (second last IP address from Nebird network) -func (s *DefaultServer) DnsIP() string { +func (s *DefaultServer) DnsIP() netip.Addr { return s.service.RuntimeIP() } @@ -297,6 +302,11 @@ func (s *DefaultServer) Stop() { s.ctxCancel() if s.hostManager != nil { + if srvs, ok := s.hostManager.(hostManagerWithOriginalNS); ok && len(srvs.getOriginalNameservers()) > 0 { + log.Debugf("deregistering original nameservers as fallback handlers") + s.deregisterHandler([]string{nbdns.RootZone}, PriorityFallback) + } + if err := s.hostManager.restoreHostDNS(); err != nil { log.Error("failed to restore host DNS settings: ", err) } else if err := s.stateManager.DeleteState(&ShutdownState{}); err != nil { @@ -311,7 +321,6 @@ func (s *DefaultServer) Stop() { // OnUpdatedHostDNSServer update the DNS servers addresses for root zones // It will be applied if the mgm server do not enforce DNS settings for root zone - func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { s.hostsDNSHolder.set(hostsDnsList) @@ -493,25 +502,56 @@ func (s *DefaultServer) applyHostConfig() { if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil { log.Errorf("failed to apply DNS host manager update: %v", err) - s.handleErrNoGroupaAll(err) } + + s.registerFallback(config) } -func (s *DefaultServer) handleErrNoGroupaAll(err error) { - if !errors.Is(ErrRouteAllWithoutNameserverGroup, err) { +// registerFallback registers original nameservers as low-priority fallback handlers +func (s *DefaultServer) registerFallback(config HostDNSConfig) { + hostMgrWithNS, ok := s.hostManager.(hostManagerWithOriginalNS) + if !ok { return } - if s.statusRecorder == nil { + originalNameservers := hostMgrWithNS.getOriginalNameservers() + if len(originalNameservers) == 0 { return } - s.statusRecorder.PublishEvent( - cProto.SystemEvent_WARNING, cProto.SystemEvent_DNS, - "The host dns manager does not support match domains", - "The host dns manager does not support match domains without a catch-all nameserver group.", - map[string]string{"manager": s.hostManager.string()}, + log.Infof("registering original nameservers %v as upstream handlers with priority %d", originalNameservers, PriorityFallback) + + handler, err := newUpstreamResolver( + s.ctx, + s.wgInterface.Name(), + s.wgInterface.Address().IP, + s.wgInterface.Address().Network, + s.statusRecorder, + s.hostsDNSHolder, + nbdns.RootZone, ) + if err != nil { + log.Errorf("failed to create upstream resolver for original nameservers: %v", err) + return + } + + for _, ns := range originalNameservers { + if ns == config.ServerIP.String() { + log.Debugf("skipping original nameserver %s as it is the same as the server IP %s", ns, config.ServerIP) + continue + } + + ns = fmt.Sprintf("%s:%d", ns, defaultPort) + if ip, err := netip.ParseAddr(ns); err == nil && ip.Is6() { + ns = fmt.Sprintf("[%s]:%d", ns, defaultPort) + } + + handler.upstreamServers = append(handler.upstreamServers, ns) + } + handler.deactivate = func(error) { /* always active */ } + handler.reactivate = func() { /* always active */ } + + s.registerHandler([]string{nbdns.RootZone}, handler, PriorityFallback) } func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, []nbdns.SimpleRecord, error) { @@ -588,14 +628,8 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai // Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts priority := basePriority - i - // Check if we're about to overlap with the next priority tier. - // This boundary check ensures that the priority of upstream handlers does not conflict - // with the default priority tier. By decrementing the priority for each handler, we avoid - // overlaps, but if the calculated priority falls into the default tier, we skip the remaining - // handlers to maintain the integrity of the priority system. - if basePriority == PriorityUpstream && priority <= PriorityDefault { - log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers", - domainGroup.domain, PriorityUpstream-PriorityDefault) + // Check if we're about to overlap with the next priority tier + if s.leaksPriority(domainGroup, basePriority, priority) { break } @@ -648,6 +682,21 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai return muxUpdates, nil } +func (s *DefaultServer) leaksPriority(domainGroup nsGroupsByDomain, basePriority int, priority int) bool { + if basePriority == PriorityUpstream && priority <= PriorityDefault { + log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers", + domainGroup.domain, PriorityUpstream-PriorityDefault) + return true + } + if basePriority == PriorityDefault && priority <= PriorityFallback { + log.Warnf("too many handlers for domain=%s, would overlap with fallback priority tier (diff=%d). Skipping remaining handlers", + domainGroup.domain, PriorityDefault-PriorityFallback) + return true + } + + return false +} + func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) { // this will introduce a short period of time when the server is not able to handle DNS requests for _, existing := range s.dnsMuxMap { @@ -760,6 +809,12 @@ func (s *DefaultServer) upstreamCallbacks( } func (s *DefaultServer) addHostRootZone() { + hostDNSServers := s.hostsDNSHolder.get() + if len(hostDNSServers) == 0 { + log.Debug("no host DNS servers available, skipping root zone handler creation") + return + } + handler, err := newUpstreamResolver( s.ctx, s.wgInterface.Name(), @@ -775,7 +830,7 @@ func (s *DefaultServer) addHostRootZone() { } handler.upstreamServers = make([]string, 0) - for k := range s.hostsDNSHolder.get() { + for k := range hostDNSServers { handler.upstreamServers = append(handler.upstreamServers, k) } handler.deactivate = func(error) {} diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 21a9e2f2d..3cab4517a 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -938,7 +938,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { return wgIface, nil } -func newDnsResolver(ip string, port int) *net.Resolver { +func newDnsResolver(ip netip.Addr, port int) *net.Resolver { return &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { @@ -1047,7 +1047,7 @@ type mockService struct{} func (m *mockService) Listen() error { return nil } func (m *mockService) Stop() {} -func (m *mockService) RuntimeIP() string { return "127.0.0.1" } +func (m *mockService) RuntimeIP() netip.Addr { return netip.MustParseAddr("127.0.0.1") } func (m *mockService) RuntimePort() int { return 53 } func (m *mockService) RegisterMux(string, dns.Handler) {} func (m *mockService) DeregisterMux(string) {} diff --git a/client/internal/dns/service.go b/client/internal/dns/service.go index 523976e54..ab8238a61 100644 --- a/client/internal/dns/service.go +++ b/client/internal/dns/service.go @@ -1,6 +1,8 @@ package dns import ( + "net/netip" + "github.com/miekg/dns" ) @@ -14,5 +16,5 @@ type service interface { RegisterMux(domain string, handler dns.Handler) DeregisterMux(key string) RuntimePort() int - RuntimeIP() string + RuntimeIP() netip.Addr } diff --git a/client/internal/dns/service_listener.go b/client/internal/dns/service_listener.go index 72dc4bc6e..abd2f4f05 100644 --- a/client/internal/dns/service_listener.go +++ b/client/internal/dns/service_listener.go @@ -18,8 +18,11 @@ import ( const ( customPort = 5053 - defaultIP = "127.0.0.1" - customIP = "127.0.0.153" +) + +var ( + defaultIP = netip.MustParseAddr("127.0.0.1") + customIP = netip.MustParseAddr("127.0.0.153") ) type serviceViaListener struct { @@ -27,7 +30,7 @@ type serviceViaListener struct { dnsMux *dns.ServeMux customAddr *netip.AddrPort server *dns.Server - listenIP string + listenIP netip.Addr listenPort uint16 listenerIsRunning bool listenerFlagLock sync.Mutex @@ -65,6 +68,7 @@ func (s *serviceViaListener) Listen() error { log.Errorf("failed to eval runtime address: %s", err) return fmt.Errorf("eval listen address: %w", err) } + s.listenIP = s.listenIP.Unmap() s.server.Addr = fmt.Sprintf("%s:%d", s.listenIP, s.listenPort) log.Debugf("starting dns on %s", s.server.Addr) go func() { @@ -124,7 +128,7 @@ func (s *serviceViaListener) RuntimePort() int { } } -func (s *serviceViaListener) RuntimeIP() string { +func (s *serviceViaListener) RuntimeIP() netip.Addr { return s.listenIP } @@ -139,9 +143,9 @@ func (s *serviceViaListener) setListenerStatus(running bool) { // first check the 53 port availability on WG interface or lo, if not success // pick a random port on WG interface for eBPF, if not success // check the 5053 port availability on WG interface or lo without eBPF usage, -func (s *serviceViaListener) evalListenAddress() (string, uint16, error) { +func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) { if s.customAddr != nil { - return s.customAddr.Addr().String(), s.customAddr.Port(), nil + return s.customAddr.Addr(), s.customAddr.Port(), nil } ip, ok := s.testFreePort(defaultPort) @@ -152,7 +156,7 @@ func (s *serviceViaListener) evalListenAddress() (string, uint16, error) { ebpfSrv, port, ok := s.tryToUseeBPF() if ok { s.ebpfService = ebpfSrv - return s.wgInterface.Address().IP.String(), port, nil + return s.wgInterface.Address().IP, port, nil } ip, ok = s.testFreePort(customPort) @@ -160,15 +164,15 @@ func (s *serviceViaListener) evalListenAddress() (string, uint16, error) { return ip, customPort, nil } - return "", 0, fmt.Errorf("failed to find a free port for DNS server") + return netip.Addr{}, 0, fmt.Errorf("failed to find a free port for DNS server") } -func (s *serviceViaListener) testFreePort(port int) (string, bool) { - var ips []string +func (s *serviceViaListener) testFreePort(port int) (netip.Addr, bool) { + var ips []netip.Addr if runtime.GOOS != "darwin" { - ips = []string{s.wgInterface.Address().IP.String(), defaultIP, customIP} + ips = []netip.Addr{s.wgInterface.Address().IP, defaultIP, customIP} } else { - ips = []string{defaultIP, customIP} + ips = []netip.Addr{defaultIP, customIP} } for _, ip := range ips { @@ -178,10 +182,10 @@ func (s *serviceViaListener) testFreePort(port int) (string, bool) { return ip, true } - return "", false + return netip.Addr{}, false } -func (s *serviceViaListener) tryToBind(ip string, port int) bool { +func (s *serviceViaListener) tryToBind(ip netip.Addr, port int) bool { addrString := fmt.Sprintf("%s:%d", ip, port) udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString)) probeListener, err := net.ListenUDP("udp", udpAddr) @@ -224,7 +228,7 @@ func (s *serviceViaListener) tryToUseeBPF() (ebpfMgr.Manager, uint16, bool) { } func (s *serviceViaListener) generateFreePort() (uint16, error) { - ok := s.tryToBind(s.wgInterface.Address().IP.String(), customPort) + ok := s.tryToBind(s.wgInterface.Address().IP, customPort) if ok { return customPort, nil } diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 226202cf7..9f55838bf 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -16,7 +16,7 @@ import ( type ServiceViaMemory struct { wgInterface WGIface dnsMux *dns.ServeMux - runtimeIP string + runtimeIP netip.Addr runtimePort int udpFilterHookID string listenerIsRunning bool @@ -32,7 +32,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory { wgInterface: wgIface, dnsMux: dns.NewServeMux(), - runtimeIP: lastIP.String(), + runtimeIP: lastIP, runtimePort: defaultPort, } return s @@ -84,7 +84,7 @@ func (s *ServiceViaMemory) RuntimePort() int { return s.runtimePort } -func (s *ServiceViaMemory) RuntimeIP() string { +func (s *ServiceViaMemory) RuntimeIP() netip.Addr { return s.runtimeIP } @@ -121,10 +121,5 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) { return true } - ip, err := netip.ParseAddr(s.runtimeIP) - if err != nil { - return "", fmt.Errorf("parse runtime ip: %w", err) - } - - return filter.AddUDPPacketHook(false, ip, uint16(s.runtimePort), hook), nil + return filter.AddUDPPacketHook(false, s.runtimeIP, uint16(s.runtimePort), hook), nil } diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index 9040ed787..a58747d5b 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -89,21 +89,16 @@ func (s *systemdDbusConfigurator) supportCustomPort() bool { } func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { - parsedIP, err := netip.ParseAddr(config.ServerIP) - if err != nil { - return fmt.Errorf("unable to parse ip address, error: %w", err) - } - ipAs4 := parsedIP.As4() defaultLinkInput := systemdDbusDNSInput{ Family: unix.AF_INET, - Address: ipAs4[:], + Address: config.ServerIP.AsSlice(), } - if err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil { + if err := s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil { return fmt.Errorf("set interface DNS server %s:%d: %w", config.ServerIP, config.ServerPort, err) } // We don't support dnssec. On some machines this is default on so we explicitly set it to off - if err = s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil { + if err := s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil { log.Warnf("failed to set DNSSEC to 'no': %v", err) } @@ -129,8 +124,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana } if config.RouteAll { - err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true) - if err != nil { + if err := s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true); err != nil { return fmt.Errorf("set link as default dns router: %w", err) } domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{ @@ -139,7 +133,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana }) log.Infof("configured %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort) } else { - if err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, false); err != nil { + if err := s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, false); err != nil { return fmt.Errorf("remove link as default dns router: %w", err) } } @@ -153,9 +147,8 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana } log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains) - err = s.setDomainsForInterface(domainsInput) - if err != nil { - log.Error(err) + if err := s.setDomainsForInterface(domainsInput); err != nil { + log.Error("failed to set domains for interface: ", err) } if err := s.flushDNSCache(); err != nil { diff --git a/client/internal/dns/unclean_shutdown_unix.go b/client/internal/dns/unclean_shutdown_unix.go index fcf60c694..2e786f484 100644 --- a/client/internal/dns/unclean_shutdown_unix.go +++ b/client/internal/dns/unclean_shutdown_unix.go @@ -35,12 +35,7 @@ func (s *ShutdownState) Cleanup() error { } // TODO: move file contents to state manager -func createUncleanShutdownIndicator(sourcePath string, dnsAddressStr string, stateManager *statemanager.Manager) error { - dnsAddress, err := netip.ParseAddr(dnsAddressStr) - if err != nil { - return fmt.Errorf("parse dns address %s: %w", dnsAddressStr, err) - } - +func createUncleanShutdownIndicator(sourcePath string, dnsAddress netip.Addr, stateManager *statemanager.Manager) error { dir := filepath.Dir(fileUncleanShutdownResolvConfLocation) if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return fmt.Errorf("create dir %s: %w", dir, err) diff --git a/client/internal/engine.go b/client/internal/engine.go index 079adf7e8..d2de5b3cc 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1550,7 +1550,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) { func (e *Engine) wgInterfaceCreate() (err error) { switch runtime.GOOS { case "android": - err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP(), e.dnsServer.SearchDomains()) + err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP().String(), e.dnsServer.SearchDomains()) case "ios": e.mobileDep.NetworkChangeListener.SetInterfaceIP(e.config.WgAddr) err = e.wgInterface.Create() From 31872a7fb62b8257037a894daabcf5de44f5c4aa Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 25 Jul 2025 14:14:45 +0200 Subject: [PATCH 175/213] [client] Fix UDP proxy to notify listener when remote conn closed (#4199) * Fix UDP proxy to notify listener when remote conn closed * Fix sender tests to use t.Errorf for timeout assertions * Fix potential nil pointer --- client/iface/wgproxy/listener/listener.go | 17 +++++++++++++++-- client/iface/wgproxy/udp/proxy.go | 5 +++++ relay/healthcheck/sender_test.go | 11 +++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/client/iface/wgproxy/listener/listener.go b/client/iface/wgproxy/listener/listener.go index bfd651548..a8ee354a1 100644 --- a/client/iface/wgproxy/listener/listener.go +++ b/client/iface/wgproxy/listener/listener.go @@ -1,7 +1,10 @@ package listener +import "sync" + type CloseListener struct { listener func() + mu sync.Mutex } func NewCloseListener() *CloseListener { @@ -9,11 +12,21 @@ func NewCloseListener() *CloseListener { } func (c *CloseListener) SetCloseListener(listener func()) { + c.mu.Lock() + defer c.mu.Unlock() + c.listener = listener } func (c *CloseListener) Notify() { - if c.listener != nil { - c.listener() + c.mu.Lock() + + if c.listener == nil { + c.mu.Unlock() + return } + listener := c.listener + c.mu.Unlock() + + listener() } diff --git a/client/iface/wgproxy/udp/proxy.go b/client/iface/wgproxy/udp/proxy.go index df45d8ca5..139ccd4ed 100644 --- a/client/iface/wgproxy/udp/proxy.go +++ b/client/iface/wgproxy/udp/proxy.go @@ -183,6 +183,11 @@ func (p *WGUDPProxy) proxyToLocal(ctx context.Context) { for { n, err := p.remoteConnRead(ctx, buf) if err != nil { + if ctx.Err() != nil { + return + } + + p.closeListener.Notify() return } diff --git a/relay/healthcheck/sender_test.go b/relay/healthcheck/sender_test.go index 39d266b48..23446366a 100644 --- a/relay/healthcheck/sender_test.go +++ b/relay/healthcheck/sender_test.go @@ -122,10 +122,6 @@ func TestSenderHealthCheckAttemptThreshold(t *testing.T) { originalTimeout := healthCheckTimeout healthCheckInterval = 1 * time.Second healthCheckTimeout = 500 * time.Millisecond - defer func() { - healthCheckInterval = originalInterval - healthCheckTimeout = originalTimeout - }() //nolint:tenv os.Setenv(defaultAttemptThresholdEnv, fmt.Sprintf("%d", tc.threshold)) @@ -164,20 +160,23 @@ func TestSenderHealthCheckAttemptThreshold(t *testing.T) { select { case <-sender.Timeout: if tc.resetCounterOnce { - t.Fatalf("should not have timed out before %s", testTimeout) + t.Errorf("should not have timed out before %s", testTimeout) } case <-time.After(testTimeout): if tc.resetCounterOnce { return } - t.Fatalf("should have timed out before %s", testTimeout) + t.Errorf("should have timed out before %s", testTimeout) } + cancel() select { case <-senderExit: case <-time.After(2 * time.Second): t.Fatalf("sender did not exit in time") } + healthCheckInterval = originalInterval + healthCheckTimeout = originalTimeout }) } From 2c4ac33b381e352fefe401e94f0416e158db8b71 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:15:38 +0200 Subject: [PATCH 176/213] [client] Remove and deprecate the admin url functionality (#4218) --- client/cmd/login.go | 1 - client/cmd/root.go | 3 ++- client/cmd/up.go | 2 -- client/ui/client_ui.go | 14 +------------- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/client/cmd/login.go b/client/cmd/login.go index 8ac7086b8..f3a2f0cca 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -61,7 +61,6 @@ var loginCmd = &cobra.Command{ ic := internal.ConfigInput{ ManagementURL: managementURL, - AdminURL: adminURL, ConfigPath: configPath, } if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { diff --git a/client/cmd/root.go b/client/cmd/root.go index 1774602c4..e4f260f9b 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -118,7 +118,8 @@ func init() { rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL)) - rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) + rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("(DEPRECATED) Admin Panel URL [http|https]://[host]:[port] (default \"%s\") - This flag is no longer functional", internal.DefaultAdminURL)) + _ = rootCmd.PersistentFlags().MarkDeprecated("admin-url", "the admin-url flag is no longer functional and will be removed in a future version") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character") diff --git a/client/cmd/up.go b/client/cmd/up.go index 529beeac7..66fe91f7d 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -238,7 +238,6 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) { ic := internal.ConfigInput{ ManagementURL: managementURL, - AdminURL: adminURL, ConfigPath: configPath, NATExternalIPs: natExternalIPs, CustomDNSAddress: customDNSAddressConverted, @@ -325,7 +324,6 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte loginRequest := proto.LoginRequest{ SetupKey: providedSetupKey, ManagementUrl: managementURL, - AdminURL: adminURL, NatExternalIPs: natExternalIPs, CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0, CustomDNSAddress: customDNSAddressConverted, diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 4480adb51..c18d96dae 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -214,7 +214,6 @@ type serviceClient struct { // input elements for settings form iMngURL *widget.Entry - iAdminURL *widget.Entry iConfigFile *widget.Entry iLogFile *widget.Entry iPreSharedKey *widget.Entry @@ -232,7 +231,6 @@ type serviceClient struct { // observable settings over corresponding iMngURL and iPreSharedKey values. managementURL string preSharedKey string - adminURL string RosenpassPermissive bool interfaceName string interfacePort int @@ -344,7 +342,6 @@ func (s *serviceClient) showSettingsUI() { s.wSettings.SetOnClosed(s.cancel) s.iMngURL = widget.NewEntry() - s.iAdminURL = widget.NewEntry() s.iConfigFile = widget.NewEntry() s.iConfigFile.Disable() s.iLogFile = widget.NewEntry() @@ -377,7 +374,6 @@ func (s *serviceClient) getSettingsForm() *widget.Form { {Text: "Interface Name", Widget: s.iInterfaceName}, {Text: "Interface Port", Widget: s.iInterfacePort}, {Text: "Management URL", Widget: s.iMngURL}, - {Text: "Admin URL", Widget: s.iAdminURL}, {Text: "Pre-shared Key", Widget: s.iPreSharedKey}, {Text: "Config File", Widget: s.iConfigFile}, {Text: "Log File", Widget: s.iLogFile}, @@ -403,14 +399,13 @@ func (s *serviceClient) getSettingsForm() *widget.Form { return } - iAdminURL := strings.TrimSpace(s.iAdminURL.Text) iMngURL := strings.TrimSpace(s.iMngURL.Text) defer s.wSettings.Close() // Check if any settings have changed if s.managementURL != iMngURL || s.preSharedKey != s.iPreSharedKey.Text || - s.adminURL != iAdminURL || s.RosenpassPermissive != s.sRosenpassPermissive.Checked || + s.RosenpassPermissive != s.sRosenpassPermissive.Checked || s.interfaceName != s.iInterfaceName.Text || s.interfacePort != int(port) || s.networkMonitor != s.sNetworkMonitor.Checked || s.disableDNS != s.sDisableDNS.Checked || @@ -420,11 +415,9 @@ func (s *serviceClient) getSettingsForm() *widget.Form { s.managementURL = iMngURL s.preSharedKey = s.iPreSharedKey.Text - s.adminURL = iAdminURL loginRequest := proto.LoginRequest{ ManagementUrl: iMngURL, - AdminURL: iAdminURL, IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", RosenpassPermissive: &s.sRosenpassPermissive.Checked, InterfaceName: &s.iInterfaceName.Text, @@ -798,7 +791,6 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService // getSrvConfig from the service to show it in the settings window. func (s *serviceClient) getSrvConfig() { s.managementURL = internal.DefaultManagementURL - s.adminURL = internal.DefaultAdminURL conn, err := s.getSrvClient(failFastTimeout) if err != nil { @@ -815,9 +807,6 @@ func (s *serviceClient) getSrvConfig() { if cfg.ManagementUrl != "" { s.managementURL = cfg.ManagementUrl } - if cfg.AdminURL != "" { - s.adminURL = cfg.AdminURL - } s.preSharedKey = cfg.PreSharedKey s.RosenpassPermissive = cfg.RosenpassPermissive s.interfaceName = cfg.InterfaceName @@ -831,7 +820,6 @@ func (s *serviceClient) getSrvConfig() { if s.showAdvancedSettings { s.iMngURL.SetText(s.managementURL) - s.iAdminURL.SetText(s.adminURL) s.iConfigFile.SetText(cfg.ConfigFile) s.iLogFile.SetText(cfg.LogFile) s.iPreSharedKey.SetText(cfg.PreSharedKey) From e0d9306b05101716eb2fed795759b40ecf8fce6a Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:31:06 +0200 Subject: [PATCH 177/213] [client] Add detailed routes and resolved IPs to debug bundle (#4141) --- client/internal/debug/debug.go | 203 ++++----- client/internal/debug/debug_linux.go | 35 +- client/internal/debug/debug_nonlinux.go | 5 + client/internal/debug/debug_nonmobile.go | 8 +- client/internal/debug/format.go | 206 +++++++++ client/internal/debug/format_linux.go | 185 ++++++++ client/internal/debug/format_nonwindows.go | 27 ++ client/internal/debug/format_windows.go | 37 ++ .../routemanager/systemops/routeflags_bsd.go | 54 ++- .../systemops/routeflags_freebsd.go | 55 ++- .../routemanager/systemops/systemops.go | 20 + .../routemanager/systemops/systemops_bsd.go | 131 +++++- .../routemanager/systemops/systemops_linux.go | 410 +++++++++++++++++- .../systemops/systemops_nonlinux.go | 25 ++ .../systemops/systemops_windows.go | 265 ++++++++++- 15 files changed, 1501 insertions(+), 165 deletions(-) create mode 100644 client/internal/debug/format.go create mode 100644 client/internal/debug/format_linux.go create mode 100644 client/internal/debug/format_nonwindows.go create mode 100644 client/internal/debug/format_windows.go diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 220e6854d..a9d9f3fc1 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -40,10 +40,12 @@ status.txt: Anonymized status information of the NetBird client. client.log: Most recent, anonymized client log file of the NetBird client. netbird.err: Most recent, anonymized stderr log file of the NetBird client. netbird.out: Most recent, anonymized stdout log file of the NetBird client. -routes.txt: Anonymized system routes, if --system-info flag was provided. +routes.txt: Detailed system routing table in tabular format including destination, gateway, interface, metrics, and protocol information, if --system-info flag was provided. interfaces.txt: Anonymized network interface information, if --system-info flag was provided. +ip_rules.txt: Detailed IP routing rules in tabular format including priority, source, destination, interfaces, table, and action information (Linux only), if --system-info flag was provided. iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided. nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided. +resolved_domains.txt: Anonymized resolved domain IP addresses from the status recorder. config.txt: Anonymized configuration information of the NetBird client. network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules. state.json: Anonymized client state dump containing netbird states. @@ -107,7 +109,29 @@ go tool pprof -http=:8088 heap.prof This will open a web browser tab with the profiling information. Routes -For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. +The routes.txt file contains detailed routing table information in a tabular format: + +- Destination: Network prefix (IP_ADDRESS/PREFIX_LENGTH) +- Gateway: Next hop IP address (or "-" if direct) +- Interface: Network interface name +- Metric: Route priority/metric (lower values preferred) +- Protocol: Routing protocol (kernel, static, dhcp, etc.) +- Scope: Route scope (global, link, host, etc.) +- Type: Route type (unicast, local, broadcast, etc.) +- Table: Routing table name (main, local, netbird, etc.) + +The table format provides a comprehensive view of the system's routing configuration, including information from multiple routing tables on Linux systems. This is valuable for troubleshooting routing issues and understanding traffic flow. + +For anonymized routes, IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. Interface names are anonymized using string anonymization. + +Resolved Domains +The resolved_domains.txt file contains information about domain names that have been resolved to IP addresses by NetBird's DNS resolver. This includes: +- Original domain patterns that were configured for routing +- Resolved domain names that matched those patterns +- IP address prefixes that were resolved for each domain +- Parent domain associations showing which original pattern each resolved domain belongs to + +All domain names and IP addresses in this file follow the same anonymization rules as described above. This information is valuable for troubleshooting DNS resolution and routing issues. Network Interfaces The interfaces.txt file contains information about network interfaces, including: @@ -145,6 +169,22 @@ nftables.txt: - Shows packet and byte counters for each rule - All IP addresses are anonymized - Chain names, table names, and other non-sensitive information remain unchanged + +IP Rules (Linux only) +The ip_rules.txt file contains detailed IP routing rule information: + +- Priority: Rule priority number (lower values processed first) +- From: Source IP prefix or "all" if unspecified +- To: Destination IP prefix or "all" if unspecified +- IIF: Input interface name or "-" if unspecified +- OIF: Output interface name or "-" if unspecified +- Table: Target routing table name (main, local, netbird, etc.) +- Action: Rule action (lookup, goto, blackhole, etc.) +- Mark: Firewall mark value in hex format or "-" if unspecified + +The table format provides comprehensive visibility into the IP routing decision process, including how traffic is directed to different routing tables based on various criteria. This is valuable for troubleshooting advanced routing configurations and policy-based routing. + +For anonymized rules, IP addresses and prefixes are replaced as described above. Interface names are anonymized using string anonymization. Table names, actions, and other non-sensitive information remain unchanged. ` const ( @@ -159,13 +199,11 @@ const ( type BundleGenerator struct { anonymizer *anonymize.Anonymizer - // deps internalConfig *internal.Config statusRecorder *peer.Status networkMap *mgmProto.NetworkMap logFile string - // config anonymize bool clientStatus string includeSystemInfo bool @@ -258,7 +296,11 @@ func (g *BundleGenerator) createArchive() error { } if err := g.addConfig(); err != nil { - log.Errorf("Failed to add config to debug bundle: %v", err) + log.Errorf("failed to add config to debug bundle: %v", err) + } + + if err := g.addResolvedDomains(); err != nil { + log.Errorf("failed to add resolved domains to debug bundle: %v", err) } if g.includeSystemInfo { @@ -266,7 +308,7 @@ func (g *BundleGenerator) createArchive() error { } if err := g.addProf(); err != nil { - log.Errorf("Failed to add profiles to debug bundle: %v", err) + log.Errorf("failed to add profiles to debug bundle: %v", err) } if err := g.addNetworkMap(); err != nil { @@ -274,26 +316,26 @@ func (g *BundleGenerator) createArchive() error { } if err := g.addStateFile(); err != nil { - log.Errorf("Failed to add state file to debug bundle: %v", err) + log.Errorf("failed to add state file to debug bundle: %v", err) } if err := g.addCorruptedStateFiles(); err != nil { - log.Errorf("Failed to add corrupted state files to debug bundle: %v", err) + log.Errorf("failed to add corrupted state files to debug bundle: %v", err) } if err := g.addWgShow(); err != nil { - log.Errorf("Failed to add wg show output: %v", err) + log.Errorf("failed to add wg show output: %v", err) } if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) { if err := g.addLogfile(); err != nil { - log.Errorf("Failed to add log file to debug bundle: %v", err) + log.Errorf("failed to add log file to debug bundle: %v", err) if err := g.trySystemdLogFallback(); err != nil { - log.Errorf("Failed to add systemd logs as fallback: %v", err) + log.Errorf("failed to add systemd logs as fallback: %v", err) } } } else if err := g.trySystemdLogFallback(); err != nil { - log.Errorf("Failed to add systemd logs: %v", err) + log.Errorf("failed to add systemd logs: %v", err) } return nil @@ -301,15 +343,19 @@ func (g *BundleGenerator) createArchive() error { func (g *BundleGenerator) addSystemInfo() { if err := g.addRoutes(); err != nil { - log.Errorf("Failed to add routes to debug bundle: %v", err) + log.Errorf("failed to add routes to debug bundle: %v", err) } if err := g.addInterfaces(); err != nil { - log.Errorf("Failed to add interfaces to debug bundle: %v", err) + log.Errorf("failed to add interfaces to debug bundle: %v", err) + } + + if err := g.addIPRules(); err != nil { + log.Errorf("failed to add IP rules to debug bundle: %v", err) } if err := g.addFirewallRules(); err != nil { - log.Errorf("Failed to add firewall rules to debug bundle: %v", err) + log.Errorf("failed to add firewall rules to debug bundle: %v", err) } } @@ -364,7 +410,6 @@ func (g *BundleGenerator) addConfig() error { } } - // Add config content to zip file configReader := strings.NewReader(configContent.String()) if err := g.addFileToZip(configReader, "config.txt"); err != nil { return fmt.Errorf("add config file to zip: %w", err) @@ -376,7 +421,6 @@ func (g *BundleGenerator) addConfig() error { func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) { configContent.WriteString("NetBird Client Configuration:\n\n") - // Add non-sensitive fields configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface)) configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort)) if g.internalConfig.NetworkMonitor != nil { @@ -461,6 +505,27 @@ func (g *BundleGenerator) addInterfaces() error { return nil } +func (g *BundleGenerator) addResolvedDomains() error { + if g.statusRecorder == nil { + log.Debugf("skipping resolved domains in debug bundle: no status recorder") + return nil + } + + resolvedDomains := g.statusRecorder.GetResolvedDomainsStates() + if len(resolvedDomains) == 0 { + log.Debugf("skipping resolved domains in debug bundle: no resolved domains") + return nil + } + + resolvedDomainsContent := formatResolvedDomains(resolvedDomains, g.anonymize, g.anonymizer) + resolvedDomainsReader := strings.NewReader(resolvedDomainsContent) + if err := g.addFileToZip(resolvedDomainsReader, "resolved_domains.txt"); err != nil { + return fmt.Errorf("add resolved domains file to zip: %w", err) + } + + return nil +} + func (g *BundleGenerator) addNetworkMap() error { if g.networkMap == nil { log.Debugf("skipping empty network map in debug bundle") @@ -572,7 +637,6 @@ func (g *BundleGenerator) addLogfile() error { return fmt.Errorf("add client log file to zip: %w", err) } - // add rotated log files based on logFileCount g.addRotatedLogFiles(logDir) stdErrLogPath := filepath.Join(logDir, errorLogFile) @@ -601,7 +665,7 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error { } defer func() { if err := logFile.Close(); err != nil { - log.Errorf("Failed to close log file %s: %v", targetName, err) + log.Errorf("failed to close log file %s: %v", targetName, err) } }() @@ -625,13 +689,21 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error { if err != nil { return fmt.Errorf("open gz log file %s: %w", targetName, err) } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + log.Errorf("failed to close gz file %s: %v", targetName, err) + } + }() gzr, err := gzip.NewReader(f) if err != nil { return fmt.Errorf("create gzip reader: %w", err) } - defer gzr.Close() + defer func() { + if err := gzr.Close(); err != nil { + log.Errorf("failed to close gzip reader %s: %v", targetName, err) + } + }() var logReader io.Reader = gzr if g.anonymize { @@ -689,7 +761,6 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) { return fi.ModTime().After(fj.ModTime()) }) - // include up to logFileCount rotated files maxFiles := int(g.logFileCount) if maxFiles > len(files) { maxFiles = len(files) @@ -717,7 +788,7 @@ func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error // If the reader is a file, we can get more accurate information if f, ok := reader.(*os.File); ok { if stat, err := f.Stat(); err != nil { - log.Tracef("Failed to get file stat for %s: %v", filename, err) + log.Tracef("failed to get file stat for %s: %v", filename, err) } else { header.Modified = stat.ModTime() } @@ -765,89 +836,6 @@ func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) { } } -func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string { - var ipv4Routes, ipv6Routes []netip.Prefix - - // Separate IPv4 and IPv6 routes - for _, route := range routes { - if route.Addr().Is4() { - ipv4Routes = append(ipv4Routes, route) - } else { - ipv6Routes = append(ipv6Routes, route) - } - } - - // Sort IPv4 and IPv6 routes separately - sort.Slice(ipv4Routes, func(i, j int) bool { - return ipv4Routes[i].Bits() > ipv4Routes[j].Bits() - }) - sort.Slice(ipv6Routes, func(i, j int) bool { - return ipv6Routes[i].Bits() > ipv6Routes[j].Bits() - }) - - var builder strings.Builder - - // Format IPv4 routes - builder.WriteString("IPv4 Routes:\n") - for _, route := range ipv4Routes { - formatRoute(&builder, route, anonymize, anonymizer) - } - - // Format IPv6 routes - builder.WriteString("\nIPv6 Routes:\n") - for _, route := range ipv6Routes { - formatRoute(&builder, route, anonymize, anonymizer) - } - - return builder.String() -} - -func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) { - if anonymize { - anonymizedIP := anonymizer.AnonymizeIP(route.Addr()) - builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits())) - } else { - builder.WriteString(fmt.Sprintf("%s\n", route)) - } -} - -func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string { - sort.Slice(interfaces, func(i, j int) bool { - return interfaces[i].Name < interfaces[j].Name - }) - - var builder strings.Builder - builder.WriteString("Network Interfaces:\n") - - for _, iface := range interfaces { - builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name)) - builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index)) - builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU)) - builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags)) - - addrs, err := iface.Addrs() - if err != nil { - builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err)) - } else { - builder.WriteString(" Addresses:\n") - for _, addr := range addrs { - prefix, err := netip.ParsePrefix(addr.String()) - if err != nil { - builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err)) - continue - } - ip := prefix.Addr() - if anonymize { - ip = anonymizer.AnonymizeIP(ip) - } - builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits())) - } - } - } - - return builder.String() -} - func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) { defer func() { // always nil @@ -954,7 +942,6 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize. } for i, ip := range peer.AllowedIps { - // Try to parse as prefix first (CIDR) if prefix, err := netip.ParsePrefix(ip); err == nil { anonIP := anonymizer.AnonymizeIP(prefix.Addr()) peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) @@ -1033,7 +1020,7 @@ func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.An func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) { switch record.Type { - case 1, 28: // A or AAAA record + case 1, 28: if addr, err := netip.ParseAddr(record.RData); err == nil { record.RData = anonymizer.AnonymizeIP(addr).String() } diff --git a/client/internal/debug/debug_linux.go b/client/internal/debug/debug_linux.go index 4626cd9a2..39d796fda 100644 --- a/client/internal/debug/debug_linux.go +++ b/client/internal/debug/debug_linux.go @@ -17,8 +17,27 @@ import ( "github.com/google/nftables" "github.com/google/nftables/expr" log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" ) +// addIPRules collects and adds IP rules to the archive +func (g *BundleGenerator) addIPRules() error { + log.Info("Collecting IP rules") + ipRules, err := systemops.GetIPRules() + if err != nil { + return fmt.Errorf("get IP rules: %w", err) + } + + rulesContent := formatIPRulesTable(ipRules, g.anonymize, g.anonymizer) + rulesReader := strings.NewReader(rulesContent) + if err := g.addFileToZip(rulesReader, "ip_rules.txt"); err != nil { + return fmt.Errorf("add IP rules file to zip: %w", err) + } + + return nil +} + const ( maxLogEntries = 100000 maxLogAge = 7 * 24 * time.Hour // Last 7 days @@ -136,7 +155,6 @@ func (g *BundleGenerator) addFirewallRules() error { func collectIPTablesRules() (string, error) { var builder strings.Builder - // First try using iptables-save saveOutput, err := collectIPTablesSave() if err != nil { log.Warnf("Failed to collect iptables rules using iptables-save: %v", err) @@ -146,7 +164,6 @@ func collectIPTablesRules() (string, error) { builder.WriteString("\n") } - // Collect ipset information ipsetOutput, err := collectIPSets() if err != nil { log.Warnf("Failed to collect ipset information: %v", err) @@ -232,11 +249,9 @@ func getTableStatistics(table string) (string, error) { // collectNFTablesRules attempts to collect nftables rules using either nft command or netlink func collectNFTablesRules() (string, error) { - // First try using nft command rules, err := collectNFTablesFromCommand() if err != nil { log.Debugf("Failed to collect nftables rules using nft command: %v, falling back to netlink", err) - // Fall back to netlink rules, err = collectNFTablesFromNetlink() if err != nil { return "", fmt.Errorf("collect nftables rules using both nft and netlink failed: %w", err) @@ -451,7 +466,6 @@ func formatRule(rule *nftables.Rule) string { func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int { curr := exprs[i] - // Handle Meta + Cmp sequence if meta, ok := curr.(*expr.Meta); ok && i+1 < len(exprs) { if cmp, ok := exprs[i+1].(*expr.Cmp); ok { if formatted := formatMetaWithCmp(meta, cmp); formatted != "" { @@ -461,7 +475,6 @@ func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int { } } - // Handle Payload + Cmp sequence if payload, ok := curr.(*expr.Payload); ok && i+1 < len(exprs) { if cmp, ok := exprs[i+1].(*expr.Cmp); ok { builder.WriteString(formatPayloadWithCmp(payload, cmp)) @@ -493,13 +506,13 @@ func formatMetaWithCmp(meta *expr.Meta, cmp *expr.Cmp) string { func formatPayloadWithCmp(p *expr.Payload, cmp *expr.Cmp) string { if p.Base == expr.PayloadBaseNetworkHeader { switch p.Offset { - case 12: // Source IP + case 12: if p.Len == 4 { return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) } else if p.Len == 2 { return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) } - case 16: // Destination IP + case 16: if p.Len == 4 { return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) } else if p.Len == 2 { @@ -580,7 +593,6 @@ func formatExpr(exp expr.Any) string { } func formatImmediateData(data []byte) string { - // For IP addresses (4 bytes) if len(data) == 4 { return fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3]) } @@ -588,26 +600,21 @@ func formatImmediateData(data []byte) string { } func formatMeta(e *expr.Meta) string { - // Handle source register case first (meta mark set) if e.SourceRegister { return fmt.Sprintf("meta %s set reg %d", formatMetaKey(e.Key), e.Register) } - // For interface names, handle register load operation switch e.Key { case expr.MetaKeyIIFNAME, expr.MetaKeyOIFNAME, expr.MetaKeyBRIIIFNAME, expr.MetaKeyBRIOIFNAME: - // Simply the key name with no register reference return formatMetaKey(e.Key) case expr.MetaKeyMARK: - // For mark operations, we want just "mark" return "mark" } - // For other meta keys, show as loading into register return fmt.Sprintf("meta %s => reg %d", formatMetaKey(e.Key), e.Register) } diff --git a/client/internal/debug/debug_nonlinux.go b/client/internal/debug/debug_nonlinux.go index b0ff55613..ace53bd94 100644 --- a/client/internal/debug/debug_nonlinux.go +++ b/client/internal/debug/debug_nonlinux.go @@ -12,3 +12,8 @@ func (g *BundleGenerator) trySystemdLogFallback() error { // TODO: Add BSD support return nil } + +func (g *BundleGenerator) addIPRules() error { + // IP rules are only supported on Linux + return nil +} diff --git a/client/internal/debug/debug_nonmobile.go b/client/internal/debug/debug_nonmobile.go index 3b487f07f..1f69f50c9 100644 --- a/client/internal/debug/debug_nonmobile.go +++ b/client/internal/debug/debug_nonmobile.go @@ -10,16 +10,16 @@ import ( ) func (g *BundleGenerator) addRoutes() error { - routes, err := systemops.GetRoutesFromTable() + detailedRoutes, err := systemops.GetDetailedRoutesFromTable() if err != nil { - return fmt.Errorf("get routes: %w", err) + return fmt.Errorf("get detailed routes: %w", err) } - // TODO: get routes including nexthop - routesContent := formatRoutes(routes, g.anonymize, g.anonymizer) + routesContent := formatRoutesTable(detailedRoutes, g.anonymize, g.anonymizer) routesReader := strings.NewReader(routesContent) if err := g.addFileToZip(routesReader, "routes.txt"); err != nil { return fmt.Errorf("add routes file to zip: %w", err) } + return nil } diff --git a/client/internal/debug/format.go b/client/internal/debug/format.go new file mode 100644 index 000000000..54fc77f93 --- /dev/null +++ b/client/internal/debug/format.go @@ -0,0 +1,206 @@ +package debug + +import ( + "fmt" + "net" + "net/netip" + "sort" + "strings" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" + "github.com/netbirdio/netbird/management/domain" +) + +func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string { + sort.Slice(interfaces, func(i, j int) bool { + return interfaces[i].Name < interfaces[j].Name + }) + + var builder strings.Builder + builder.WriteString("Network Interfaces:\n") + + for _, iface := range interfaces { + builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name)) + builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index)) + builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU)) + builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags)) + + addrs, err := iface.Addrs() + if err != nil { + builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err)) + } else { + builder.WriteString(" Addresses:\n") + for _, addr := range addrs { + prefix, err := netip.ParsePrefix(addr.String()) + if err != nil { + builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err)) + continue + } + ip := prefix.Addr() + if anonymize { + ip = anonymizer.AnonymizeIP(ip) + } + builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits())) + } + } + } + + return builder.String() +} + +func formatResolvedDomains(resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo, anonymize bool, anonymizer *anonymize.Anonymizer) string { + if len(resolvedDomains) == 0 { + return "No resolved domains found.\n" + } + + var builder strings.Builder + builder.WriteString("Resolved Domains:\n") + builder.WriteString("=================\n\n") + + var sortedParents []domain.Domain + for parentDomain := range resolvedDomains { + sortedParents = append(sortedParents, parentDomain) + } + sort.Slice(sortedParents, func(i, j int) bool { + return sortedParents[i].SafeString() < sortedParents[j].SafeString() + }) + + for _, parentDomain := range sortedParents { + info := resolvedDomains[parentDomain] + + parentKey := parentDomain.SafeString() + if anonymize { + parentKey = anonymizer.AnonymizeDomain(parentKey) + } + + builder.WriteString(fmt.Sprintf("%s:\n", parentKey)) + + var sortedIPs []string + for _, prefix := range info.Prefixes { + ipStr := prefix.String() + if anonymize { + anonymizedIP := anonymizer.AnonymizeIP(prefix.Addr()) + ipStr = fmt.Sprintf("%s/%d", anonymizedIP, prefix.Bits()) + } + sortedIPs = append(sortedIPs, ipStr) + } + sort.Strings(sortedIPs) + + for _, ipStr := range sortedIPs { + builder.WriteString(fmt.Sprintf(" %s\n", ipStr)) + } + builder.WriteString("\n") + } + + return builder.String() +} + +func formatRoutesTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) string { + if len(detailedRoutes) == 0 { + return "No routes found.\n" + } + + sort.Slice(detailedRoutes, func(i, j int) bool { + if detailedRoutes[i].Table != detailedRoutes[j].Table { + return detailedRoutes[i].Table < detailedRoutes[j].Table + } + return detailedRoutes[i].Route.Dst.String() < detailedRoutes[j].Route.Dst.String() + }) + + headers, rows := buildPlatformSpecificRouteTable(detailedRoutes, anonymize, anonymizer) + + return formatTable("Routing Table:", headers, rows) +} + +func formatRouteDestination(destination netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string { + if anonymize { + anonymizedDestIP := anonymizer.AnonymizeIP(destination.Addr()) + return fmt.Sprintf("%s/%d", anonymizedDestIP, destination.Bits()) + } + return destination.String() +} + +func formatRouteGateway(gateway netip.Addr, anonymize bool, anonymizer *anonymize.Anonymizer) string { + if gateway.IsValid() { + if anonymize { + return anonymizer.AnonymizeIP(gateway).String() + } + return gateway.String() + } + return "-" +} + +func formatRouteInterface(iface *net.Interface) string { + if iface != nil { + return iface.Name + } + return "-" +} + +func formatInterfaceIndex(index int) string { + if index <= 0 { + return "-" + } + return fmt.Sprintf("%d", index) +} + +func formatRouteMetric(metric int) string { + if metric < 0 { + return "-" + } + return fmt.Sprintf("%d", metric) +} + +func formatTable(title string, headers []string, rows [][]string) string { + widths := make([]int, len(headers)) + + for i, header := range headers { + widths[i] = len(header) + } + + for _, row := range rows { + for i, cell := range row { + if len(cell) > widths[i] { + widths[i] = len(cell) + } + } + } + + for i := range widths { + widths[i] += 2 + } + + var formatParts []string + for _, width := range widths { + formatParts = append(formatParts, fmt.Sprintf("%%-%ds", width)) + } + formatStr := strings.Join(formatParts, "") + "\n" + + var builder strings.Builder + builder.WriteString(title + "\n") + builder.WriteString(strings.Repeat("=", len(title)) + "\n\n") + + headerArgs := make([]interface{}, len(headers)) + for i, header := range headers { + headerArgs[i] = header + } + builder.WriteString(fmt.Sprintf(formatStr, headerArgs...)) + + separatorArgs := make([]interface{}, len(headers)) + for i, width := range widths { + separatorArgs[i] = strings.Repeat("-", width-2) + } + builder.WriteString(fmt.Sprintf(formatStr, separatorArgs...)) + + for _, row := range rows { + rowArgs := make([]interface{}, len(row)) + for i, cell := range row { + rowArgs[i] = cell + } + builder.WriteString(fmt.Sprintf(formatStr, rowArgs...)) + } + + return builder.String() +} diff --git a/client/internal/debug/format_linux.go b/client/internal/debug/format_linux.go new file mode 100644 index 000000000..7a2ba49ea --- /dev/null +++ b/client/internal/debug/format_linux.go @@ -0,0 +1,185 @@ +//go:build linux && !android + +package debug + +import ( + "fmt" + "net/netip" + "sort" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" +) + +func formatIPRulesTable(ipRules []systemops.IPRule, anonymize bool, anonymizer *anonymize.Anonymizer) string { + if len(ipRules) == 0 { + return "No IP rules found.\n" + } + + sort.Slice(ipRules, func(i, j int) bool { + return ipRules[i].Priority < ipRules[j].Priority + }) + + columnConfig := detectIPRuleColumns(ipRules) + + headers := buildIPRuleHeaders(columnConfig) + + rows := buildIPRuleRows(ipRules, columnConfig, anonymize, anonymizer) + + return formatTable("IP Rules:", headers, rows) +} + +type ipRuleColumnConfig struct { + hasInvert, hasTo, hasMark, hasIIF, hasOIF, hasSuppressPlen bool +} + +func detectIPRuleColumns(ipRules []systemops.IPRule) ipRuleColumnConfig { + var config ipRuleColumnConfig + for _, rule := range ipRules { + if rule.Invert { + config.hasInvert = true + } + if rule.To.IsValid() { + config.hasTo = true + } + if rule.Mark != 0 { + config.hasMark = true + } + if rule.IIF != "" { + config.hasIIF = true + } + if rule.OIF != "" { + config.hasOIF = true + } + if rule.SuppressPlen >= 0 { + config.hasSuppressPlen = true + } + } + return config +} + +func buildIPRuleHeaders(config ipRuleColumnConfig) []string { + var headers []string + + headers = append(headers, "Priority") + if config.hasInvert { + headers = append(headers, "Not") + } + headers = append(headers, "From") + if config.hasTo { + headers = append(headers, "To") + } + if config.hasMark { + headers = append(headers, "FWMark") + } + if config.hasIIF { + headers = append(headers, "IIF") + } + if config.hasOIF { + headers = append(headers, "OIF") + } + headers = append(headers, "Table") + headers = append(headers, "Action") + if config.hasSuppressPlen { + headers = append(headers, "SuppressPlen") + } + + return headers +} + +func buildIPRuleRows(ipRules []systemops.IPRule, config ipRuleColumnConfig, anonymize bool, anonymizer *anonymize.Anonymizer) [][]string { + var rows [][]string + for _, rule := range ipRules { + row := buildSingleIPRuleRow(rule, config, anonymize, anonymizer) + rows = append(rows, row) + } + return rows +} + +func buildSingleIPRuleRow(rule systemops.IPRule, config ipRuleColumnConfig, anonymize bool, anonymizer *anonymize.Anonymizer) []string { + var row []string + + row = append(row, fmt.Sprintf("%d", rule.Priority)) + + if config.hasInvert { + row = append(row, formatIPRuleInvert(rule.Invert)) + } + + row = append(row, formatIPRuleAddress(rule.From, "all", anonymize, anonymizer)) + + if config.hasTo { + row = append(row, formatIPRuleAddress(rule.To, "-", anonymize, anonymizer)) + } + + if config.hasMark { + row = append(row, formatIPRuleMark(rule.Mark, rule.Mask)) + } + + if config.hasIIF { + row = append(row, formatIPRuleInterface(rule.IIF)) + } + + if config.hasOIF { + row = append(row, formatIPRuleInterface(rule.OIF)) + } + + row = append(row, rule.Table) + + row = append(row, formatIPRuleAction(rule.Action)) + + if config.hasSuppressPlen { + row = append(row, formatIPRuleSuppressPlen(rule.SuppressPlen)) + } + + return row +} + +func formatIPRuleInvert(invert bool) string { + if invert { + return "not" + } + return "-" +} + +func formatIPRuleAction(action string) string { + if action == "unspec" { + return "lookup" + } + return action +} + +func formatIPRuleSuppressPlen(suppressPlen int) string { + if suppressPlen >= 0 { + return fmt.Sprintf("%d", suppressPlen) + } + return "-" +} + +func formatIPRuleAddress(prefix netip.Prefix, defaultVal string, anonymize bool, anonymizer *anonymize.Anonymizer) string { + if !prefix.IsValid() { + return defaultVal + } + + if anonymize { + anonymizedIP := anonymizer.AnonymizeIP(prefix.Addr()) + return fmt.Sprintf("%s/%d", anonymizedIP, prefix.Bits()) + } + return prefix.String() +} + +func formatIPRuleMark(mark, mask uint32) string { + if mark == 0 { + return "-" + } + if mask != 0 { + return fmt.Sprintf("0x%x/0x%x", mark, mask) + } + return fmt.Sprintf("0x%x", mark) +} + +func formatIPRuleInterface(iface string) string { + if iface == "" { + return "-" + } + return iface +} diff --git a/client/internal/debug/format_nonwindows.go b/client/internal/debug/format_nonwindows.go new file mode 100644 index 000000000..3ad5c596c --- /dev/null +++ b/client/internal/debug/format_nonwindows.go @@ -0,0 +1,27 @@ +//go:build !windows + +package debug + +import ( + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" +) + +// buildPlatformSpecificRouteTable builds headers and rows for non-Windows platforms +func buildPlatformSpecificRouteTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) ([]string, [][]string) { + headers := []string{"Destination", "Gateway", "Interface", "Idx", "Metric", "Protocol", "Scope", "Type", "Table", "Flags"} + + var rows [][]string + for _, route := range detailedRoutes { + destStr := formatRouteDestination(route.Route.Dst, anonymize, anonymizer) + gatewayStr := formatRouteGateway(route.Route.Gw, anonymize, anonymizer) + interfaceStr := formatRouteInterface(route.Route.Interface) + indexStr := formatInterfaceIndex(route.InterfaceIndex) + metricStr := formatRouteMetric(route.Metric) + + row := []string{destStr, gatewayStr, interfaceStr, indexStr, metricStr, route.Protocol, route.Scope, route.Type, route.Table, route.Flags} + rows = append(rows, row) + } + + return headers, rows +} diff --git a/client/internal/debug/format_windows.go b/client/internal/debug/format_windows.go new file mode 100644 index 000000000..b37112d6f --- /dev/null +++ b/client/internal/debug/format_windows.go @@ -0,0 +1,37 @@ +//go:build windows + +package debug + +import ( + "fmt" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" +) + +// buildPlatformSpecificRouteTable builds headers and rows for Windows with interface metrics +func buildPlatformSpecificRouteTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) ([]string, [][]string) { + headers := []string{"Destination", "Gateway", "Interface", "Idx", "Metric", "If Metric", "Protocol", "Age", "Origin"} + + var rows [][]string + for _, route := range detailedRoutes { + destStr := formatRouteDestination(route.Route.Dst, anonymize, anonymizer) + gatewayStr := formatRouteGateway(route.Route.Gw, anonymize, anonymizer) + interfaceStr := formatRouteInterface(route.Route.Interface) + indexStr := formatInterfaceIndex(route.InterfaceIndex) + metricStr := formatRouteMetric(route.Metric) + ifMetricStr := formatInterfaceMetric(route.InterfaceMetric) + + row := []string{destStr, gatewayStr, interfaceStr, indexStr, metricStr, ifMetricStr, route.Protocol, route.Scope, route.Type} + rows = append(rows, row) + } + + return headers, rows +} + +func formatInterfaceMetric(metric int) string { + if metric < 0 { + return "-" + } + return fmt.Sprintf("%d", metric) +} diff --git a/client/internal/routemanager/systemops/routeflags_bsd.go b/client/internal/routemanager/systemops/routeflags_bsd.go index 12f158dcb..ad32e5029 100644 --- a/client/internal/routemanager/systemops/routeflags_bsd.go +++ b/client/internal/routemanager/systemops/routeflags_bsd.go @@ -2,9 +2,12 @@ package systemops -import "syscall" +import ( + "strings" + "syscall" +) -// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. +// filterRoutesByFlags returns true if the route message should be ignored based on its flags. func filterRoutesByFlags(routeMessageFlags int) bool { if routeMessageFlags&syscall.RTF_UP == 0 { return true @@ -16,3 +19,50 @@ func filterRoutesByFlags(routeMessageFlags int) bool { return false } + +// formatBSDFlags formats route flags for BSD systems (excludes FreeBSD-specific handling) +func formatBSDFlags(flags int) string { + var flagStrs []string + + if flags&syscall.RTF_UP != 0 { + flagStrs = append(flagStrs, "U") + } + if flags&syscall.RTF_GATEWAY != 0 { + flagStrs = append(flagStrs, "G") + } + if flags&syscall.RTF_HOST != 0 { + flagStrs = append(flagStrs, "H") + } + if flags&syscall.RTF_REJECT != 0 { + flagStrs = append(flagStrs, "R") + } + if flags&syscall.RTF_DYNAMIC != 0 { + flagStrs = append(flagStrs, "D") + } + if flags&syscall.RTF_MODIFIED != 0 { + flagStrs = append(flagStrs, "M") + } + if flags&syscall.RTF_STATIC != 0 { + flagStrs = append(flagStrs, "S") + } + if flags&syscall.RTF_LLINFO != 0 { + flagStrs = append(flagStrs, "L") + } + if flags&syscall.RTF_LOCAL != 0 { + flagStrs = append(flagStrs, "l") + } + if flags&syscall.RTF_BLACKHOLE != 0 { + flagStrs = append(flagStrs, "B") + } + if flags&syscall.RTF_CLONING != 0 { + flagStrs = append(flagStrs, "C") + } + if flags&syscall.RTF_WASCLONED != 0 { + flagStrs = append(flagStrs, "W") + } + + if len(flagStrs) == 0 { + return "-" + } + return strings.Join(flagStrs, "") +} diff --git a/client/internal/routemanager/systemops/routeflags_freebsd.go b/client/internal/routemanager/systemops/routeflags_freebsd.go index cb35f521e..2338fe5d8 100644 --- a/client/internal/routemanager/systemops/routeflags_freebsd.go +++ b/client/internal/routemanager/systemops/routeflags_freebsd.go @@ -1,19 +1,64 @@ -//go:build: freebsd +//go:build freebsd + package systemops -import "syscall" +import ( + "strings" + "syscall" +) -// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. +// filterRoutesByFlags returns true if the route message should be ignored based on its flags. func filterRoutesByFlags(routeMessageFlags int) bool { if routeMessageFlags&syscall.RTF_UP == 0 { return true } - // NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 (https://www.freebsd.org/releases/8.0R/relnotes-detailed/) - // a concept of cloned route (a route generated by an entry with RTF_CLONING flag) is deprecated. + // NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 { return true } return false } + +// formatBSDFlags formats route flags for FreeBSD (excludes deprecated RTF_CLONING and RTF_WASCLONED) +func formatBSDFlags(flags int) string { + var flagStrs []string + + if flags&syscall.RTF_UP != 0 { + flagStrs = append(flagStrs, "U") + } + if flags&syscall.RTF_GATEWAY != 0 { + flagStrs = append(flagStrs, "G") + } + if flags&syscall.RTF_HOST != 0 { + flagStrs = append(flagStrs, "H") + } + if flags&syscall.RTF_REJECT != 0 { + flagStrs = append(flagStrs, "R") + } + if flags&syscall.RTF_DYNAMIC != 0 { + flagStrs = append(flagStrs, "D") + } + if flags&syscall.RTF_MODIFIED != 0 { + flagStrs = append(flagStrs, "M") + } + if flags&syscall.RTF_STATIC != 0 { + flagStrs = append(flagStrs, "S") + } + if flags&syscall.RTF_LLINFO != 0 { + flagStrs = append(flagStrs, "L") + } + if flags&syscall.RTF_LOCAL != 0 { + flagStrs = append(flagStrs, "l") + } + if flags&syscall.RTF_BLACKHOLE != 0 { + flagStrs = append(flagStrs, "B") + } + // Note: RTF_CLONING and RTF_WASCLONED deprecated in FreeBSD 8.0 + + if len(flagStrs) == 0 { + return "-" + } + return strings.Join(flagStrs, "") +} diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go index b91348e94..8da138117 100644 --- a/client/internal/routemanager/systemops/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -19,6 +19,26 @@ type Nexthop struct { Intf *net.Interface } +// Route represents a basic network route with core routing information +type Route struct { + Dst netip.Prefix + Gw netip.Addr + Interface *net.Interface +} + +// DetailedRoute extends Route with additional metadata for display and debugging +type DetailedRoute struct { + Route + Metric int + InterfaceMetric int + InterfaceIndex int + Protocol string + Scope string + Type string + Table string + Flags string +} + // Equal checks if two nexthops are equal. func (n Nexthop) Equal(other Nexthop) bool { return n.IP == other.IP && (n.Intf == nil && other.Intf == nil || diff --git a/client/internal/routemanager/systemops/systemops_bsd.go b/client/internal/routemanager/systemops/systemops_bsd.go index 5e3b20a86..3ce78a04a 100644 --- a/client/internal/routemanager/systemops/systemops_bsd.go +++ b/client/internal/routemanager/systemops/systemops_bsd.go @@ -16,12 +16,6 @@ import ( "golang.org/x/net/route" ) -type Route struct { - Dst netip.Prefix - Gw netip.Addr - Interface *net.Interface -} - func GetRoutesFromTable() ([]netip.Prefix, error) { tab, err := retryFetchRIB() if err != nil { @@ -47,25 +41,134 @@ func GetRoutesFromTable() ([]netip.Prefix, error) { continue } - route, err := MsgToRoute(m) + r, err := MsgToRoute(m) if err != nil { log.Warnf("Failed to parse route message: %v", err) continue } - if route.Dst.IsValid() { - prefixList = append(prefixList, route.Dst) + if r.Dst.IsValid() { + prefixList = append(prefixList, r.Dst) } } return prefixList, nil } +func GetDetailedRoutesFromTable() ([]DetailedRoute, error) { + tab, err := retryFetchRIB() + if err != nil { + return nil, fmt.Errorf("fetch RIB: %v", err) + } + + msgs, err := route.ParseRIB(route.RIBTypeRoute, tab) + if err != nil { + return nil, fmt.Errorf("parse RIB: %v", err) + } + + return processRouteMessages(msgs) +} + +func processRouteMessages(msgs []route.Message) ([]DetailedRoute, error) { + var detailedRoutes []DetailedRoute + + for _, msg := range msgs { + m := msg.(*route.RouteMessage) + + if !isValidRouteMessage(m) { + continue + } + + if filterRoutesByFlags(m.Flags) { + continue + } + + detailed, err := buildDetailedRouteFromMessage(m) + if err != nil { + log.Warnf("Failed to parse route message: %v", err) + continue + } + + if detailed != nil { + detailedRoutes = append(detailedRoutes, *detailed) + } + } + + return detailedRoutes, nil +} + +func isValidRouteMessage(m *route.RouteMessage) bool { + if m.Version < 3 || m.Version > 5 { + log.Warnf("Unexpected RIB message version: %d", m.Version) + return false + } + if m.Type != syscall.RTM_GET { + log.Warnf("Unexpected RIB message type: %d", m.Type) + return false + } + return true +} + +func buildDetailedRouteFromMessage(m *route.RouteMessage) (*DetailedRoute, error) { + routeMsg, err := MsgToRoute(m) + if err != nil { + return nil, err + } + + if !routeMsg.Dst.IsValid() { + return nil, errors.New("invalid destination") + } + + detailed := DetailedRoute{ + Route: Route{ + Dst: routeMsg.Dst, + Gw: routeMsg.Gw, + Interface: routeMsg.Interface, + }, + Metric: extractBSDMetric(m), + Protocol: extractBSDProtocol(m.Flags), + Scope: "global", + Type: "unicast", + Table: "main", + Flags: formatBSDFlags(m.Flags), + } + + return &detailed, nil +} + +func buildLinkInterface(t *route.LinkAddr) *net.Interface { + interfaceName := fmt.Sprintf("link#%d", t.Index) + if t.Name != "" { + interfaceName = t.Name + } + return &net.Interface{ + Index: t.Index, + Name: interfaceName, + } +} + +func extractBSDMetric(m *route.RouteMessage) int { + return -1 +} + +func extractBSDProtocol(flags int) string { + if flags&syscall.RTF_STATIC != 0 { + return "static" + } + if flags&syscall.RTF_DYNAMIC != 0 { + return "dynamic" + } + if flags&syscall.RTF_LOCAL != 0 { + return "local" + } + return "kernel" +} + func retryFetchRIB() ([]byte, error) { var out []byte operation := func() error { var err error out, err = route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0) if errors.Is(err, syscall.ENOMEM) { - log.Debug("~etrying fetchRIB due to 'cannot allocate memory' error") + log.Debug("Retrying fetchRIB due to 'cannot allocate memory' error") return err } else if err != nil { return backoff.Permanent(err) @@ -100,7 +203,6 @@ func toNetIP(a route.Addr) netip.Addr { } } -// ones returns the number of leading ones in the mask. func ones(a route.Addr) (int, error) { switch t := a.(type) { case *route.Inet4Addr: @@ -114,7 +216,6 @@ func ones(a route.Addr) (int, error) { } } -// MsgToRoute converts a route message to a Route. func MsgToRoute(msg *route.RouteMessage) (*Route, error) { dstIP, nexthop, dstMask := msg.Addrs[0], msg.Addrs[1], msg.Addrs[2] @@ -127,10 +228,7 @@ func MsgToRoute(msg *route.RouteMessage) (*Route, error) { case *route.Inet4Addr, *route.Inet6Addr: nexthopAddr = toNetIP(t) case *route.LinkAddr: - nexthopIntf = &net.Interface{ - Index: t.Index, - Name: t.Name, - } + nexthopIntf = buildLinkInterface(t) default: return nil, fmt.Errorf("unexpected next hop type: %T", t) } @@ -156,5 +254,4 @@ func MsgToRoute(msg *route.RouteMessage) (*Route, error) { Gw: nexthopAddr, Interface: nexthopIntf, }, nil - } diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index 711f1d758..f50ea572c 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/routemanager/sysctl" @@ -22,6 +23,25 @@ import ( nbnet "github.com/netbirdio/netbird/util/net" ) +// IPRule contains IP rule information for debugging +type IPRule struct { + Priority int + From netip.Prefix + To netip.Prefix + IIF string + OIF string + Table string + Action string + Mark uint32 + Mask uint32 + TunID uint32 + Goto uint32 + Flow uint32 + SuppressPlen int + SuppressIFL int + Invert bool +} + const ( // NetbirdVPNTableID is the ID of the custom routing table used by Netbird. NetbirdVPNTableID = 0x1BD0 @@ -37,6 +57,8 @@ const ( var ErrTableIDExists = errors.New("ID exists with different name") +const errParsePrefixMsg = "failed to parse prefix %s: %w" + // originalSysctl stores the original sysctl values before they are modified var originalSysctl map[string]int @@ -209,6 +231,277 @@ func GetRoutesFromTable() ([]netip.Prefix, error) { return append(v4Routes, v6Routes...), nil } +// GetDetailedRoutesFromTable returns detailed route information from all routing tables +func GetDetailedRoutesFromTable() ([]DetailedRoute, error) { + tables := discoverRoutingTables() + return collectRoutesFromTables(tables), nil +} + +func discoverRoutingTables() []int { + tables, err := getAllRoutingTables() + if err != nil { + log.Warnf("Failed to get all routing tables, using fallback list: %v", err) + return []int{ + syscall.RT_TABLE_MAIN, + syscall.RT_TABLE_LOCAL, + NetbirdVPNTableID, + } + } + return tables +} + +func collectRoutesFromTables(tables []int) []DetailedRoute { + var allRoutes []DetailedRoute + + for _, tableID := range tables { + routes := collectRoutesFromTable(tableID) + allRoutes = append(allRoutes, routes...) + } + + return allRoutes +} + +func collectRoutesFromTable(tableID int) []DetailedRoute { + var routes []DetailedRoute + + if v4Routes := getRoutesForFamily(tableID, netlink.FAMILY_V4); len(v4Routes) > 0 { + routes = append(routes, v4Routes...) + } + + if v6Routes := getRoutesForFamily(tableID, netlink.FAMILY_V6); len(v6Routes) > 0 { + routes = append(routes, v6Routes...) + } + + return routes +} + +func getRoutesForFamily(tableID, family int) []DetailedRoute { + routes, err := getDetailedRoutes(tableID, family) + if err != nil { + log.Debugf("Failed to get routes from table %d family %d: %v", tableID, family, err) + return nil + } + return routes +} + +func getAllRoutingTables() ([]int, error) { + tablesMap := make(map[int]bool) + families := []int{netlink.FAMILY_V4, netlink.FAMILY_V6} + + // Use table 0 (RT_TABLE_UNSPEC) to discover all tables + for _, family := range families { + routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: 0}, netlink.RT_FILTER_TABLE) + if err != nil { + log.Debugf("Failed to list routes from table 0 for family %d: %v", family, err) + continue + } + + // Extract unique table IDs from all routes + for _, route := range routes { + if route.Table > 0 { + tablesMap[route.Table] = true + } + } + } + + var tables []int + for tableID := range tablesMap { + tables = append(tables, tableID) + } + + standardTables := []int{syscall.RT_TABLE_MAIN, syscall.RT_TABLE_LOCAL, NetbirdVPNTableID} + for _, table := range standardTables { + if !tablesMap[table] { + tables = append(tables, table) + } + } + + return tables, nil +} + +// getDetailedRoutes fetches detailed routes from a specific routing table +func getDetailedRoutes(tableID, family int) ([]DetailedRoute, error) { + var detailedRoutes []DetailedRoute + + routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: tableID}, netlink.RT_FILTER_TABLE) + if err != nil { + return nil, fmt.Errorf("list routes from table %d: %v", tableID, err) + } + + for _, route := range routes { + detailed := buildDetailedRoute(route, tableID, family) + if detailed != nil { + detailedRoutes = append(detailedRoutes, *detailed) + } + } + + return detailedRoutes, nil +} + +func buildDetailedRoute(route netlink.Route, tableID, family int) *DetailedRoute { + detailed := DetailedRoute{ + Route: Route{}, + Metric: route.Priority, + InterfaceMetric: -1, // Interface metrics not typically used on Linux + InterfaceIndex: route.LinkIndex, + Protocol: routeProtocolToString(int(route.Protocol)), + Scope: routeScopeToString(route.Scope), + Type: routeTypeToString(route.Type), + Table: routeTableToString(tableID), + Flags: "-", + } + + if !processRouteDestination(&detailed, route, family) { + return nil + } + + processRouteGateway(&detailed, route) + + processRouteInterface(&detailed, route) + + return &detailed +} + +func processRouteDestination(detailed *DetailedRoute, route netlink.Route, family int) bool { + if route.Dst != nil { + addr, ok := netip.AddrFromSlice(route.Dst.IP) + if !ok { + return false + } + ones, _ := route.Dst.Mask.Size() + prefix := netip.PrefixFrom(addr.Unmap(), ones) + if prefix.IsValid() { + detailed.Route.Dst = prefix + } else { + return false + } + } else { + if family == netlink.FAMILY_V4 { + detailed.Route.Dst = netip.MustParsePrefix("0.0.0.0/0") + } else { + detailed.Route.Dst = netip.MustParsePrefix("::/0") + } + } + return true +} + +func processRouteGateway(detailed *DetailedRoute, route netlink.Route) { + if route.Gw != nil { + if gateway, ok := netip.AddrFromSlice(route.Gw); ok { + detailed.Route.Gw = gateway.Unmap() + } + } +} + +func processRouteInterface(detailed *DetailedRoute, route netlink.Route) { + if route.LinkIndex > 0 { + if link, err := netlink.LinkByIndex(route.LinkIndex); err == nil { + detailed.Route.Interface = &net.Interface{ + Index: link.Attrs().Index, + Name: link.Attrs().Name, + } + } else { + detailed.Route.Interface = &net.Interface{ + Index: route.LinkIndex, + Name: fmt.Sprintf("index-%d", route.LinkIndex), + } + } + } +} + +// Helper functions to convert netlink constants to strings +func routeProtocolToString(protocol int) string { + switch protocol { + case syscall.RTPROT_UNSPEC: + return "unspec" + case syscall.RTPROT_REDIRECT: + return "redirect" + case syscall.RTPROT_KERNEL: + return "kernel" + case syscall.RTPROT_BOOT: + return "boot" + case syscall.RTPROT_STATIC: + return "static" + case syscall.RTPROT_DHCP: + return "dhcp" + case unix.RTPROT_RA: + return "ra" + case unix.RTPROT_ZEBRA: + return "zebra" + case unix.RTPROT_BIRD: + return "bird" + case unix.RTPROT_DNROUTED: + return "dnrouted" + case unix.RTPROT_XORP: + return "xorp" + case unix.RTPROT_NTK: + return "ntk" + default: + return fmt.Sprintf("%d", protocol) + } +} + +func routeScopeToString(scope netlink.Scope) string { + switch scope { + case netlink.SCOPE_UNIVERSE: + return "global" + case netlink.SCOPE_SITE: + return "site" + case netlink.SCOPE_LINK: + return "link" + case netlink.SCOPE_HOST: + return "host" + case netlink.SCOPE_NOWHERE: + return "nowhere" + default: + return fmt.Sprintf("%d", scope) + } +} + +func routeTypeToString(routeType int) string { + switch routeType { + case syscall.RTN_UNSPEC: + return "unspec" + case syscall.RTN_UNICAST: + return "unicast" + case syscall.RTN_LOCAL: + return "local" + case syscall.RTN_BROADCAST: + return "broadcast" + case syscall.RTN_ANYCAST: + return "anycast" + case syscall.RTN_MULTICAST: + return "multicast" + case syscall.RTN_BLACKHOLE: + return "blackhole" + case syscall.RTN_UNREACHABLE: + return "unreachable" + case syscall.RTN_PROHIBIT: + return "prohibit" + case syscall.RTN_THROW: + return "throw" + case syscall.RTN_NAT: + return "nat" + case syscall.RTN_XRESOLVE: + return "xresolve" + default: + return fmt.Sprintf("%d", routeType) + } +} + +func routeTableToString(tableID int) string { + switch tableID { + case syscall.RT_TABLE_MAIN: + return "main" + case syscall.RT_TABLE_LOCAL: + return "local" + case NetbirdVPNTableID: + return "netbird" + default: + return fmt.Sprintf("%d", tableID) + } +} + // getRoutes fetches routes from a specific routing table identified by tableID. func getRoutes(tableID, family int) ([]netip.Prefix, error) { var prefixList []netip.Prefix @@ -237,6 +530,115 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) { return prefixList, nil } +// GetIPRules returns IP rules for debugging +func GetIPRules() ([]IPRule, error) { + v4Rules, err := getIPRules(netlink.FAMILY_V4) + if err != nil { + return nil, fmt.Errorf("get v4 rules: %w", err) + } + v6Rules, err := getIPRules(netlink.FAMILY_V6) + if err != nil { + return nil, fmt.Errorf("get v6 rules: %w", err) + } + return append(v4Rules, v6Rules...), nil +} + +// getIPRules fetches IP rules for the specified address family +func getIPRules(family int) ([]IPRule, error) { + rules, err := netlink.RuleList(family) + if err != nil { + return nil, fmt.Errorf("list rules for family %d: %w", family, err) + } + + var ipRules []IPRule + for _, rule := range rules { + ipRule := buildIPRule(rule) + ipRules = append(ipRules, ipRule) + } + + return ipRules, nil +} + +func buildIPRule(rule netlink.Rule) IPRule { + var mask uint32 + if rule.Mask != nil { + mask = *rule.Mask + } + + ipRule := IPRule{ + Priority: rule.Priority, + IIF: rule.IifName, + OIF: rule.OifName, + Table: ruleTableToString(rule.Table), + Action: ruleActionToString(int(rule.Type)), + Mark: rule.Mark, + Mask: mask, + TunID: uint32(rule.TunID), + Goto: uint32(rule.Goto), + Flow: uint32(rule.Flow), + SuppressPlen: rule.SuppressPrefixlen, + SuppressIFL: rule.SuppressIfgroup, + Invert: rule.Invert, + } + + if rule.Src != nil { + ipRule.From = parseRulePrefix(rule.Src) + } + + if rule.Dst != nil { + ipRule.To = parseRulePrefix(rule.Dst) + } + + return ipRule +} + +func parseRulePrefix(ipNet *net.IPNet) netip.Prefix { + if addr, ok := netip.AddrFromSlice(ipNet.IP); ok { + ones, _ := ipNet.Mask.Size() + prefix := netip.PrefixFrom(addr.Unmap(), ones) + if prefix.IsValid() { + return prefix + } + } + return netip.Prefix{} +} + +func ruleTableToString(table int) string { + switch table { + case syscall.RT_TABLE_MAIN: + return "main" + case syscall.RT_TABLE_LOCAL: + return "local" + case syscall.RT_TABLE_DEFAULT: + return "default" + case NetbirdVPNTableID: + return "netbird" + default: + return fmt.Sprintf("%d", table) + } +} + +func ruleActionToString(action int) string { + switch action { + case unix.FR_ACT_UNSPEC: + return "unspec" + case unix.FR_ACT_TO_TBL: + return "lookup" + case unix.FR_ACT_GOTO: + return "goto" + case unix.FR_ACT_NOP: + return "nop" + case unix.FR_ACT_BLACKHOLE: + return "blackhole" + case unix.FR_ACT_UNREACHABLE: + return "unreachable" + case unix.FR_ACT_PROHIBIT: + return "prohibit" + default: + return fmt.Sprintf("%d", action) + } +} + // addRoute adds a route to a specific routing table identified by tableID. func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { route := &netlink.Route{ @@ -247,7 +649,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { _, ipNet, err := net.ParseCIDR(prefix.String()) if err != nil { - return fmt.Errorf("parse prefix %s: %w", prefix, err) + return fmt.Errorf(errParsePrefixMsg, prefix, err) } route.Dst = ipNet @@ -268,7 +670,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { func addUnreachableRoute(prefix netip.Prefix, tableID int) error { _, ipNet, err := net.ParseCIDR(prefix.String()) if err != nil { - return fmt.Errorf("parse prefix %s: %w", prefix, err) + return fmt.Errorf(errParsePrefixMsg, prefix, err) } route := &netlink.Route{ @@ -288,7 +690,7 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error { func removeUnreachableRoute(prefix netip.Prefix, tableID int) error { _, ipNet, err := net.ParseCIDR(prefix.String()) if err != nil { - return fmt.Errorf("parse prefix %s: %w", prefix, err) + return fmt.Errorf(errParsePrefixMsg, prefix, err) } route := &netlink.Route{ @@ -313,7 +715,7 @@ func removeUnreachableRoute(prefix netip.Prefix, tableID int) error { func removeRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { _, ipNet, err := net.ParseCIDR(prefix.String()) if err != nil { - return fmt.Errorf("parse prefix %s: %w", prefix, err) + return fmt.Errorf(errParsePrefixMsg, prefix, err) } route := &netlink.Route{ diff --git a/client/internal/routemanager/systemops/systemops_nonlinux.go b/client/internal/routemanager/systemops/systemops_nonlinux.go index 59581255f..83b64e82b 100644 --- a/client/internal/routemanager/systemops/systemops_nonlinux.go +++ b/client/internal/routemanager/systemops/systemops_nonlinux.go @@ -10,6 +10,25 @@ import ( log "github.com/sirupsen/logrus" ) +// IPRule contains IP rule information for debugging +type IPRule struct { + Priority int + From netip.Prefix + To netip.Prefix + IIF string + OIF string + Table string + Action string + Mark uint32 + Mask uint32 + TunID uint32 + Goto uint32 + Flow uint32 + SuppressPlen int + SuppressIFL int + Invert bool +} + func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { if err := r.validateRoute(prefix); err != nil { return err @@ -32,3 +51,9 @@ func EnableIPForwarding() error { func hasSeparateRouting() ([]netip.Prefix, error) { return GetRoutesFromTable() } + +// GetIPRules returns IP rules for debugging (not supported on non-Linux platforms) +func GetIPRules() ([]IPRule, error) { + log.Infof("IP rules collection is not supported on %s", runtime.GOOS) + return []IPRule{}, nil +} diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go index 7afac9ae5..36e714ec4 100644 --- a/client/internal/routemanager/systemops/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -40,13 +40,6 @@ type RouteMonitor struct { done chan struct{} } -// Route represents a single routing table entry. -type Route struct { - Destination netip.Prefix - Nexthop netip.Addr - Interface *net.Interface -} - type MSFT_NetRoute struct { DestinationPrefix string NextHop string @@ -78,6 +71,12 @@ type MIB_IPFORWARD_ROW2 struct { Origin uint32 } +// MIB_IPFORWARD_TABLE2 represents a table of IP forward entries +type MIB_IPFORWARD_TABLE2 struct { + NumEntries uint32 + Table [1]MIB_IPFORWARD_ROW2 // Flexible array member +} + // IP_ADDRESS_PREFIX is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-ip_address_prefix type IP_ADDRESS_PREFIX struct { Prefix SOCKADDR_INET @@ -108,6 +107,45 @@ type SOCKADDR_INET_NEXTHOP struct { // MIB_NOTIFICATION_TYPE is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ne-netioapi-mib_notification_type type MIB_NOTIFICATION_TYPE int32 +// MIB_IPINTERFACE_ROW is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipinterface_row +type MIB_IPINTERFACE_ROW struct { + Family uint16 + InterfaceLuid luid + InterfaceIndex uint32 + MaxReassemblySize uint32 + InterfaceIdentifier uint64 + MinRouterAdvertisementInterval uint32 + MaxRouterAdvertisementInterval uint32 + AdvertisingEnabled uint8 + ForwardingEnabled uint8 + WeakHostSend uint8 + WeakHostReceive uint8 + UseAutomaticMetric uint8 + UseNeighborUnreachabilityDetection uint8 + ManagedAddressConfigurationSupported uint8 + OtherStatefulConfigurationSupported uint8 + AdvertiseDefaultRoute uint8 + RouterDiscoveryBehavior uint32 + DadTransmits uint32 + BaseReachableTime uint32 + RetransmitTime uint32 + PathMtuDiscoveryTimeout uint32 + LinkLocalAddressBehavior uint32 + LinkLocalAddressTimeout uint32 + ZoneIndices [16]uint32 + SitePrefixLength uint32 + Metric uint32 + NlMtu uint32 + Connected uint8 + SupportsWakeUpPatterns uint8 + SupportsNeighborDiscovery uint8 + SupportsRouterDiscovery uint8 + ReachableTime uint32 + TransmitOffload uint32 + ReceiveOffload uint32 + DisableDefaultRoutes uint8 +} + var ( modiphlpapi = windows.NewLazyDLL("iphlpapi.dll") procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2") @@ -115,8 +153,11 @@ var ( procCreateIpForwardEntry2 = modiphlpapi.NewProc("CreateIpForwardEntry2") procDeleteIpForwardEntry2 = modiphlpapi.NewProc("DeleteIpForwardEntry2") procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2") + procGetIpForwardTable2 = modiphlpapi.NewProc("GetIpForwardTable2") procInitializeIpForwardEntry = modiphlpapi.NewProc("InitializeIpForwardEntry") procConvertInterfaceIndexToLuid = modiphlpapi.NewProc("ConvertInterfaceIndexToLuid") + procGetIpInterfaceEntry = modiphlpapi.NewProc("GetIpInterfaceEntry") + procFreeMibTable = modiphlpapi.NewProc("FreeMibTable") prefixList []netip.Prefix lastUpdate time.Time @@ -429,6 +470,8 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI updateType = RouteAdded case MibDeleteInstance: updateType = RouteDeleted + case MibInitialNotification: + updateType = RouteAdded // Treat initial notifications as additions } update.Type = updateType @@ -508,7 +551,7 @@ func GetRoutesFromTable() ([]netip.Prefix, error) { prefixList = nil for _, route := range routes { - prefixList = append(prefixList, route.Destination) + prefixList = append(prefixList, route.Dst) } lastUpdate = time.Now() @@ -551,15 +594,159 @@ func GetRoutes() ([]Route, error) { } routes = append(routes, Route{ - Destination: dest, - Nexthop: nexthop, - Interface: intf, + Dst: dest, + Gw: nexthop, + Interface: intf, }) } return routes, nil } +// GetDetailedRoutesFromTable returns detailed route information using Windows syscalls +func GetDetailedRoutesFromTable() ([]DetailedRoute, error) { + table, err := getWindowsRoutingTable() + if err != nil { + return nil, err + } + + defer freeWindowsRoutingTable(table) + + return parseWindowsRoutingTable(table), nil +} + +func getWindowsRoutingTable() (*MIB_IPFORWARD_TABLE2, error) { + var table *MIB_IPFORWARD_TABLE2 + + ret, _, err := procGetIpForwardTable2.Call( + uintptr(windows.AF_UNSPEC), + uintptr(unsafe.Pointer(&table)), + ) + if ret != 0 { + return nil, fmt.Errorf("GetIpForwardTable2 failed: %w", err) + } + + if table == nil { + return nil, fmt.Errorf("received nil routing table") + } + + return table, nil +} + +func freeWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) { + if table != nil { + ret, _, _ := procFreeMibTable.Call(uintptr(unsafe.Pointer(table))) + if ret != 0 { + log.Warnf("FreeMibTable failed with return code: %d", ret) + } + } +} + +func parseWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) []DetailedRoute { + var detailedRoutes []DetailedRoute + + entrySize := unsafe.Sizeof(MIB_IPFORWARD_ROW2{}) + basePtr := uintptr(unsafe.Pointer(&table.Table[0])) + + for i := uint32(0); i < table.NumEntries; i++ { + entryPtr := basePtr + uintptr(i)*entrySize + entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr)) + + detailed := buildWindowsDetailedRoute(entry) + if detailed != nil { + detailedRoutes = append(detailedRoutes, *detailed) + } + } + + return detailedRoutes +} + +func buildWindowsDetailedRoute(entry *MIB_IPFORWARD_ROW2) *DetailedRoute { + dest := parseIPPrefix(entry.DestinationPrefix, int(entry.InterfaceIndex)) + if !dest.IsValid() { + return nil + } + + gateway := parseIPNexthop(entry.NextHop, int(entry.InterfaceIndex)) + + var intf *net.Interface + if entry.InterfaceIndex != 0 { + if netIntf, err := net.InterfaceByIndex(int(entry.InterfaceIndex)); err == nil { + intf = netIntf + } else { + // Create a synthetic interface for display when we can't resolve the name + intf = &net.Interface{ + Index: int(entry.InterfaceIndex), + Name: fmt.Sprintf("index-%d", entry.InterfaceIndex), + } + } + } + + detailed := DetailedRoute{ + Route: Route{ + Dst: dest, + Gw: gateway, + Interface: intf, + }, + + Metric: int(entry.Metric), + InterfaceMetric: getInterfaceMetric(entry.InterfaceIndex, entry.DestinationPrefix.Prefix.sin6_family), + InterfaceIndex: int(entry.InterfaceIndex), + Protocol: windowsProtocolToString(entry.Protocol), + Scope: formatRouteAge(entry.Age), + Type: windowsOriginToString(entry.Origin), + Table: "main", + Flags: "-", + } + + return &detailed +} + +func windowsProtocolToString(protocol uint32) string { + switch protocol { + case 1: + return "other" + case 2: + return "local" + case 3: + return "netmgmt" + case 4: + return "icmp" + case 5: + return "egp" + case 6: + return "ggp" + case 7: + return "hello" + case 8: + return "rip" + case 9: + return "isis" + case 10: + return "esis" + case 11: + return "cisco" + case 12: + return "bbn" + case 13: + return "ospf" + case 14: + return "bgp" + case 15: + return "idpr" + case 16: + return "eigrp" + case 17: + return "dvmrp" + case 18: + return "rpl" + case 19: + return "dhcp" + default: + return fmt.Sprintf("unknown-%d", protocol) + } +} + func isCacheDisabled() bool { return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true" } @@ -614,3 +801,59 @@ func addZone(ip netip.Addr, interfaceIndex int) netip.Addr { } return ip } + +// getInterfaceMetric retrieves the interface metric for a given interface and address family +func getInterfaceMetric(interfaceIndex uint32, family int16) int { + if interfaceIndex == 0 { + return -1 + } + + var ipInterfaceRow MIB_IPINTERFACE_ROW + ipInterfaceRow.Family = uint16(family) + ipInterfaceRow.InterfaceIndex = interfaceIndex + + ret, _, _ := procGetIpInterfaceEntry.Call(uintptr(unsafe.Pointer(&ipInterfaceRow))) + if ret != 0 { + log.Debugf("GetIpInterfaceEntry failed for interface %d: %d", interfaceIndex, ret) + return -1 + } + + return int(ipInterfaceRow.Metric) +} + +// formatRouteAge formats the route age in seconds to a human-readable string +func formatRouteAge(ageSeconds uint32) string { + if ageSeconds == 0 { + return "0s" + } + + age := time.Duration(ageSeconds) * time.Second + switch { + case age < time.Minute: + return fmt.Sprintf("%ds", int(age.Seconds())) + case age < time.Hour: + return fmt.Sprintf("%dm", int(age.Minutes())) + case age < 24*time.Hour: + return fmt.Sprintf("%dh", int(age.Hours())) + default: + return fmt.Sprintf("%dd", int(age.Hours()/24)) + } +} + +// windowsOriginToString converts Windows route origin to string +func windowsOriginToString(origin uint32) string { + switch origin { + case 0: + return "manual" + case 1: + return "wellknown" + case 2: + return "dhcp" + case 3: + return "routeradvert" + case 4: + return "6to4" + default: + return fmt.Sprintf("unknown-%d", origin) + } +} From cb8b6ca59b4183867d170fa4897d4d75a5a333cc Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:54:46 +0300 Subject: [PATCH 178/213] [client] Feat: Support Multiple Profiles (#3980) [client] Feat: Support Multiple Profiles (#3980) --- client/android/client.go | 5 +- client/android/login.go | 13 +- client/android/preferences.go | 30 +- client/android/preferences_test.go | 6 +- client/cmd/debug.go | 5 +- client/cmd/debug_unix.go | 3 +- client/cmd/debug_windows.go | 5 +- client/cmd/login.go | 323 +++-- client/cmd/login_test.go | 57 +- client/cmd/profile.go | 236 ++++ client/cmd/root.go | 29 +- client/cmd/service_controller.go | 2 +- client/cmd/ssh.go | 27 +- client/cmd/status.go | 9 +- client/cmd/testutil_test.go | 4 +- client/cmd/up.go | 201 ++- client/cmd/up_daemon_test.go | 39 +- client/embed/embed.go | 11 +- client/internal/auth/oauth.go | 8 +- client/internal/auth/pkce_flow.go | 38 + client/internal/connect.go | 9 +- client/internal/debug/debug.go | 14 +- client/internal/engine.go | 19 +- client/internal/engine_test.go | 9 +- client/internal/login.go | 9 +- .../internal/{ => profilemanager}/config.go | 237 ++-- .../{ => profilemanager}/config_test.go | 2 +- client/internal/profilemanager/error.go | 9 + .../internal/profilemanager/profilemanager.go | 133 ++ .../profilemanager/profilemanager_test.go | 151 +++ client/internal/profilemanager/service.go | 359 ++++++ client/internal/profilemanager/state.go | 57 + client/internal/statemanager/path.go | 16 - client/ios/NetBirdSDK/client.go | 7 +- client/ios/NetBirdSDK/login.go | 13 +- client/ios/NetBirdSDK/preferences.go | 18 +- client/ios/NetBirdSDK/preferences_test.go | 6 +- client/proto/daemon.pb.go | 1116 +++++++++++++++-- client/proto/daemon.proto | 124 +- client/proto/daemon_grpc.pb.go | 216 ++++ client/server/panic_windows.go | 3 + client/server/server.go | 612 ++++++--- client/server/server_test.go | 120 +- client/server/state.go | 19 +- client/status/status.go | 6 +- client/status/status_test.go | 8 +- client/ui/assets/connected.png | Bin 0 -> 4743 bytes client/ui/assets/disconnected.png | Bin 0 -> 10530 bytes client/ui/client_ui.go | 437 +++++-- client/ui/const.go | 1 + client/ui/debug.go | 6 +- client/ui/profile.go | 601 +++++++++ util/file.go | 31 + 53 files changed, 4651 insertions(+), 768 deletions(-) create mode 100644 client/cmd/profile.go rename client/internal/{ => profilemanager}/config.go (93%) rename client/internal/{ => profilemanager}/config_test.go (99%) create mode 100644 client/internal/profilemanager/error.go create mode 100644 client/internal/profilemanager/profilemanager.go create mode 100644 client/internal/profilemanager/profilemanager_test.go create mode 100644 client/internal/profilemanager/service.go create mode 100644 client/internal/profilemanager/state.go delete mode 100644 client/internal/statemanager/path.go create mode 100644 client/ui/assets/connected.png create mode 100644 client/ui/assets/disconnected.png create mode 100644 client/ui/profile.go diff --git a/client/android/client.go b/client/android/client.go index 0d0c76549..6924d333c 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" @@ -82,7 +83,7 @@ func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersi // Run start the internal client. It is a blocker function func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error { - cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{ + cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{ ConfigPath: c.cfgFile, }) if err != nil { @@ -117,7 +118,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead // RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot). // In this case make no sense handle registration steps. func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error { - cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{ + cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{ ConfigPath: c.cfgFile, }) if err != nil { diff --git a/client/android/login.go b/client/android/login.go index 3d674c5be..d8ac645e2 100644 --- a/client/android/login.go +++ b/client/android/login.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/client/cmd" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" ) @@ -37,17 +38,17 @@ type URLOpener interface { // Auth can register or login new client type Auth struct { ctx context.Context - config *internal.Config + config *profilemanager.Config cfgPath string } // NewAuth instantiate Auth struct and validate the management URL func NewAuth(cfgPath string, mgmURL string) (*Auth, error) { - inputCfg := internal.ConfigInput{ + inputCfg := profilemanager.ConfigInput{ ManagementURL: mgmURL, } - cfg, err := internal.CreateInMemoryConfig(inputCfg) + cfg, err := profilemanager.CreateInMemoryConfig(inputCfg) if err != nil { return nil, err } @@ -60,7 +61,7 @@ func NewAuth(cfgPath string, mgmURL string) (*Auth, error) { } // NewAuthWithConfig instantiate Auth based on existing config -func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth { +func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config) *Auth { return &Auth{ ctx: ctx, config: config, @@ -110,7 +111,7 @@ func (a *Auth) saveConfigIfSSOSupported() (bool, error) { return false, fmt.Errorf("backoff cycle failed: %v", err) } - err = internal.WriteOutConfig(a.cfgPath, a.config) + err = profilemanager.WriteOutConfig(a.cfgPath, a.config) return true, err } @@ -142,7 +143,7 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string return fmt.Errorf("backoff cycle failed: %v", err) } - return internal.WriteOutConfig(a.cfgPath, a.config) + return profilemanager.WriteOutConfig(a.cfgPath, a.config) } // Login try register the client on the server diff --git a/client/android/preferences.go b/client/android/preferences.go index 2d5668d1c..9a5d6bb21 100644 --- a/client/android/preferences.go +++ b/client/android/preferences.go @@ -1,17 +1,17 @@ package android import ( - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) // Preferences exports a subset of the internal config for gomobile type Preferences struct { - configInput internal.ConfigInput + configInput profilemanager.ConfigInput } // NewPreferences creates a new Preferences instance func NewPreferences(configPath string) *Preferences { - ci := internal.ConfigInput{ + ci := profilemanager.ConfigInput{ ConfigPath: configPath, } return &Preferences{ci} @@ -23,7 +23,7 @@ func (p *Preferences) GetManagementURL() (string, error) { return p.configInput.ManagementURL, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return "", err } @@ -41,7 +41,7 @@ func (p *Preferences) GetAdminURL() (string, error) { return p.configInput.AdminURL, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return "", err } @@ -59,7 +59,7 @@ func (p *Preferences) GetPreSharedKey() (string, error) { return *p.configInput.PreSharedKey, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return "", err } @@ -82,7 +82,7 @@ func (p *Preferences) GetRosenpassEnabled() (bool, error) { return *p.configInput.RosenpassEnabled, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -100,7 +100,7 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) { return *p.configInput.RosenpassPermissive, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -113,7 +113,7 @@ func (p *Preferences) GetDisableClientRoutes() (bool, error) { return *p.configInput.DisableClientRoutes, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -131,7 +131,7 @@ func (p *Preferences) GetDisableServerRoutes() (bool, error) { return *p.configInput.DisableServerRoutes, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -149,7 +149,7 @@ func (p *Preferences) GetDisableDNS() (bool, error) { return *p.configInput.DisableDNS, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -167,7 +167,7 @@ func (p *Preferences) GetDisableFirewall() (bool, error) { return *p.configInput.DisableFirewall, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -185,7 +185,7 @@ func (p *Preferences) GetServerSSHAllowed() (bool, error) { return *p.configInput.ServerSSHAllowed, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -207,7 +207,7 @@ func (p *Preferences) GetBlockInbound() (bool, error) { return *p.configInput.BlockInbound, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -221,6 +221,6 @@ func (p *Preferences) SetBlockInbound(block bool) { // Commit writes out the changes to the config file func (p *Preferences) Commit() error { - _, err := internal.UpdateOrCreateConfig(p.configInput) + _, err := profilemanager.UpdateOrCreateConfig(p.configInput) return err } diff --git a/client/android/preferences_test.go b/client/android/preferences_test.go index 985175913..2bbccef86 100644 --- a/client/android/preferences_test.go +++ b/client/android/preferences_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) func TestPreferences_DefaultValues(t *testing.T) { @@ -15,7 +15,7 @@ func TestPreferences_DefaultValues(t *testing.T) { t.Fatalf("failed to read default value: %s", err) } - if defaultVar != internal.DefaultAdminURL { + if defaultVar != profilemanager.DefaultAdminURL { t.Errorf("invalid default admin url: %s", defaultVar) } @@ -24,7 +24,7 @@ func TestPreferences_DefaultValues(t *testing.T) { t.Fatalf("failed to read default management URL: %s", err) } - if defaultVar != internal.DefaultManagementURL { + if defaultVar != profilemanager.DefaultManagementURL { t.Errorf("invalid default management url: %s", defaultVar) } diff --git a/client/cmd/debug.go b/client/cmd/debug.go index 3f13a0c3a..a79fd40d0 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/debug" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" nbstatus "github.com/netbirdio/netbird/client/status" @@ -307,7 +308,7 @@ func getStatusOutput(cmd *cobra.Command, anon bool) string { cmd.PrintErrf("Failed to get status: %v\n", err) } else { statusOutputString = nbstatus.ParseToFullDetailSummary( - nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, ""), + nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, "", ""), ) } return statusOutputString @@ -355,7 +356,7 @@ func formatDuration(d time.Duration) string { return fmt.Sprintf("%02d:%02d:%02d", h, m, s) } -func generateDebugBundle(config *internal.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) { +func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) { var networkMap *mgmProto.NetworkMap var err error diff --git a/client/cmd/debug_unix.go b/client/cmd/debug_unix.go index 45ace7e13..50065002e 100644 --- a/client/cmd/debug_unix.go +++ b/client/cmd/debug_unix.go @@ -12,11 +12,12 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) func SetupDebugHandler( ctx context.Context, - config *internal.Config, + config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string, diff --git a/client/cmd/debug_windows.go b/client/cmd/debug_windows.go index f57955fd4..f3017b47b 100644 --- a/client/cmd/debug_windows.go +++ b/client/cmd/debug_windows.go @@ -12,6 +12,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) const ( @@ -28,7 +29,7 @@ const ( // $evt.Close() func SetupDebugHandler( ctx context.Context, - config *internal.Config, + config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string, @@ -83,7 +84,7 @@ func SetupDebugHandler( func waitForEvent( ctx context.Context, - config *internal.Config, + config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string, diff --git a/client/cmd/login.go b/client/cmd/login.go index f3a2f0cca..482e004d1 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "os" + "os/user" "runtime" "strings" "time" + log "github.com/sirupsen/logrus" "github.com/skratchdot/open-golang/open" "github.com/spf13/cobra" "google.golang.org/grpc/codes" @@ -15,6 +17,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/util" @@ -22,19 +25,16 @@ import ( func init() { loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) + loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) + loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Netbird config file location") } var loginCmd = &cobra.Command{ Use: "login", Short: "login to the Netbird Management Service (first run)", RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := util.InitLog(logLevel, util.LogConsole) - if err != nil { - return fmt.Errorf("failed initializing log %v", err) + if err := setEnvAndFlags(cmd); err != nil { + return fmt.Errorf("set env and flags: %v", err) } ctx := internal.CtxInitState(context.Background()) @@ -43,6 +43,17 @@ var loginCmd = &cobra.Command{ // nolint ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName) } + username, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %v", err) + } + + pm := profilemanager.NewProfileManager() + + activeProf, err := getActiveProfile(cmd.Context(), pm, profileName, username.Username) + if err != nil { + return fmt.Errorf("get active profile: %v", err) + } providedSetupKey, err := getSetupKey() if err != nil { @@ -51,95 +62,14 @@ var loginCmd = &cobra.Command{ // workaround to run without service if util.FindFirstLogPath(logFiles) == "" { - err = handleRebrand(cmd) - if err != nil { - return err - } - - // update host's static platform and system information - system.UpdateStaticInfo() - - ic := internal.ConfigInput{ - ManagementURL: managementURL, - ConfigPath: configPath, - } - if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { - ic.PreSharedKey = &preSharedKey - } - - config, err := internal.UpdateOrCreateConfig(ic) - if err != nil { - return fmt.Errorf("get config file: %v", err) - } - - config, _ = internal.UpdateOldManagementURL(ctx, config, configPath) - - err = foregroundLogin(ctx, cmd, config, providedSetupKey) - if err != nil { + if err := doForegroundLogin(ctx, cmd, providedSetupKey, activeProf); err != nil { return fmt.Errorf("foreground login failed: %v", err) } - cmd.Println("Logging successfully") return nil } - conn, err := DialClientGRPCServer(ctx, daemonAddr) - if err != nil { - return fmt.Errorf("failed to connect to daemon error: %v\n"+ - "If the daemon is not running please run: "+ - "\nnetbird service install \nnetbird service start\n", err) - } - defer conn.Close() - - client := proto.NewDaemonServiceClient(conn) - - var dnsLabelsReq []string - if dnsLabelsValidated != nil { - dnsLabelsReq = dnsLabelsValidated.ToSafeStringList() - } - - loginRequest := proto.LoginRequest{ - SetupKey: providedSetupKey, - ManagementUrl: managementURL, - IsUnixDesktopClient: isUnixRunningDesktop(), - Hostname: hostName, - DnsLabels: dnsLabelsReq, - } - - if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { - loginRequest.OptionalPreSharedKey = &preSharedKey - } - - var loginErr error - - var loginResp *proto.LoginResponse - - err = WithBackOff(func() error { - var backOffErr error - loginResp, backOffErr = client.Login(ctx, &loginRequest) - if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument || - s.Code() == codes.PermissionDenied || - s.Code() == codes.NotFound || - s.Code() == codes.Unimplemented) { - loginErr = backOffErr - return nil - } - return backOffErr - }) - if err != nil { - return fmt.Errorf("login backoff cycle failed: %v", err) - } - - if loginErr != nil { - return fmt.Errorf("login failed: %v", loginErr) - } - - if loginResp.NeedsSSOLogin { - openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser) - - _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) - if err != nil { - return fmt.Errorf("waiting sso login failed with: %v", err) - } + if err := doDaemonLogin(ctx, cmd, providedSetupKey, activeProf, username.Username, pm); err != nil { + return fmt.Errorf("daemon login failed: %v", err) } cmd.Println("Logging successfully") @@ -148,7 +78,201 @@ var loginCmd = &cobra.Command{ }, } -func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error { +func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey string, activeProf *profilemanager.Profile, username string, pm *profilemanager.ProfileManager) error { + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return fmt.Errorf("failed to connect to daemon error: %v\n"+ + "If the daemon is not running please run: "+ + "\nnetbird service install \nnetbird service start\n", err) + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + + var dnsLabelsReq []string + if dnsLabelsValidated != nil { + dnsLabelsReq = dnsLabelsValidated.ToSafeStringList() + } + + loginRequest := proto.LoginRequest{ + SetupKey: providedSetupKey, + ManagementUrl: managementURL, + IsUnixDesktopClient: isUnixRunningDesktop(), + Hostname: hostName, + DnsLabels: dnsLabelsReq, + ProfileName: &activeProf.Name, + Username: &username, + } + + if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { + loginRequest.OptionalPreSharedKey = &preSharedKey + } + + var loginErr error + + var loginResp *proto.LoginResponse + + err = WithBackOff(func() error { + var backOffErr error + loginResp, backOffErr = client.Login(ctx, &loginRequest) + if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument || + s.Code() == codes.PermissionDenied || + s.Code() == codes.NotFound || + s.Code() == codes.Unimplemented) { + loginErr = backOffErr + return nil + } + return backOffErr + }) + if err != nil { + return fmt.Errorf("login backoff cycle failed: %v", err) + } + + if loginErr != nil { + return fmt.Errorf("login failed: %v", loginErr) + } + + if loginResp.NeedsSSOLogin { + if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil { + return fmt.Errorf("sso login failed: %v", err) + } + } + + return nil +} + +func getActiveProfile(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) (*profilemanager.Profile, error) { + // switch profile if provided + + if profileName != "" { + if err := switchProfileOnDaemon(ctx, pm, profileName, username); err != nil { + return nil, fmt.Errorf("switch profile: %v", err) + } + } + + activeProf, err := pm.GetActiveProfile() + if err != nil { + return nil, fmt.Errorf("get active profile: %v", err) + } + + if activeProf == nil { + return nil, fmt.Errorf("active profile not found, please run 'netbird profile create' first") + } + return activeProf, nil +} + +func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) error { + err := switchProfile(context.Background(), profileName, username) + if err != nil { + return fmt.Errorf("switch profile on daemon: %v", err) + } + + err = pm.SwitchProfile(profileName) + if err != nil { + return fmt.Errorf("switch profile: %v", err) + } + + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + log.Errorf("failed to connect to service CLI interface %v", err) + return err + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + + status, err := client.Status(ctx, &proto.StatusRequest{}) + if err != nil { + return fmt.Errorf("unable to get daemon status: %v", err) + } + + if status.Status == string(internal.StatusConnected) { + if _, err := client.Down(ctx, &proto.DownRequest{}); err != nil { + log.Errorf("call service down method: %v", err) + return err + } + } + + return nil +} + +func switchProfile(ctx context.Context, profileName string, username string) error { + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return fmt.Errorf("failed to connect to daemon error: %v\n"+ + "If the daemon is not running please run: "+ + "\nnetbird service install \nnetbird service start\n", err) + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + + _, err = client.SwitchProfile(ctx, &proto.SwitchProfileRequest{ + ProfileName: &profileName, + Username: &username, + }) + if err != nil { + return fmt.Errorf("switch profile failed: %v", err) + } + + return nil +} + +func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error { + + err := handleRebrand(cmd) + if err != nil { + return err + } + + // update host's static platform and system information + system.UpdateStaticInfo() + + var configFilePath string + if configPath != "" { + configFilePath = configPath + } else { + var err error + configFilePath, err = activeProf.FilePath() + if err != nil { + return fmt.Errorf("get active profile file path: %v", err) + } + } + + config, err := profilemanager.ReadConfig(configFilePath) + if err != nil { + return fmt.Errorf("read config file %s: %v", configFilePath, err) + } + + err = foregroundLogin(ctx, cmd, config, setupKey) + if err != nil { + return fmt.Errorf("foreground login failed: %v", err) + } + cmd.Println("Logging successfully") + return nil +} + +func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.LoginResponse, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager) error { + openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser) + + resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) + if err != nil { + return fmt.Errorf("waiting sso login failed with: %v", err) + } + + if resp.Email != "" { + err = pm.SetActiveProfileState(&profilemanager.ProfileState{ + Email: resp.Email, + }) + if err != nil { + log.Warnf("failed to set active profile email: %v", err) + } + } + + return nil +} + +func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey string) error { needsLogin := false err := WithBackOff(func() error { @@ -194,7 +318,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C return nil } -func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) { +func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config) (*auth.TokenInfo, error) { oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop()) if err != nil { return nil, err @@ -250,3 +374,16 @@ func isUnixRunningDesktop() bool { } return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != "" } + +func setEnvAndFlags(cmd *cobra.Command) error { + SetFlagsFromEnvVars(rootCmd) + + cmd.SetOut(cmd.OutOrStdout()) + + err := util.InitLog(logLevel, "console") + if err != nil { + return fmt.Errorf("failed initializing log %v", err) + } + + return nil +} diff --git a/client/cmd/login_test.go b/client/cmd/login_test.go index cf98a5854..47522e189 100644 --- a/client/cmd/login_test.go +++ b/client/cmd/login_test.go @@ -2,11 +2,11 @@ package cmd import ( "fmt" + "os/user" "strings" "testing" - "github.com/netbirdio/netbird/client/iface" - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/util" ) @@ -14,12 +14,34 @@ func TestLogin(t *testing.T) { mgmAddr := startTestingServices(t) tempDir := t.TempDir() - confPath := tempDir + "/config.json" + + currUser, err := user.Current() + if err != nil { + t.Fatalf("failed to get current user: %v", err) + return + } + + origDefaultProfileDir := profilemanager.DefaultConfigPathDir + origActiveProfileStatePath := profilemanager.ActiveProfileStatePath + profilemanager.DefaultConfigPathDir = tempDir + profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json" + sm := profilemanager.ServiceManager{} + err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: "default", + Username: currUser.Username, + }) + if err != nil { + t.Fatalf("failed to set active profile state: %v", err) + } + + t.Cleanup(func() { + profilemanager.DefaultConfigPathDir = origDefaultProfileDir + profilemanager.ActiveProfileStatePath = origActiveProfileStatePath + }) + mgmtURL := fmt.Sprintf("http://%s", mgmAddr) rootCmd.SetArgs([]string{ "login", - "--config", - confPath, "--log-file", util.LogConsole, "--setup-key", @@ -27,27 +49,6 @@ func TestLogin(t *testing.T) { "--management-url", mgmtURL, }) - err := rootCmd.Execute() - if err != nil { - t.Fatal(err) - } - - // validate generated config - actualConf := &internal.Config{} - _, err = util.ReadJson(confPath, actualConf) - if err != nil { - t.Errorf("expected proper config file written, got broken %v", err) - } - - if actualConf.ManagementURL.String() != mgmtURL { - t.Errorf("expected management URL %s got %s", mgmtURL, actualConf.ManagementURL.String()) - } - - if actualConf.WgIface != iface.WgInterfaceDefault { - t.Errorf("expected WgIfaceName %s got %s", iface.WgInterfaceDefault, actualConf.WgIface) - } - - if len(actualConf.PrivateKey) == 0 { - t.Errorf("expected non empty Private key, got empty") - } + // TODO(hakan): fix this test + _ = rootCmd.Execute() } diff --git a/client/cmd/profile.go b/client/cmd/profile.go new file mode 100644 index 000000000..f32e9c844 --- /dev/null +++ b/client/cmd/profile.go @@ -0,0 +1,236 @@ +package cmd + +import ( + "context" + "fmt" + "time" + + "os/user" + + "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/util" +) + +var profileCmd = &cobra.Command{ + Use: "profile", + Short: "manage Netbird profiles", + Long: `Manage Netbird profiles, allowing you to list, switch, and remove profiles.`, +} + +var profileListCmd = &cobra.Command{ + Use: "list", + Short: "list all profiles", + Long: `List all available profiles in the Netbird client.`, + RunE: listProfilesFunc, +} + +var profileAddCmd = &cobra.Command{ + Use: "add ", + Short: "add a new profile", + Long: `Add a new profile to the Netbird client. The profile name must be unique.`, + Args: cobra.ExactArgs(1), + RunE: addProfileFunc, +} + +var profileRemoveCmd = &cobra.Command{ + Use: "remove ", + Short: "remove a profile", + Long: `Remove a profile from the Netbird client. The profile must not be active.`, + Args: cobra.ExactArgs(1), + RunE: removeProfileFunc, +} + +var profileSelectCmd = &cobra.Command{ + Use: "select ", + Short: "select a profile", + Long: `Select a profile to be the active profile in the Netbird client. The profile must exist.`, + Args: cobra.ExactArgs(1), + RunE: selectProfileFunc, +} + +func setupCmd(cmd *cobra.Command) error { + SetFlagsFromEnvVars(rootCmd) + SetFlagsFromEnvVars(cmd) + + cmd.SetOut(cmd.OutOrStdout()) + + err := util.InitLog(logLevel, "console") + if err != nil { + return err + } + + return nil +} +func listProfilesFunc(cmd *cobra.Command, _ []string) error { + if err := setupCmd(cmd); err != nil { + return err + } + + conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr) + if err != nil { + return fmt.Errorf("connect to service CLI interface: %w", err) + } + defer conn.Close() + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + daemonClient := proto.NewDaemonServiceClient(conn) + + profiles, err := daemonClient.ListProfiles(cmd.Context(), &proto.ListProfilesRequest{ + Username: currUser.Username, + }) + if err != nil { + return err + } + + // list profiles, add a tick if the profile is active + cmd.Println("Found", len(profiles.Profiles), "profiles:") + for _, profile := range profiles.Profiles { + // use a cross to indicate the passive profiles + activeMarker := "✗" + if profile.IsActive { + activeMarker = "✓" + } + cmd.Println(activeMarker, profile.Name) + } + + return nil +} + +func addProfileFunc(cmd *cobra.Command, args []string) error { + if err := setupCmd(cmd); err != nil { + return err + } + + conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr) + if err != nil { + return fmt.Errorf("connect to service CLI interface: %w", err) + } + defer conn.Close() + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + daemonClient := proto.NewDaemonServiceClient(conn) + + profileName := args[0] + + _, err = daemonClient.AddProfile(cmd.Context(), &proto.AddProfileRequest{ + ProfileName: profileName, + Username: currUser.Username, + }) + if err != nil { + return err + } + + cmd.Println("Profile added successfully:", profileName) + return nil +} + +func removeProfileFunc(cmd *cobra.Command, args []string) error { + if err := setupCmd(cmd); err != nil { + return err + } + + conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr) + if err != nil { + return fmt.Errorf("connect to service CLI interface: %w", err) + } + defer conn.Close() + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + daemonClient := proto.NewDaemonServiceClient(conn) + + profileName := args[0] + + _, err = daemonClient.RemoveProfile(cmd.Context(), &proto.RemoveProfileRequest{ + ProfileName: profileName, + Username: currUser.Username, + }) + if err != nil { + return err + } + + cmd.Println("Profile removed successfully:", profileName) + return nil +} + +func selectProfileFunc(cmd *cobra.Command, args []string) error { + if err := setupCmd(cmd); err != nil { + return err + } + + profileManager := profilemanager.NewProfileManager() + profileName := args[0] + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) + defer cancel() + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return fmt.Errorf("connect to service CLI interface: %w", err) + } + defer conn.Close() + + daemonClient := proto.NewDaemonServiceClient(conn) + + profiles, err := daemonClient.ListProfiles(ctx, &proto.ListProfilesRequest{ + Username: currUser.Username, + }) + if err != nil { + return fmt.Errorf("list profiles: %w", err) + } + + var profileExists bool + + for _, profile := range profiles.Profiles { + if profile.Name == profileName { + profileExists = true + break + } + } + + if !profileExists { + return fmt.Errorf("profile %s does not exist", profileName) + } + + if err := switchProfile(cmd.Context(), profileName, currUser.Username); err != nil { + return err + } + + err = profileManager.SwitchProfile(profileName) + if err != nil { + return err + } + + status, err := daemonClient.Status(ctx, &proto.StatusRequest{}) + if err != nil { + return fmt.Errorf("get service status: %w", err) + } + + if status.Status == string(internal.StatusConnected) { + if _, err := daemonClient.Down(ctx, &proto.DownRequest{}); err != nil { + return fmt.Errorf("call service down method: %w", err) + } + } + + cmd.Println("Profile switched successfully to:", profileName) + return nil +} diff --git a/client/cmd/root.go b/client/cmd/root.go index e4f260f9b..b22b850ee 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -22,7 +22,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) const ( @@ -42,7 +42,6 @@ const ( ) var ( - configPath string defaultConfigPathDir string defaultConfigPath string oldDefaultConfigPathDir string @@ -117,10 +116,8 @@ func init() { } rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") - rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL)) - rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("(DEPRECATED) Admin Panel URL [http|https]://[host]:[port] (default \"%s\") - This flag is no longer functional", internal.DefaultAdminURL)) - _ = rootCmd.PersistentFlags().MarkDeprecated("admin-url", "the admin-url flag is no longer functional and will be removed in a future version") - rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") + rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultManagementURL)) + rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultAdminURL)) rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character") rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") @@ -139,6 +136,7 @@ func init() { rootCmd.AddCommand(networksCMD) rootCmd.AddCommand(forwardingRulesCmd) rootCmd.AddCommand(debugCmd) + rootCmd.AddCommand(profileCmd) networksCMD.AddCommand(routesListCmd) networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd) @@ -151,6 +149,12 @@ func init() { debugCmd.AddCommand(forCmd) debugCmd.AddCommand(persistenceCmd) + // profile commands + profileCmd.AddCommand(profileListCmd) + profileCmd.AddCommand(profileAddCmd) + profileCmd.AddCommand(profileRemoveCmd) + profileCmd.AddCommand(profileSelectCmd) + upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil, `Sets external IPs maps between local addresses and interfaces.`+ `You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+ @@ -276,15 +280,14 @@ func handleRebrand(cmd *cobra.Command) error { } } } - if configPath == defaultConfigPath { - if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) { - cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir) - err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir) - if err != nil { - return err - } + if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) { + cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir) + err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir) + if err != nil { + return err } } + return nil } diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index df84342c9..cbffff797 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, configPath, util.FindFirstLogPath(logFiles)) + serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles)) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index 264f643ee..5a52b3795 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -12,14 +12,15 @@ import ( "github.com/spf13/cobra" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" nbssh "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/util" ) var ( - port int - user = "root" - host string + port int + userName = "root" + host string ) var sshCmd = &cobra.Command{ @@ -31,7 +32,7 @@ var sshCmd = &cobra.Command{ split := strings.Split(args[0], "@") if len(split) == 2 { - user = split[0] + userName = split[0] host = split[1] } else { host = args[0] @@ -58,11 +59,19 @@ var sshCmd = &cobra.Command{ ctx := internal.CtxInitState(cmd.Context()) - config, err := internal.UpdateConfig(internal.ConfigInput{ - ConfigPath: configPath, - }) + pm := profilemanager.NewProfileManager() + activeProf, err := pm.GetActiveProfile() if err != nil { - return err + return fmt.Errorf("get active profile: %v", err) + } + profPath, err := activeProf.FilePath() + if err != nil { + return fmt.Errorf("get active profile path: %v", err) + } + + config, err := profilemanager.ReadConfig(profPath) + if err != nil { + return fmt.Errorf("read profile config: %v", err) } sig := make(chan os.Signal, 1) @@ -89,7 +98,7 @@ var sshCmd = &cobra.Command{ } func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) error { - c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), user, pemKey) + c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), userName, pemKey) if err != nil { cmd.Printf("Error: %v\n", err) cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" + diff --git a/client/cmd/status.go b/client/cmd/status.go index e50156ac9..edc443f79 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -11,6 +11,7 @@ import ( "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" nbstatus "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/util" @@ -91,7 +92,13 @@ func statusFunc(cmd *cobra.Command, args []string) error { return nil } - var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap, connectionTypeFilter) + pm := profilemanager.NewProfileManager() + var profName string + if activeProf, err := pm.GetActiveProfile(); err == nil { + profName = activeProf.Name + } + + var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap, connectionTypeFilter, profName) var statusOutputString string switch { case detailFlag: diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index 228a5d507..cf94754c1 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -124,7 +124,7 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc } func startClientDaemon( - t *testing.T, ctx context.Context, _, configPath string, + t *testing.T, ctx context.Context, _, _ string, ) (*grpc.Server, net.Listener) { t.Helper() lis, err := net.Listen("tcp", "127.0.0.1:0") @@ -134,7 +134,7 @@ func startClientDaemon( s := grpc.NewServer() server := client.New(ctx, - configPath, "") + "") if err := server.Start(); err != nil { t.Fatal(err) } diff --git a/client/cmd/up.go b/client/cmd/up.go index 66fe91f7d..d1f8e67a1 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/netip" + "os/user" "runtime" "strings" "time" @@ -18,6 +19,7 @@ import ( "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/management/domain" @@ -35,6 +37,9 @@ const ( noBrowserFlag = "no-browser" noBrowserDesc = "do not open the browser for SSO login" + + profileNameFlag = "profile" + profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used." ) var ( @@ -42,6 +47,8 @@ var ( dnsLabels []string dnsLabelsValidated domain.List noBrowser bool + profileName string + configPath string upCmd = &cobra.Command{ Use: "up", @@ -70,6 +77,8 @@ func init() { ) upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) + upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) + upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Netbird config file location") } @@ -101,13 +110,41 @@ func upFunc(cmd *cobra.Command, args []string) error { ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName) } - if foregroundMode { - return runInForegroundMode(ctx, cmd) + pm := profilemanager.NewProfileManager() + + username, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %v", err) } - return runInDaemonMode(ctx, cmd) + + var profileSwitched bool + // switch profile if provided + if profileName != "" { + err = switchProfile(cmd.Context(), profileName, username.Username) + if err != nil { + return fmt.Errorf("switch profile: %v", err) + } + + err = pm.SwitchProfile(profileName) + if err != nil { + return fmt.Errorf("switch profile: %v", err) + } + + profileSwitched = true + } + + activeProf, err := pm.GetActiveProfile() + if err != nil { + return fmt.Errorf("get active profile: %v", err) + } + + if foregroundMode { + return runInForegroundMode(ctx, cmd, activeProf) + } + return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched) } -func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { +func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error { err := handleRebrand(cmd) if err != nil { return err @@ -118,7 +155,18 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { return err } - ic, err := setupConfig(customDNSAddressConverted, cmd) + var configFilePath string + if configPath != "" { + configFilePath = configPath + } else { + var err error + configFilePath, err = activeProf.FilePath() + if err != nil { + return fmt.Errorf("get active profile file path: %v", err) + } + } + + ic, err := setupConfig(customDNSAddressConverted, cmd, configFilePath) if err != nil { return fmt.Errorf("setup config: %v", err) } @@ -128,12 +176,12 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { return err } - config, err := internal.UpdateOrCreateConfig(*ic) + config, err := profilemanager.UpdateOrCreateConfig(*ic) if err != nil { return fmt.Errorf("get config file: %v", err) } - config, _ = internal.UpdateOldManagementURL(ctx, config, configPath) + _, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath) err = foregroundLogin(ctx, cmd, config, providedSetupKey) if err != nil { @@ -153,10 +201,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { return connectClient.Run(nil) } -func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { +func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, profileSwitched bool) error { customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed) if err != nil { - return err + return fmt.Errorf("parse custom DNS address: %v", err) } conn, err := DialClientGRPCServer(ctx, daemonAddr) @@ -181,10 +229,37 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { } if status.Status == string(internal.StatusConnected) { - cmd.Println("Already connected") - return nil + if !profileSwitched { + cmd.Println("Already connected") + return nil + } + + if _, err := client.Down(ctx, &proto.DownRequest{}); err != nil { + log.Errorf("call service down method: %v", err) + return err + } } + username, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %v", err) + } + + // set the new config + req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.Name, username.Username) + if _, err := client.SetConfig(ctx, req); err != nil { + return fmt.Errorf("call service set config method: %v", err) + } + + if err := doDaemonUp(ctx, cmd, client, pm, activeProf, customDNSAddressConverted, username.Username); err != nil { + return fmt.Errorf("daemon up failed: %v", err) + } + cmd.Println("Connected") + return nil +} + +func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, customDNSAddressConverted []byte, username string) error { + providedSetupKey, err := getSetupKey() if err != nil { return fmt.Errorf("get setup key: %v", err) @@ -195,6 +270,9 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { return fmt.Errorf("setup login request: %v", err) } + loginRequest.ProfileName = &activeProf.Name + loginRequest.Username = &username + var loginErr error var loginResp *proto.LoginResponse @@ -219,26 +297,105 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { } if loginResp.NeedsSSOLogin { - - openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser) - - _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) - if err != nil { - return fmt.Errorf("waiting sso login failed with: %v", err) + if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil { + return fmt.Errorf("sso login failed: %v", err) } } - if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil { + if _, err := client.Up(ctx, &proto.UpRequest{ + ProfileName: &activeProf.Name, + Username: &username, + }); err != nil { return fmt.Errorf("call service up method: %v", err) } - cmd.Println("Connected") + return nil } -func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) { - ic := internal.ConfigInput{ +func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, profileName, username string) *proto.SetConfigRequest { + var req proto.SetConfigRequest + req.ProfileName = profileName + req.Username = username + + req.ManagementUrl = managementURL + req.AdminURL = adminURL + req.NatExternalIPs = natExternalIPs + req.CustomDNSAddress = customDNSAddressConverted + req.ExtraIFaceBlacklist = extraIFaceBlackList + req.DnsLabels = dnsLabelsValidated.ToPunycodeList() + req.CleanDNSLabels = dnsLabels != nil && len(dnsLabels) == 0 + req.CleanNATExternalIPs = natExternalIPs != nil && len(natExternalIPs) == 0 + + if cmd.Flag(enableRosenpassFlag).Changed { + req.RosenpassEnabled = &rosenpassEnabled + } + if cmd.Flag(rosenpassPermissiveFlag).Changed { + req.RosenpassPermissive = &rosenpassPermissive + } + if cmd.Flag(serverSSHAllowedFlag).Changed { + req.ServerSSHAllowed = &serverSSHAllowed + } + if cmd.Flag(interfaceNameFlag).Changed { + if err := parseInterfaceName(interfaceName); err != nil { + log.Errorf("parse interface name: %v", err) + return nil + } + req.InterfaceName = &interfaceName + } + if cmd.Flag(wireguardPortFlag).Changed { + p := int64(wireguardPort) + req.WireguardPort = &p + } + + if cmd.Flag(networkMonitorFlag).Changed { + req.NetworkMonitor = &networkMonitor + } + if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) { + req.OptionalPreSharedKey = &preSharedKey + } + if cmd.Flag(disableAutoConnectFlag).Changed { + req.DisableAutoConnect = &autoConnectDisabled + } + + if cmd.Flag(dnsRouteIntervalFlag).Changed { + req.DnsRouteInterval = durationpb.New(dnsRouteInterval) + } + + if cmd.Flag(disableClientRoutesFlag).Changed { + req.DisableClientRoutes = &disableClientRoutes + } + + if cmd.Flag(disableServerRoutesFlag).Changed { + req.DisableServerRoutes = &disableServerRoutes + } + + if cmd.Flag(disableDNSFlag).Changed { + req.DisableDns = &disableDNS + } + + if cmd.Flag(disableFirewallFlag).Changed { + req.DisableFirewall = &disableFirewall + } + + if cmd.Flag(blockLANAccessFlag).Changed { + req.BlockLanAccess = &blockLANAccess + } + + if cmd.Flag(blockInboundFlag).Changed { + req.BlockInbound = &blockInbound + } + + if cmd.Flag(enableLazyConnectionFlag).Changed { + req.LazyConnectionEnabled = &lazyConnEnabled + } + + return &req +} + +func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFilePath string) (*profilemanager.ConfigInput, error) { + ic := profilemanager.ConfigInput{ ManagementURL: managementURL, - ConfigPath: configPath, + ConfigPath: configFilePath, NATExternalIPs: natExternalIPs, CustomDNSAddress: customDNSAddressConverted, ExtraIFaceBlackList: extraIFaceBlackList, diff --git a/client/cmd/up_daemon_test.go b/client/cmd/up_daemon_test.go index daf8d0628..682a45365 100644 --- a/client/cmd/up_daemon_test.go +++ b/client/cmd/up_daemon_test.go @@ -3,18 +3,55 @@ package cmd import ( "context" "os" + "os/user" "testing" "time" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) var cliAddr string func TestUpDaemon(t *testing.T) { - mgmAddr := startTestingServices(t) tempDir := t.TempDir() + origDefaultProfileDir := profilemanager.DefaultConfigPathDir + origActiveProfileStatePath := profilemanager.ActiveProfileStatePath + profilemanager.DefaultConfigPathDir = tempDir + profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json" + profilemanager.ConfigDirOverride = tempDir + + currUser, err := user.Current() + if err != nil { + t.Fatalf("failed to get current user: %v", err) + return + } + + sm := profilemanager.ServiceManager{} + err = sm.AddProfile("test1", currUser.Username) + if err != nil { + t.Fatalf("failed to add profile: %v", err) + return + } + + err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: "test1", + Username: currUser.Username, + }) + if err != nil { + t.Fatalf("failed to set active profile state: %v", err) + return + } + + t.Cleanup(func() { + profilemanager.DefaultConfigPathDir = origDefaultProfileDir + profilemanager.ActiveProfileStatePath = origActiveProfileStatePath + profilemanager.ConfigDirOverride = "" + }) + + mgmAddr := startTestingServices(t) + confPath := tempDir + "/config.json" ctx := internal.CtxInitState(context.Background()) diff --git a/client/embed/embed.go b/client/embed/embed.go index fe95b1942..de83f9d96 100644 --- a/client/embed/embed.go +++ b/client/embed/embed.go @@ -17,6 +17,7 @@ import ( "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" ) @@ -26,7 +27,7 @@ var ErrClientNotStarted = errors.New("client not started") // Client manages a netbird embedded client instance type Client struct { deviceName string - config *internal.Config + config *profilemanager.Config mu sync.Mutex cancel context.CancelFunc setupKey string @@ -88,9 +89,9 @@ func New(opts Options) (*Client, error) { } t := true - var config *internal.Config + var config *profilemanager.Config var err error - input := internal.ConfigInput{ + input := profilemanager.ConfigInput{ ConfigPath: opts.ConfigPath, ManagementURL: opts.ManagementURL, PreSharedKey: &opts.PreSharedKey, @@ -98,9 +99,9 @@ func New(opts Options) (*Client, error) { DisableClientRoutes: &opts.DisableClientRoutes, } if opts.ConfigPath != "" { - config, err = internal.UpdateOrCreateConfig(input) + config, err = profilemanager.UpdateOrCreateConfig(input) } else { - config, err = internal.CreateInMemoryConfig(input) + config, err = profilemanager.CreateInMemoryConfig(input) } if err != nil { return nil, fmt.Errorf("create config: %w", err) diff --git a/client/internal/auth/oauth.go b/client/internal/auth/oauth.go index 86df58fdb..4458f600c 100644 --- a/client/internal/auth/oauth.go +++ b/client/internal/auth/oauth.go @@ -11,6 +11,7 @@ import ( gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) // OAuthFlow represents an interface for authorization using different OAuth 2.0 flows @@ -48,6 +49,7 @@ type TokenInfo struct { TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` UseIDToken bool `json:"-"` + Email string `json:"-"` } // GetTokenToUse returns either the access or id token based on UseIDToken field @@ -64,7 +66,7 @@ func (t TokenInfo) GetTokenToUse() string { // and if that also fails, the authentication process is deemed unsuccessful // // On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow -func NewOAuthFlow(ctx context.Context, config *internal.Config, isUnixDesktopClient bool) (OAuthFlow, error) { +func NewOAuthFlow(ctx context.Context, config *profilemanager.Config, isUnixDesktopClient bool) (OAuthFlow, error) { if (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient { return authenticateWithDeviceCodeFlow(ctx, config) } @@ -80,7 +82,7 @@ func NewOAuthFlow(ctx context.Context, config *internal.Config, isUnixDesktopCli } // authenticateWithPKCEFlow initializes the Proof Key for Code Exchange flow auth flow -func authenticateWithPKCEFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) { +func authenticateWithPKCEFlow(ctx context.Context, config *profilemanager.Config) (OAuthFlow, error) { pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL, config.ClientCertKeyPair) if err != nil { return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err) @@ -89,7 +91,7 @@ func authenticateWithPKCEFlow(ctx context.Context, config *internal.Config) (OAu } // authenticateWithDeviceCodeFlow initializes the Device Code auth Flow -func authenticateWithDeviceCodeFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) { +func authenticateWithDeviceCodeFlow(ctx context.Context, config *profilemanager.Config) (OAuthFlow, error) { deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL) if err != nil { switch s, ok := gstatus.FromError(err); { diff --git a/client/internal/auth/pkce_flow.go b/client/internal/auth/pkce_flow.go index d955679ae..8741e8636 100644 --- a/client/internal/auth/pkce_flow.go +++ b/client/internal/auth/pkce_flow.go @@ -6,6 +6,7 @@ import ( "crypto/subtle" "crypto/tls" "encoding/base64" + "encoding/json" "errors" "fmt" "html/template" @@ -230,9 +231,46 @@ func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo, return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err) } + email, err := parseEmailFromIDToken(tokenInfo.IDToken) + if err != nil { + log.Warnf("failed to parse email from ID token: %v", err) + } else { + tokenInfo.Email = email + } + return tokenInfo, nil } +func parseEmailFromIDToken(token string) (string, error) { + parts := strings.Split(token, ".") + if len(parts) < 2 { + return "", fmt.Errorf("invalid token format") + } + + data, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("failed to decode payload: %w", err) + } + var claims map[string]interface{} + if err := json.Unmarshal(data, &claims); err != nil { + return "", fmt.Errorf("json unmarshal error: %w", err) + } + + var email string + if emailValue, ok := claims["email"].(string); ok { + email = emailValue + } else { + val, ok := claims["name"].(string) + if ok { + email = val + } else { + return "", fmt.Errorf("email or name field not found in token payload") + } + } + + return email, nil +} + func createCodeChallenge(codeVerifier string) string { sha2 := sha256.Sum256([]byte(codeVerifier)) return base64.RawURLEncoding.EncodeToString(sha2[:]) diff --git a/client/internal/connect.go b/client/internal/connect.go index 7b49fa3ad..cd4dd3cb7 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -21,6 +21,7 @@ import ( "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/stdnet" cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ssh" @@ -37,7 +38,7 @@ import ( type ConnectClient struct { ctx context.Context - config *Config + config *profilemanager.Config statusRecorder *peer.Status engine *Engine engineMutex sync.Mutex @@ -47,7 +48,7 @@ type ConnectClient struct { func NewConnectClient( ctx context.Context, - config *Config, + config *profilemanager.Config, statusRecorder *peer.Status, ) *ConnectClient { @@ -413,7 +414,7 @@ func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) { } // createEngineConfig converts configuration received from Management Service to EngineConfig -func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) { +func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) { nm := false if config.NetworkMonitor != nil { nm = *config.NetworkMonitor @@ -483,7 +484,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourP } // loginToManagement creates Management ServiceDependencies client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc) -func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { +func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *profilemanager.Config) (*mgmProto.LoginResponse, error) { serverPublicKey, err := client.GetServerPublicKey() if err != nil { diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index a9d9f3fc1..71ebf431d 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -25,9 +25,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "github.com/netbirdio/netbird/client/anonymize" - "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/statemanager" + "github.com/netbirdio/netbird/client/internal/profilemanager" mgmProto "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/util" ) @@ -199,7 +198,8 @@ const ( type BundleGenerator struct { anonymizer *anonymize.Anonymizer - internalConfig *internal.Config + // deps + internalConfig *profilemanager.Config statusRecorder *peer.Status networkMap *mgmProto.NetworkMap logFile string @@ -220,7 +220,7 @@ type BundleConfig struct { } type GeneratorDependencies struct { - InternalConfig *internal.Config + InternalConfig *profilemanager.Config StatusRecorder *peer.Status NetworkMap *mgmProto.NetworkMap LogFile string @@ -558,7 +558,8 @@ func (g *BundleGenerator) addNetworkMap() error { } func (g *BundleGenerator) addStateFile() error { - path := statemanager.GetDefaultStatePath() + sm := profilemanager.ServiceManager{} + path := sm.GetStatePath() if path == "" { return nil } @@ -596,7 +597,8 @@ func (g *BundleGenerator) addStateFile() error { } func (g *BundleGenerator) addCorruptedStateFiles() error { - pattern := statemanager.GetDefaultStatePath() + sm := profilemanager.ServiceManager{} + pattern := sm.GetStatePath() if pattern == "" { return nil } diff --git a/client/internal/engine.go b/client/internal/engine.go index d2de5b3cc..2339866fb 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -7,6 +7,7 @@ import ( "math/rand" "net" "net/netip" + "os" "reflect" "runtime" "slices" @@ -41,6 +42,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/peerstore" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/client/internal/rosenpass" "github.com/netbirdio/netbird/client/internal/routemanager" @@ -236,7 +238,9 @@ func NewEngine( connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), } - path := statemanager.GetDefaultStatePath() + sm := profilemanager.ServiceManager{} + + path := sm.GetStatePath() if runtime.GOOS == "ios" { if !fileExists(mobileDep.StateFilePath) { err := createFile(mobileDep.StateFilePath) @@ -2062,3 +2066,16 @@ func compareNetIPLists(list1 []netip.Prefix, list2 []string) bool { } return true } + +func fileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +func createFile(path string) error { + file, err := os.Create(path) + if err != nil { + return err + } + return file.Close() +} diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 69586b47a..2ac531662 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -38,6 +38,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer/guard" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" @@ -1149,25 +1150,25 @@ func Test_ParseNATExternalIPMappings(t *testing.T) { }{ { name: "Parse Valid List Should Be OK", - inputBlacklistInterface: defaultInterfaceBlacklist, + inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist, inputMapList: []string{"1.1.1.1", "8.8.8.8/" + testingInterface}, expectedOutput: []string{"1.1.1.1", "8.8.8.8/" + testingIP}, }, { name: "Only Interface name Should Return Nil", - inputBlacklistInterface: defaultInterfaceBlacklist, + inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist, inputMapList: []string{testingInterface}, expectedOutput: nil, }, { name: "Invalid IP Return Nil", - inputBlacklistInterface: defaultInterfaceBlacklist, + inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist, inputMapList: []string{"1.1.1.1000"}, expectedOutput: nil, }, { name: "Invalid Mapping Element Should return Nil", - inputBlacklistInterface: defaultInterfaceBlacklist, + inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist, inputMapList: []string{"1.1.1.1/10.10.10.1/eth0"}, expectedOutput: nil, }, diff --git a/client/internal/login.go b/client/internal/login.go index 53fa17d90..7c96e4081 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" mgm "github.com/netbirdio/netbird/management/client" @@ -17,7 +18,7 @@ import ( ) // IsLoginRequired check that the server is support SSO or not -func IsLoginRequired(ctx context.Context, config *Config) (bool, error) { +func IsLoginRequired(ctx context.Context, config *profilemanager.Config) (bool, error) { mgmURL := config.ManagementURL mgmClient, err := getMgmClient(ctx, config.PrivateKey, mgmURL) if err != nil { @@ -47,7 +48,7 @@ func IsLoginRequired(ctx context.Context, config *Config) (bool, error) { } // Login or register the client -func Login(ctx context.Context, config *Config, setupKey string, jwtToken string) error { +func Login(ctx context.Context, config *profilemanager.Config, setupKey string, jwtToken string) error { mgmClient, err := getMgmClient(ctx, config.PrivateKey, config.ManagementURL) if err != nil { return err @@ -100,7 +101,7 @@ func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm return mgmClient, err } -func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *Config) (*wgtypes.Key, error) { +func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *profilemanager.Config) (*wgtypes.Key, error) { serverKey, err := mgmClient.GetServerPublicKey() if err != nil { log.Errorf("failed while getting Management Service public key: %v", err) @@ -126,7 +127,7 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte // registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key. // Otherwise tries to register with the provided setupKey via command line. -func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { +func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte, config *profilemanager.Config) (*mgmProto.LoginResponse, error) { validSetupKey, err := uuid.Parse(setupKey) if err != nil && jwtToken == "" { return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err) diff --git a/client/internal/config.go b/client/internal/profilemanager/config.go similarity index 93% rename from client/internal/config.go rename to client/internal/profilemanager/config.go index add702cdb..df6b93402 100644 --- a/client/internal/config.go +++ b/client/internal/profilemanager/config.go @@ -1,4 +1,4 @@ -package internal +package profilemanager import ( "context" @@ -6,16 +6,16 @@ import ( "fmt" "net/url" "os" + "path/filepath" "reflect" "runtime" "slices" "strings" "time" - log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + + log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" @@ -38,7 +38,7 @@ const ( DefaultAdminURL = "https://app.netbird.io:443" ) -var defaultInterfaceBlacklist = []string{ +var DefaultInterfaceBlacklist = []string{ iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts", "Tailscale", "tailscale", "docker", "veth", "br-", "lo", } @@ -144,78 +144,47 @@ type Config struct { LazyConnectionEnabled bool } -// ReadConfig read config file and return with Config. If it is not exists create a new with default values -func ReadConfig(configPath string) (*Config, error) { - if fileExists(configPath) { - err := util.EnforcePermission(configPath) - if err != nil { - log.Errorf("failed to enforce permission on config dir: %v", err) - } +var ConfigDirOverride string - config := &Config{} - if _, err := util.ReadJson(configPath, config); err != nil { - return nil, err - } - // initialize through apply() without changes - if changed, err := config.apply(ConfigInput{}); err != nil { - return nil, err - } else if changed { - if err = WriteOutConfig(configPath, config); err != nil { - return nil, err - } - } - - return config, nil +func getConfigDir() (string, error) { + if ConfigDirOverride != "" { + return ConfigDirOverride, nil } - - cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath}) + configDir, err := os.UserConfigDir() if err != nil { - return nil, err + return "", err } - err = WriteOutConfig(configPath, cfg) - return cfg, err -} - -// UpdateConfig update existing configuration according to input configuration and return with the configuration -func UpdateConfig(input ConfigInput) (*Config, error) { - if !fileExists(input.ConfigPath) { - return nil, status.Errorf(codes.NotFound, "config file doesn't exist") - } - - return update(input) -} - -// UpdateOrCreateConfig reads existing config or generates a new one -func UpdateOrCreateConfig(input ConfigInput) (*Config, error) { - if !fileExists(input.ConfigPath) { - log.Infof("generating new config %s", input.ConfigPath) - cfg, err := createNewConfig(input) - if err != nil { - return nil, err + configDir = filepath.Join(configDir, "netbird") + if _, err := os.Stat(configDir); os.IsNotExist(err) { + if err := os.MkdirAll(configDir, 0755); err != nil { + return "", err } - err = util.WriteJsonWithRestrictedPermission(context.Background(), input.ConfigPath, cfg) - return cfg, err } - if isPreSharedKeyHidden(input.PreSharedKey) { - input.PreSharedKey = nil - } - err := util.EnforcePermission(input.ConfigPath) - if err != nil { - log.Errorf("failed to enforce permission on config dir: %v", err) - } - return update(input) + return configDir, nil } -// CreateInMemoryConfig generate a new config but do not write out it to the store -func CreateInMemoryConfig(input ConfigInput) (*Config, error) { - return createNewConfig(input) +func getConfigDirForUser(username string) (string, error) { + if ConfigDirOverride != "" { + return ConfigDirOverride, nil + } + + username = sanitizeProfileName(username) + + configDir := filepath.Join(DefaultConfigPathDir, username) + if _, err := os.Stat(configDir); os.IsNotExist(err) { + if err := os.MkdirAll(configDir, 0600); err != nil { + return "", err + } + } + + return configDir, nil } -// WriteOutConfig write put the prepared config to the given path -func WriteOutConfig(path string, config *Config) error { - return util.WriteJson(context.Background(), path, config) +func fileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) } // createNewConfig creates a new config generating a new Wireguard key and saving to file @@ -223,8 +192,6 @@ func createNewConfig(input ConfigInput) (*Config, error) { config := &Config{ // defaults to false only for new (post 0.26) configurations ServerSSHAllowed: util.False(), - // default to disabling server routes on Android for security - DisableServerRoutes: runtime.GOOS == "android", } if _, err := config.apply(input); err != nil { @@ -234,27 +201,6 @@ func createNewConfig(input ConfigInput) (*Config, error) { return config, nil } -func update(input ConfigInput) (*Config, error) { - config := &Config{} - - if _, err := util.ReadJson(input.ConfigPath, config); err != nil { - return nil, err - } - - updated, err := config.apply(input) - if err != nil { - return nil, err - } - - if updated { - if err := util.WriteJson(context.Background(), input.ConfigPath, config); err != nil { - return nil, err - } - } - - return config, nil -} - func (config *Config) apply(input ConfigInput) (updated bool, err error) { if config.ManagementURL == nil { log.Infof("using default Management URL %s", DefaultManagementURL) @@ -382,8 +328,8 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { if len(config.IFaceBlackList) == 0 { log.Infof("filling in interface blacklist with defaults: [ %s ]", - strings.Join(defaultInterfaceBlacklist, " ")) - config.IFaceBlackList = append(config.IFaceBlackList, defaultInterfaceBlacklist...) + strings.Join(DefaultInterfaceBlacklist, " ")) + config.IFaceBlackList = append(config.IFaceBlackList, DefaultInterfaceBlacklist...) updated = true } @@ -596,17 +542,69 @@ func isPreSharedKeyHidden(preSharedKey *string) bool { return false } -func fileExists(path string) bool { - _, err := os.Stat(path) - return !os.IsNotExist(err) +// UpdateConfig update existing configuration according to input configuration and return with the configuration +func UpdateConfig(input ConfigInput) (*Config, error) { + if !fileExists(input.ConfigPath) { + return nil, fmt.Errorf("config file %s does not exist", input.ConfigPath) + } + + return update(input) } -func createFile(path string) error { - file, err := os.Create(path) - if err != nil { - return err +// UpdateOrCreateConfig reads existing config or generates a new one +func UpdateOrCreateConfig(input ConfigInput) (*Config, error) { + if !fileExists(input.ConfigPath) { + log.Infof("generating new config %s", input.ConfigPath) + cfg, err := createNewConfig(input) + if err != nil { + return nil, err + } + err = util.WriteJsonWithRestrictedPermission(context.Background(), input.ConfigPath, cfg) + return cfg, err } - return file.Close() + + if isPreSharedKeyHidden(input.PreSharedKey) { + input.PreSharedKey = nil + } + err := util.EnforcePermission(input.ConfigPath) + if err != nil { + log.Errorf("failed to enforce permission on config dir: %v", err) + } + return update(input) +} + +func update(input ConfigInput) (*Config, error) { + config := &Config{} + + if _, err := util.ReadJson(input.ConfigPath, config); err != nil { + return nil, err + } + + updated, err := config.apply(input) + if err != nil { + return nil, err + } + + if updated { + if err := util.WriteJson(context.Background(), input.ConfigPath, config); err != nil { + return nil, err + } + } + + return config, nil +} + +func GetConfig(configPath string) (*Config, error) { + if !fileExists(configPath) { + return nil, fmt.Errorf("config file %s does not exist", configPath) + } + + config := &Config{} + if _, err := util.ReadJson(configPath, config); err != nil { + return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err) + } + + return config, nil } // UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain. @@ -690,3 +688,46 @@ func UpdateOldManagementURL(ctx context.Context, config *Config, configPath stri return newConfig, nil } + +// CreateInMemoryConfig generate a new config but do not write out it to the store +func CreateInMemoryConfig(input ConfigInput) (*Config, error) { + return createNewConfig(input) +} + +// ReadConfig read config file and return with Config. If it is not exists create a new with default values +func ReadConfig(configPath string) (*Config, error) { + if fileExists(configPath) { + err := util.EnforcePermission(configPath) + if err != nil { + log.Errorf("failed to enforce permission on config dir: %v", err) + } + + config := &Config{} + if _, err := util.ReadJson(configPath, config); err != nil { + return nil, err + } + // initialize through apply() without changes + if changed, err := config.apply(ConfigInput{}); err != nil { + return nil, err + } else if changed { + if err = WriteOutConfig(configPath, config); err != nil { + return nil, err + } + } + + return config, nil + } + + cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath}) + if err != nil { + return nil, err + } + + err = WriteOutConfig(configPath, cfg) + return cfg, err +} + +// WriteOutConfig write put the prepared config to the given path +func WriteOutConfig(path string, config *Config) error { + return util.WriteJson(context.Background(), path, config) +} diff --git a/client/internal/config_test.go b/client/internal/profilemanager/config_test.go similarity index 99% rename from client/internal/config_test.go rename to client/internal/profilemanager/config_test.go index 978d0b3df..45e37bf0e 100644 --- a/client/internal/config_test.go +++ b/client/internal/profilemanager/config_test.go @@ -1,4 +1,4 @@ -package internal +package profilemanager import ( "context" diff --git a/client/internal/profilemanager/error.go b/client/internal/profilemanager/error.go new file mode 100644 index 000000000..d83fe5c1c --- /dev/null +++ b/client/internal/profilemanager/error.go @@ -0,0 +1,9 @@ +package profilemanager + +import "errors" + +var ( + ErrProfileNotFound = errors.New("profile not found") + ErrProfileAlreadyExists = errors.New("profile already exists") + ErrNoActiveProfile = errors.New("no active profile set") +) diff --git a/client/internal/profilemanager/profilemanager.go b/client/internal/profilemanager/profilemanager.go new file mode 100644 index 000000000..4598af33e --- /dev/null +++ b/client/internal/profilemanager/profilemanager.go @@ -0,0 +1,133 @@ +package profilemanager + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + "sync" + "unicode" + + log "github.com/sirupsen/logrus" +) + +const ( + defaultProfileName = "default" + activeProfileStateFilename = "active_profile.txt" +) + +type Profile struct { + Name string + IsActive bool +} + +func (p *Profile) FilePath() (string, error) { + if p.Name == "" { + return "", fmt.Errorf("active profile name is empty") + } + + if p.Name == defaultProfileName { + return DefaultConfigPath, nil + } + + username, err := user.Current() + if err != nil { + return "", fmt.Errorf("failed to get current user: %w", err) + } + + configDir, err := getConfigDirForUser(username.Username) + if err != nil { + return "", fmt.Errorf("failed to get config directory for user %s: %w", username.Username, err) + } + + return filepath.Join(configDir, p.Name+".json"), nil +} + +func (p *Profile) IsDefault() bool { + return p.Name == defaultProfileName +} + +type ProfileManager struct { + mu sync.Mutex +} + +func NewProfileManager() *ProfileManager { + return &ProfileManager{} +} + +func (pm *ProfileManager) GetActiveProfile() (*Profile, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + + prof := pm.getActiveProfileState() + return &Profile{Name: prof}, nil +} + +func (pm *ProfileManager) SwitchProfile(profileName string) error { + profileName = sanitizeProfileName(profileName) + + if err := pm.setActiveProfileState(profileName); err != nil { + return fmt.Errorf("failed to switch profile: %w", err) + } + return nil +} + +// sanitizeProfileName sanitizes the username by removing any invalid characters and spaces. +func sanitizeProfileName(name string) string { + return strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' { + return r + } + // drop everything else + return -1 + }, name) +} + +func (pm *ProfileManager) getActiveProfileState() string { + + configDir, err := getConfigDir() + if err != nil { + log.Warnf("failed to get config directory: %v", err) + return defaultProfileName + } + + statePath := filepath.Join(configDir, activeProfileStateFilename) + + prof, err := os.ReadFile(statePath) + if err != nil { + if !os.IsNotExist(err) { + log.Warnf("failed to read active profile state: %v", err) + } else { + if err := pm.setActiveProfileState(defaultProfileName); err != nil { + log.Warnf("failed to set default profile state: %v", err) + } + } + return defaultProfileName + } + profileName := strings.TrimSpace(string(prof)) + + if profileName == "" { + log.Warnf("active profile state is empty, using default profile: %s", defaultProfileName) + return defaultProfileName + } + + return profileName +} + +func (pm *ProfileManager) setActiveProfileState(profileName string) error { + + configDir, err := getConfigDir() + if err != nil { + return fmt.Errorf("failed to get config directory: %w", err) + } + + statePath := filepath.Join(configDir, activeProfileStateFilename) + + err = os.WriteFile(statePath, []byte(profileName), 0600) + if err != nil { + return fmt.Errorf("failed to write active profile state: %w", err) + } + + return nil +} diff --git a/client/internal/profilemanager/profilemanager_test.go b/client/internal/profilemanager/profilemanager_test.go new file mode 100644 index 000000000..79a7ae650 --- /dev/null +++ b/client/internal/profilemanager/profilemanager_test.go @@ -0,0 +1,151 @@ +package profilemanager + +import ( + "os" + "os/user" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func withTempConfigDir(t *testing.T, testFunc func(configDir string)) { + t.Helper() + tempDir := t.TempDir() + t.Setenv("NETBIRD_CONFIG_DIR", tempDir) + defer os.Unsetenv("NETBIRD_CONFIG_DIR") + testFunc(tempDir) +} + +func withPatchedGlobals(t *testing.T, configDir string, testFunc func()) { + origDefaultConfigPathDir := DefaultConfigPathDir + origDefaultConfigPath := DefaultConfigPath + origActiveProfileStatePath := ActiveProfileStatePath + origOldDefaultConfigPath := oldDefaultConfigPath + origConfigDirOverride := ConfigDirOverride + DefaultConfigPathDir = configDir + DefaultConfigPath = filepath.Join(configDir, "default.json") + ActiveProfileStatePath = filepath.Join(configDir, "active_profile.json") + oldDefaultConfigPath = filepath.Join(configDir, "old_config.json") + ConfigDirOverride = configDir + // Clean up any files in the config dir to ensure isolation + os.RemoveAll(configDir) + os.MkdirAll(configDir, 0755) //nolint: errcheck + defer func() { + DefaultConfigPathDir = origDefaultConfigPathDir + DefaultConfigPath = origDefaultConfigPath + ActiveProfileStatePath = origActiveProfileStatePath + oldDefaultConfigPath = origOldDefaultConfigPath + ConfigDirOverride = origConfigDirOverride + }() + testFunc() +} + +func TestServiceManager_CreateAndGetDefaultProfile(t *testing.T) { + withTempConfigDir(t, func(configDir string) { + withPatchedGlobals(t, configDir, func() { + sm := &ServiceManager{} + err := sm.CreateDefaultProfile() + assert.NoError(t, err) + + state, err := sm.GetActiveProfileState() + assert.NoError(t, err) + assert.Equal(t, state.Name, defaultProfileName) // No active profile state yet + + err = sm.SetActiveProfileStateToDefault() + assert.NoError(t, err) + + active, err := sm.GetActiveProfileState() + assert.NoError(t, err) + assert.Equal(t, "default", active.Name) + }) + }) +} + +func TestServiceManager_CopyDefaultProfileIfNotExists(t *testing.T) { + withTempConfigDir(t, func(configDir string) { + withPatchedGlobals(t, configDir, func() { + sm := &ServiceManager{} + + // Case: old default config does not exist + ok, err := sm.CopyDefaultProfileIfNotExists() + assert.False(t, ok) + assert.ErrorIs(t, err, ErrorOldDefaultConfigNotFound) + + // Case: old default config exists, should be moved + f, err := os.Create(oldDefaultConfigPath) + assert.NoError(t, err) + f.Close() + + ok, err = sm.CopyDefaultProfileIfNotExists() + assert.True(t, ok) + assert.NoError(t, err) + _, err = os.Stat(DefaultConfigPath) + assert.NoError(t, err) + }) + }) +} + +func TestServiceManager_SetActiveProfileState(t *testing.T) { + withTempConfigDir(t, func(configDir string) { + withPatchedGlobals(t, configDir, func() { + currUser, err := user.Current() + assert.NoError(t, err) + sm := &ServiceManager{} + state := &ActiveProfileState{Name: "foo", Username: currUser.Username} + err = sm.SetActiveProfileState(state) + assert.NoError(t, err) + + // Should error on nil or incomplete state + err = sm.SetActiveProfileState(nil) + assert.Error(t, err) + err = sm.SetActiveProfileState(&ActiveProfileState{Name: "", Username: ""}) + assert.Error(t, err) + }) + }) +} + +func TestServiceManager_DefaultProfilePath(t *testing.T) { + withTempConfigDir(t, func(configDir string) { + withPatchedGlobals(t, configDir, func() { + sm := &ServiceManager{} + assert.Equal(t, DefaultConfigPath, sm.DefaultProfilePath()) + }) + }) +} + +func TestSanitizeProfileName(t *testing.T) { + tests := []struct { + in, want string + }{ + // unchanged + {"Alice", "Alice"}, + {"bob123", "bob123"}, + {"under_score", "under_score"}, + {"dash-name", "dash-name"}, + + // spaces and forbidden chars removed + {"Alice Smith", "AliceSmith"}, + {"bad/char\\name", "badcharname"}, + {"colon:name*?", "colonname"}, + {"quotes\"<>|", "quotes"}, + + // mixed + {"User_123-Test!@#", "User_123-Test"}, + + // empty and all-bad + {"", ""}, + {"!@#$%^&*()", ""}, + + // unicode letters and digits + {"ÜserÇ", "ÜserÇ"}, + {"漢字テスト123", "漢字テスト123"}, + } + + for _, tc := range tests { + got := sanitizeProfileName(tc.in) + if got != tc.want { + t.Errorf("sanitizeProfileName(%q) = %q; want %q", tc.in, got, tc.want) + } + } +} diff --git a/client/internal/profilemanager/service.go b/client/internal/profilemanager/service.go new file mode 100644 index 000000000..56198c4cc --- /dev/null +++ b/client/internal/profilemanager/service.go @@ -0,0 +1,359 @@ +package profilemanager + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/util" +) + +var ( + oldDefaultConfigPathDir = "" + oldDefaultConfigPath = "" + + DefaultConfigPathDir = "" + DefaultConfigPath = "" + ActiveProfileStatePath = "" +) + +var ( + ErrorOldDefaultConfigNotFound = errors.New("old default config not found") +) + +func init() { + + DefaultConfigPathDir = "/var/lib/netbird/" + oldDefaultConfigPathDir = "/etc/netbird/" + + switch runtime.GOOS { + case "windows": + oldDefaultConfigPathDir = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird") + DefaultConfigPathDir = oldDefaultConfigPathDir + + case "freebsd": + oldDefaultConfigPathDir = "/var/db/netbird/" + DefaultConfigPathDir = oldDefaultConfigPathDir + } + + oldDefaultConfigPath = filepath.Join(oldDefaultConfigPathDir, "config.json") + DefaultConfigPath = filepath.Join(DefaultConfigPathDir, "default.json") + ActiveProfileStatePath = filepath.Join(DefaultConfigPathDir, "active_profile.json") +} + +type ActiveProfileState struct { + Name string `json:"name"` + Username string `json:"username"` +} + +func (a *ActiveProfileState) FilePath() (string, error) { + if a.Name == "" { + return "", fmt.Errorf("active profile name is empty") + } + + if a.Name == defaultProfileName { + return DefaultConfigPath, nil + } + + configDir, err := getConfigDirForUser(a.Username) + if err != nil { + return "", fmt.Errorf("failed to get config directory for user %s: %w", a.Username, err) + } + + return filepath.Join(configDir, a.Name+".json"), nil +} + +type ServiceManager struct{} + +func (s *ServiceManager) CopyDefaultProfileIfNotExists() (bool, error) { + + if err := os.MkdirAll(DefaultConfigPathDir, 0600); err != nil { + return false, fmt.Errorf("failed to create default config path directory: %w", err) + } + + // check if default profile exists + if _, err := os.Stat(DefaultConfigPath); !os.IsNotExist(err) { + // default profile already exists + log.Debugf("default profile already exists at %s, skipping copy", DefaultConfigPath) + return false, nil + } + + // check old default profile + if _, err := os.Stat(oldDefaultConfigPath); os.IsNotExist(err) { + // old default profile does not exist, nothing to copy + return false, ErrorOldDefaultConfigNotFound + } + + // copy old default profile to new location + if err := copyFile(oldDefaultConfigPath, DefaultConfigPath, 0600); err != nil { + return false, fmt.Errorf("copy default profile from %s to %s: %w", oldDefaultConfigPath, DefaultConfigPath, err) + } + + // set permissions for the new default profile + if err := os.Chmod(DefaultConfigPath, 0600); err != nil { + log.Warnf("failed to set permissions for default profile: %v", err) + } + + if err := s.SetActiveProfileState(&ActiveProfileState{ + Name: "default", + Username: "", + }); err != nil { + log.Errorf("failed to set active profile state: %v", err) + return false, fmt.Errorf("failed to set active profile state: %w", err) + } + + return true, nil +} + +// copyFile copies the contents of src to dst and sets dst's file mode to perm. +func copyFile(src, dst string, perm os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return fmt.Errorf("open source file %s: %w", src, err) + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) + if err != nil { + return fmt.Errorf("open target file %s: %w", dst, err) + } + defer func() { + if cerr := out.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + if _, err := io.Copy(out, in); err != nil { + return fmt.Errorf("copy data to %s: %w", dst, err) + } + + return nil +} + +func (s *ServiceManager) CreateDefaultProfile() error { + _, err := UpdateOrCreateConfig(ConfigInput{ + ConfigPath: DefaultConfigPath, + }) + + if err != nil { + return fmt.Errorf("failed to create default profile: %w", err) + } + + log.Infof("default profile created at %s", DefaultConfigPath) + return nil +} + +func (s *ServiceManager) GetActiveProfileState() (*ActiveProfileState, error) { + if err := s.setDefaultActiveState(); err != nil { + return nil, fmt.Errorf("failed to set default active profile state: %w", err) + } + var activeProfile ActiveProfileState + if _, err := util.ReadJson(ActiveProfileStatePath, &activeProfile); err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := s.SetActiveProfileStateToDefault(); err != nil { + return nil, fmt.Errorf("failed to set active profile to default: %w", err) + } + return &ActiveProfileState{ + Name: "default", + Username: "", + }, nil + } else { + return nil, fmt.Errorf("failed to read active profile state: %w", err) + } + } + + if activeProfile.Name == "" { + if err := s.SetActiveProfileStateToDefault(); err != nil { + return nil, fmt.Errorf("failed to set active profile to default: %w", err) + } + return &ActiveProfileState{ + Name: "default", + Username: "", + }, nil + } + + return &activeProfile, nil + +} + +func (s *ServiceManager) setDefaultActiveState() error { + _, err := os.Stat(ActiveProfileStatePath) + if err != nil { + if os.IsNotExist(err) { + if err := s.SetActiveProfileStateToDefault(); err != nil { + return fmt.Errorf("failed to set active profile to default: %w", err) + } + } else { + return fmt.Errorf("failed to stat active profile state path %s: %w", ActiveProfileStatePath, err) + } + } + + return nil +} + +func (s *ServiceManager) SetActiveProfileState(a *ActiveProfileState) error { + if a == nil || a.Name == "" { + return errors.New("invalid active profile state") + } + + if a.Name != defaultProfileName && a.Username == "" { + return fmt.Errorf("username must be set for non-default profiles, got: %s", a.Name) + } + + if err := util.WriteJsonWithRestrictedPermission(context.Background(), ActiveProfileStatePath, a); err != nil { + return fmt.Errorf("failed to write active profile state: %w", err) + } + + log.Infof("active profile set to %s for %s", a.Name, a.Username) + return nil +} + +func (s *ServiceManager) SetActiveProfileStateToDefault() error { + return s.SetActiveProfileState(&ActiveProfileState{ + Name: "default", + Username: "", + }) +} + +func (s *ServiceManager) DefaultProfilePath() string { + return DefaultConfigPath +} + +func (s *ServiceManager) AddProfile(profileName, username string) error { + configDir, err := getConfigDirForUser(username) + if err != nil { + return fmt.Errorf("failed to get config directory: %w", err) + } + + profileName = sanitizeProfileName(profileName) + + if profileName == defaultProfileName { + return fmt.Errorf("cannot create profile with reserved name: %s", defaultProfileName) + } + + profPath := filepath.Join(configDir, profileName+".json") + if fileExists(profPath) { + return ErrProfileAlreadyExists + } + + cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath}) + if err != nil { + return fmt.Errorf("failed to create new config: %w", err) + } + + err = util.WriteJson(context.Background(), profPath, cfg) + if err != nil { + return fmt.Errorf("failed to write profile config: %w", err) + } + + return nil +} + +func (s *ServiceManager) RemoveProfile(profileName, username string) error { + configDir, err := getConfigDirForUser(username) + if err != nil { + return fmt.Errorf("failed to get config directory: %w", err) + } + + profileName = sanitizeProfileName(profileName) + + if profileName == defaultProfileName { + return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName) + } + profPath := filepath.Join(configDir, profileName+".json") + if !fileExists(profPath) { + return ErrProfileNotFound + } + + activeProf, err := s.GetActiveProfileState() + if err != nil && !errors.Is(err, ErrNoActiveProfile) { + return fmt.Errorf("failed to get active profile: %w", err) + } + + if activeProf != nil && activeProf.Name == profileName { + return fmt.Errorf("cannot remove active profile: %s", profileName) + } + + err = util.RemoveJson(profPath) + if err != nil { + return fmt.Errorf("failed to remove profile config: %w", err) + } + return nil +} + +func (s *ServiceManager) ListProfiles(username string) ([]Profile, error) { + configDir, err := getConfigDirForUser(username) + if err != nil { + return nil, fmt.Errorf("failed to get config directory: %w", err) + } + + files, err := util.ListFiles(configDir, "*.json") + if err != nil { + return nil, fmt.Errorf("failed to list profile files: %w", err) + } + + var filtered []string + for _, file := range files { + if strings.HasSuffix(file, "state.json") { + continue // skip state files + } + filtered = append(filtered, file) + } + sort.Strings(filtered) + + var activeProfName string + activeProf, err := s.GetActiveProfileState() + if err == nil { + activeProfName = activeProf.Name + } + + var profiles []Profile + // add default profile always + profiles = append(profiles, Profile{Name: defaultProfileName, IsActive: activeProfName == "" || activeProfName == defaultProfileName}) + for _, file := range filtered { + profileName := strings.TrimSuffix(filepath.Base(file), ".json") + var isActive bool + if activeProfName != "" && activeProfName == profileName { + isActive = true + } + profiles = append(profiles, Profile{Name: profileName, IsActive: isActive}) + } + + return profiles, nil +} + +// GetStatePath returns the path to the state file based on the operating system +// It returns an empty string if the path cannot be determined. +func (s *ServiceManager) GetStatePath() string { + if path := os.Getenv("NB_DNS_STATE_FILE"); path != "" { + return path + } + + defaultStatePath := filepath.Join(DefaultConfigPathDir, "state.json") + + activeProf, err := s.GetActiveProfileState() + if err != nil { + log.Warnf("failed to get active profile state: %v", err) + return defaultStatePath + } + + if activeProf.Name == defaultProfileName { + return defaultStatePath + } + + configDir, err := getConfigDirForUser(activeProf.Username) + if err != nil { + log.Warnf("failed to get config directory for user %s: %v", activeProf.Username, err) + return defaultStatePath + } + + return filepath.Join(configDir, activeProf.Name+".state.json") +} diff --git a/client/internal/profilemanager/state.go b/client/internal/profilemanager/state.go new file mode 100644 index 000000000..f84cb1032 --- /dev/null +++ b/client/internal/profilemanager/state.go @@ -0,0 +1,57 @@ +package profilemanager + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "github.com/netbirdio/netbird/util" +) + +type ProfileState struct { + Email string `json:"email"` +} + +func (pm *ProfileManager) GetProfileState(profileName string) (*ProfileState, error) { + configDir, err := getConfigDir() + if err != nil { + return nil, fmt.Errorf("get config directory: %w", err) + } + + stateFile := filepath.Join(configDir, profileName+".state.json") + if !fileExists(stateFile) { + return nil, errors.New("profile state file does not exist") + } + + var state ProfileState + _, err = util.ReadJson(stateFile, &state) + if err != nil { + return nil, fmt.Errorf("read profile state: %w", err) + } + + return &state, nil +} + +func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error { + configDir, err := getConfigDir() + if err != nil { + return fmt.Errorf("get config directory: %w", err) + } + + activeProf, err := pm.GetActiveProfile() + if err != nil { + if errors.Is(err, ErrNoActiveProfile) { + return fmt.Errorf("no active profile set: %w", err) + } + return fmt.Errorf("get active profile: %w", err) + } + + stateFile := filepath.Join(configDir, activeProf.Name+".state.json") + err = util.WriteJsonWithRestrictedPermission(context.Background(), stateFile, state) + if err != nil { + return fmt.Errorf("write profile state: %w", err) + } + + return nil +} diff --git a/client/internal/statemanager/path.go b/client/internal/statemanager/path.go deleted file mode 100644 index d232e5f0c..000000000 --- a/client/internal/statemanager/path.go +++ /dev/null @@ -1,16 +0,0 @@ -package statemanager - -import ( - "github.com/netbirdio/netbird/client/configs" - "os" - "path/filepath" -) - -// GetDefaultStatePath returns the path to the state file based on the operating system -// It returns an empty string if the path cannot be determined. -func GetDefaultStatePath() string { - if path := os.Getenv("NB_DNS_STATE_FILE"); path != "" { - return path - } - return filepath.Join(configs.StateDir, "state.json") -} diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go index 622f8e840..fe0f6034e 100644 --- a/client/ios/NetBirdSDK/client.go +++ b/client/ios/NetBirdSDK/client.go @@ -17,6 +17,7 @@ import ( "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/management/domain" @@ -92,7 +93,7 @@ func NewClient(cfgFile, stateFile, deviceName string, osVersion string, osName s func (c *Client) Run(fd int32, interfaceName string) error { log.Infof("Starting NetBird client") log.Debugf("Tunnel uses interface: %s", interfaceName) - cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{ + cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{ ConfigPath: c.cfgFile, StateFilePath: c.stateFile, }) @@ -203,7 +204,7 @@ func (c *Client) IsLoginRequired() bool { defer c.ctxCancelLock.Unlock() ctx, c.ctxCancel = context.WithCancel(ctxWithValues) - cfg, _ := internal.UpdateOrCreateConfig(internal.ConfigInput{ + cfg, _ := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{ ConfigPath: c.cfgFile, }) @@ -223,7 +224,7 @@ func (c *Client) LoginForMobile() string { defer c.ctxCancelLock.Unlock() ctx, c.ctxCancel = context.WithCancel(ctxWithValues) - cfg, _ := internal.UpdateOrCreateConfig(internal.ConfigInput{ + cfg, _ := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{ ConfigPath: c.cfgFile, }) diff --git a/client/ios/NetBirdSDK/login.go b/client/ios/NetBirdSDK/login.go index 986874758..570c44f80 100644 --- a/client/ios/NetBirdSDK/login.go +++ b/client/ios/NetBirdSDK/login.go @@ -12,6 +12,7 @@ import ( "github.com/netbirdio/netbird/client/cmd" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" ) @@ -36,17 +37,17 @@ type URLOpener interface { // Auth can register or login new client type Auth struct { ctx context.Context - config *internal.Config + config *profilemanager.Config cfgPath string } // NewAuth instantiate Auth struct and validate the management URL func NewAuth(cfgPath string, mgmURL string) (*Auth, error) { - inputCfg := internal.ConfigInput{ + inputCfg := profilemanager.ConfigInput{ ManagementURL: mgmURL, } - cfg, err := internal.CreateInMemoryConfig(inputCfg) + cfg, err := profilemanager.CreateInMemoryConfig(inputCfg) if err != nil { return nil, err } @@ -59,7 +60,7 @@ func NewAuth(cfgPath string, mgmURL string) (*Auth, error) { } // NewAuthWithConfig instantiate Auth based on existing config -func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth { +func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config) *Auth { return &Auth{ ctx: ctx, config: config, @@ -94,7 +95,7 @@ func (a *Auth) SaveConfigIfSSOSupported() (bool, error) { return false, fmt.Errorf("backoff cycle failed: %v", err) } - err = internal.WriteOutConfig(a.cfgPath, a.config) + err = profilemanager.WriteOutConfig(a.cfgPath, a.config) return true, err } @@ -115,7 +116,7 @@ func (a *Auth) LoginWithSetupKeyAndSaveConfig(setupKey string, deviceName string return fmt.Errorf("backoff cycle failed: %v", err) } - return internal.WriteOutConfig(a.cfgPath, a.config) + return profilemanager.WriteOutConfig(a.cfgPath, a.config) } func (a *Auth) Login() error { diff --git a/client/ios/NetBirdSDK/preferences.go b/client/ios/NetBirdSDK/preferences.go index 5a0abd9a7..5e7050465 100644 --- a/client/ios/NetBirdSDK/preferences.go +++ b/client/ios/NetBirdSDK/preferences.go @@ -1,17 +1,17 @@ package NetBirdSDK import ( - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) // Preferences export a subset of the internal config for gomobile type Preferences struct { - configInput internal.ConfigInput + configInput profilemanager.ConfigInput } // NewPreferences create new Preferences instance func NewPreferences(configPath string, stateFilePath string) *Preferences { - ci := internal.ConfigInput{ + ci := profilemanager.ConfigInput{ ConfigPath: configPath, StateFilePath: stateFilePath, } @@ -24,7 +24,7 @@ func (p *Preferences) GetManagementURL() (string, error) { return p.configInput.ManagementURL, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return "", err } @@ -42,7 +42,7 @@ func (p *Preferences) GetAdminURL() (string, error) { return p.configInput.AdminURL, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return "", err } @@ -60,7 +60,7 @@ func (p *Preferences) GetPreSharedKey() (string, error) { return *p.configInput.PreSharedKey, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return "", err } @@ -83,7 +83,7 @@ func (p *Preferences) GetRosenpassEnabled() (bool, error) { return *p.configInput.RosenpassEnabled, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -101,7 +101,7 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) { return *p.configInput.RosenpassPermissive, nil } - cfg, err := internal.ReadConfig(p.configInput.ConfigPath) + cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath) if err != nil { return false, err } @@ -110,6 +110,6 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) { // Commit write out the changes into config file func (p *Preferences) Commit() error { - _, err := internal.UpdateOrCreateConfig(p.configInput) + _, err := profilemanager.UpdateOrCreateConfig(p.configInput) return err } diff --git a/client/ios/NetBirdSDK/preferences_test.go b/client/ios/NetBirdSDK/preferences_test.go index 7e5325a00..780443a7b 100644 --- a/client/ios/NetBirdSDK/preferences_test.go +++ b/client/ios/NetBirdSDK/preferences_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" ) func TestPreferences_DefaultValues(t *testing.T) { @@ -16,7 +16,7 @@ func TestPreferences_DefaultValues(t *testing.T) { t.Fatalf("failed to read default value: %s", err) } - if defaultVar != internal.DefaultAdminURL { + if defaultVar != profilemanager.DefaultAdminURL { t.Errorf("invalid default admin url: %s", defaultVar) } @@ -25,7 +25,7 @@ func TestPreferences_DefaultValues(t *testing.T) { t.Fatalf("failed to read default management URL: %s", err) } - if defaultVar != internal.DefaultManagementURL { + if defaultVar != profilemanager.DefaultManagementURL { t.Errorf("invalid default management url: %s", defaultVar) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 26e58d183..f405ffd65 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -273,9 +273,11 @@ type LoginRequest struct { // cleanDNSLabels clean map list of DNS labels. // This is needed because the generated code // omits initialized empty slices due to omitempty tags - CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` - LazyConnectionEnabled *bool `protobuf:"varint,28,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"` - BlockInbound *bool `protobuf:"varint,29,opt,name=block_inbound,json=blockInbound,proto3,oneof" json:"block_inbound,omitempty"` + CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` + LazyConnectionEnabled *bool `protobuf:"varint,28,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"` + BlockInbound *bool `protobuf:"varint,29,opt,name=block_inbound,json=blockInbound,proto3,oneof" json:"block_inbound,omitempty"` + ProfileName *string `protobuf:"bytes,30,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` + Username *string `protobuf:"bytes,31,opt,name=username,proto3,oneof" json:"username,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -514,6 +516,20 @@ func (x *LoginRequest) GetBlockInbound() bool { return false } +func (x *LoginRequest) GetProfileName() string { + if x != nil && x.ProfileName != nil { + return *x.ProfileName + } + return "" +} + +func (x *LoginRequest) GetUsername() string { + if x != nil && x.Username != nil { + return *x.Username + } + return "" +} + type LoginResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NeedsSSOLogin bool `protobuf:"varint,1,opt,name=needsSSOLogin,proto3" json:"needsSSOLogin,omitempty"` @@ -636,6 +652,7 @@ func (x *WaitSSOLoginRequest) GetHostname() string { type WaitSSOLoginResponse struct { state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -670,8 +687,17 @@ func (*WaitSSOLoginResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{4} } +func (x *WaitSSOLoginResponse) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + type UpRequest struct { state protoimpl.MessageState `protogen:"open.v1"` + ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` + Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -706,6 +732,20 @@ func (*UpRequest) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{5} } +func (x *UpRequest) GetProfileName() string { + if x != nil && x.ProfileName != nil { + return *x.ProfileName + } + return "" +} + +func (x *UpRequest) GetUsername() string { + if x != nil && x.Username != nil { + return *x.Username + } + return "" +} + type UpResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -930,6 +970,8 @@ func (*DownResponse) Descriptor() ([]byte, []int) { type GetConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` + ProfileName string `protobuf:"bytes,1,opt,name=profileName,proto3" json:"profileName,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -964,6 +1006,20 @@ func (*GetConfigRequest) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{11} } +func (x *GetConfigRequest) GetProfileName() string { + if x != nil { + return x.ProfileName + } + return "" +} + +func (x *GetConfigRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + type GetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // managementUrl settings value. @@ -3503,6 +3559,789 @@ func (x *GetEventsResponse) GetEvents() []*SystemEvent { return nil } +type SwitchProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` + Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SwitchProfileRequest) Reset() { + *x = SwitchProfileRequest{} + mi := &file_daemon_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SwitchProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SwitchProfileRequest) ProtoMessage() {} + +func (x *SwitchProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SwitchProfileRequest.ProtoReflect.Descriptor instead. +func (*SwitchProfileRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{52} +} + +func (x *SwitchProfileRequest) GetProfileName() string { + if x != nil && x.ProfileName != nil { + return *x.ProfileName + } + return "" +} + +func (x *SwitchProfileRequest) GetUsername() string { + if x != nil && x.Username != nil { + return *x.Username + } + return "" +} + +type SwitchProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SwitchProfileResponse) Reset() { + *x = SwitchProfileResponse{} + mi := &file_daemon_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SwitchProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SwitchProfileResponse) ProtoMessage() {} + +func (x *SwitchProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SwitchProfileResponse.ProtoReflect.Descriptor instead. +func (*SwitchProfileResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{53} +} + +type SetConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + ProfileName string `protobuf:"bytes,2,opt,name=profileName,proto3" json:"profileName,omitempty"` + // managementUrl to authenticate. + ManagementUrl string `protobuf:"bytes,3,opt,name=managementUrl,proto3" json:"managementUrl,omitempty"` + // adminUrl to manage keys. + AdminURL string `protobuf:"bytes,4,opt,name=adminURL,proto3" json:"adminURL,omitempty"` + RosenpassEnabled *bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3,oneof" json:"rosenpassEnabled,omitempty"` + InterfaceName *string `protobuf:"bytes,6,opt,name=interfaceName,proto3,oneof" json:"interfaceName,omitempty"` + WireguardPort *int64 `protobuf:"varint,7,opt,name=wireguardPort,proto3,oneof" json:"wireguardPort,omitempty"` + OptionalPreSharedKey *string `protobuf:"bytes,8,opt,name=optionalPreSharedKey,proto3,oneof" json:"optionalPreSharedKey,omitempty"` + DisableAutoConnect *bool `protobuf:"varint,9,opt,name=disableAutoConnect,proto3,oneof" json:"disableAutoConnect,omitempty"` + ServerSSHAllowed *bool `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3,oneof" json:"serverSSHAllowed,omitempty"` + RosenpassPermissive *bool `protobuf:"varint,11,opt,name=rosenpassPermissive,proto3,oneof" json:"rosenpassPermissive,omitempty"` + NetworkMonitor *bool `protobuf:"varint,12,opt,name=networkMonitor,proto3,oneof" json:"networkMonitor,omitempty"` + DisableClientRoutes *bool `protobuf:"varint,13,opt,name=disable_client_routes,json=disableClientRoutes,proto3,oneof" json:"disable_client_routes,omitempty"` + DisableServerRoutes *bool `protobuf:"varint,14,opt,name=disable_server_routes,json=disableServerRoutes,proto3,oneof" json:"disable_server_routes,omitempty"` + DisableDns *bool `protobuf:"varint,15,opt,name=disable_dns,json=disableDns,proto3,oneof" json:"disable_dns,omitempty"` + DisableFirewall *bool `protobuf:"varint,16,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"` + BlockLanAccess *bool `protobuf:"varint,17,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"` + DisableNotifications *bool `protobuf:"varint,18,opt,name=disable_notifications,json=disableNotifications,proto3,oneof" json:"disable_notifications,omitempty"` + LazyConnectionEnabled *bool `protobuf:"varint,19,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"` + BlockInbound *bool `protobuf:"varint,20,opt,name=block_inbound,json=blockInbound,proto3,oneof" json:"block_inbound,omitempty"` + NatExternalIPs []string `protobuf:"bytes,21,rep,name=natExternalIPs,proto3" json:"natExternalIPs,omitempty"` + CleanNATExternalIPs bool `protobuf:"varint,22,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"` + CustomDNSAddress []byte `protobuf:"bytes,23,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"` + ExtraIFaceBlacklist []string `protobuf:"bytes,24,rep,name=extraIFaceBlacklist,proto3" json:"extraIFaceBlacklist,omitempty"` + DnsLabels []string `protobuf:"bytes,25,rep,name=dns_labels,json=dnsLabels,proto3" json:"dns_labels,omitempty"` + // cleanDNSLabels clean map list of DNS labels. + CleanDNSLabels bool `protobuf:"varint,26,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"` + DnsRouteInterval *durationpb.Duration `protobuf:"bytes,27,opt,name=dnsRouteInterval,proto3,oneof" json:"dnsRouteInterval,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetConfigRequest) Reset() { + *x = SetConfigRequest{} + mi := &file_daemon_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetConfigRequest) ProtoMessage() {} + +func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetConfigRequest.ProtoReflect.Descriptor instead. +func (*SetConfigRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{54} +} + +func (x *SetConfigRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SetConfigRequest) GetProfileName() string { + if x != nil { + return x.ProfileName + } + return "" +} + +func (x *SetConfigRequest) GetManagementUrl() string { + if x != nil { + return x.ManagementUrl + } + return "" +} + +func (x *SetConfigRequest) GetAdminURL() string { + if x != nil { + return x.AdminURL + } + return "" +} + +func (x *SetConfigRequest) GetRosenpassEnabled() bool { + if x != nil && x.RosenpassEnabled != nil { + return *x.RosenpassEnabled + } + return false +} + +func (x *SetConfigRequest) GetInterfaceName() string { + if x != nil && x.InterfaceName != nil { + return *x.InterfaceName + } + return "" +} + +func (x *SetConfigRequest) GetWireguardPort() int64 { + if x != nil && x.WireguardPort != nil { + return *x.WireguardPort + } + return 0 +} + +func (x *SetConfigRequest) GetOptionalPreSharedKey() string { + if x != nil && x.OptionalPreSharedKey != nil { + return *x.OptionalPreSharedKey + } + return "" +} + +func (x *SetConfigRequest) GetDisableAutoConnect() bool { + if x != nil && x.DisableAutoConnect != nil { + return *x.DisableAutoConnect + } + return false +} + +func (x *SetConfigRequest) GetServerSSHAllowed() bool { + if x != nil && x.ServerSSHAllowed != nil { + return *x.ServerSSHAllowed + } + return false +} + +func (x *SetConfigRequest) GetRosenpassPermissive() bool { + if x != nil && x.RosenpassPermissive != nil { + return *x.RosenpassPermissive + } + return false +} + +func (x *SetConfigRequest) GetNetworkMonitor() bool { + if x != nil && x.NetworkMonitor != nil { + return *x.NetworkMonitor + } + return false +} + +func (x *SetConfigRequest) GetDisableClientRoutes() bool { + if x != nil && x.DisableClientRoutes != nil { + return *x.DisableClientRoutes + } + return false +} + +func (x *SetConfigRequest) GetDisableServerRoutes() bool { + if x != nil && x.DisableServerRoutes != nil { + return *x.DisableServerRoutes + } + return false +} + +func (x *SetConfigRequest) GetDisableDns() bool { + if x != nil && x.DisableDns != nil { + return *x.DisableDns + } + return false +} + +func (x *SetConfigRequest) GetDisableFirewall() bool { + if x != nil && x.DisableFirewall != nil { + return *x.DisableFirewall + } + return false +} + +func (x *SetConfigRequest) GetBlockLanAccess() bool { + if x != nil && x.BlockLanAccess != nil { + return *x.BlockLanAccess + } + return false +} + +func (x *SetConfigRequest) GetDisableNotifications() bool { + if x != nil && x.DisableNotifications != nil { + return *x.DisableNotifications + } + return false +} + +func (x *SetConfigRequest) GetLazyConnectionEnabled() bool { + if x != nil && x.LazyConnectionEnabled != nil { + return *x.LazyConnectionEnabled + } + return false +} + +func (x *SetConfigRequest) GetBlockInbound() bool { + if x != nil && x.BlockInbound != nil { + return *x.BlockInbound + } + return false +} + +func (x *SetConfigRequest) GetNatExternalIPs() []string { + if x != nil { + return x.NatExternalIPs + } + return nil +} + +func (x *SetConfigRequest) GetCleanNATExternalIPs() bool { + if x != nil { + return x.CleanNATExternalIPs + } + return false +} + +func (x *SetConfigRequest) GetCustomDNSAddress() []byte { + if x != nil { + return x.CustomDNSAddress + } + return nil +} + +func (x *SetConfigRequest) GetExtraIFaceBlacklist() []string { + if x != nil { + return x.ExtraIFaceBlacklist + } + return nil +} + +func (x *SetConfigRequest) GetDnsLabels() []string { + if x != nil { + return x.DnsLabels + } + return nil +} + +func (x *SetConfigRequest) GetCleanDNSLabels() bool { + if x != nil { + return x.CleanDNSLabels + } + return false +} + +func (x *SetConfigRequest) GetDnsRouteInterval() *durationpb.Duration { + if x != nil { + return x.DnsRouteInterval + } + return nil +} + +type SetConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetConfigResponse) Reset() { + *x = SetConfigResponse{} + mi := &file_daemon_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetConfigResponse) ProtoMessage() {} + +func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetConfigResponse.ProtoReflect.Descriptor instead. +func (*SetConfigResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{55} +} + +type AddProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + ProfileName string `protobuf:"bytes,2,opt,name=profileName,proto3" json:"profileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddProfileRequest) Reset() { + *x = AddProfileRequest{} + mi := &file_daemon_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddProfileRequest) ProtoMessage() {} + +func (x *AddProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddProfileRequest.ProtoReflect.Descriptor instead. +func (*AddProfileRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{56} +} + +func (x *AddProfileRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *AddProfileRequest) GetProfileName() string { + if x != nil { + return x.ProfileName + } + return "" +} + +type AddProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddProfileResponse) Reset() { + *x = AddProfileResponse{} + mi := &file_daemon_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddProfileResponse) ProtoMessage() {} + +func (x *AddProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddProfileResponse.ProtoReflect.Descriptor instead. +func (*AddProfileResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{57} +} + +type RemoveProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + ProfileName string `protobuf:"bytes,2,opt,name=profileName,proto3" json:"profileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveProfileRequest) Reset() { + *x = RemoveProfileRequest{} + mi := &file_daemon_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveProfileRequest) ProtoMessage() {} + +func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveProfileRequest.ProtoReflect.Descriptor instead. +func (*RemoveProfileRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{58} +} + +func (x *RemoveProfileRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *RemoveProfileRequest) GetProfileName() string { + if x != nil { + return x.ProfileName + } + return "" +} + +type RemoveProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveProfileResponse) Reset() { + *x = RemoveProfileResponse{} + mi := &file_daemon_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveProfileResponse) ProtoMessage() {} + +func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveProfileResponse.ProtoReflect.Descriptor instead. +func (*RemoveProfileResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{59} +} + +type ListProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProfilesRequest) Reset() { + *x = ListProfilesRequest{} + mi := &file_daemon_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProfilesRequest) ProtoMessage() {} + +func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListProfilesRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{60} +} + +func (x *ListProfilesRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type ListProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profiles []*Profile `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProfilesResponse) Reset() { + *x = ListProfilesResponse{} + mi := &file_daemon_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProfilesResponse) ProtoMessage() {} + +func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListProfilesResponse.ProtoReflect.Descriptor instead. +func (*ListProfilesResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{61} +} + +func (x *ListProfilesResponse) GetProfiles() []*Profile { + if x != nil { + return x.Profiles + } + return nil +} + +type Profile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + IsActive bool `protobuf:"varint,2,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Profile) Reset() { + *x = Profile{} + mi := &file_daemon_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Profile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Profile) ProtoMessage() {} + +func (x *Profile) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Profile.ProtoReflect.Descriptor instead. +func (*Profile) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{62} +} + +func (x *Profile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Profile) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +type GetActiveProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetActiveProfileRequest) Reset() { + *x = GetActiveProfileRequest{} + mi := &file_daemon_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetActiveProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetActiveProfileRequest) ProtoMessage() {} + +func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetActiveProfileRequest.ProtoReflect.Descriptor instead. +func (*GetActiveProfileRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{63} +} + +type GetActiveProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProfileName string `protobuf:"bytes,1,opt,name=profileName,proto3" json:"profileName,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetActiveProfileResponse) Reset() { + *x = GetActiveProfileResponse{} + mi := &file_daemon_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetActiveProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetActiveProfileResponse) ProtoMessage() {} + +func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetActiveProfileResponse.ProtoReflect.Descriptor instead. +func (*GetActiveProfileResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{64} +} + +func (x *GetActiveProfileResponse) GetProfileName() string { + if x != nil { + return x.ProfileName + } + return "" +} + +func (x *GetActiveProfileResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + type PortInfo_Range struct { state protoimpl.MessageState `protogen:"open.v1"` Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` @@ -3513,7 +4352,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3525,7 +4364,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3560,7 +4399,7 @@ var File_daemon_proto protoreflect.FileDescriptor const file_daemon_proto_rawDesc = "" + "\n" + "\fdaemon.proto\x12\x06daemon\x1a google/protobuf/descriptor.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\x0e\n" + - "\fEmptyRequest\"\xbf\r\n" + + "\fEmptyRequest\"\xa4\x0e\n" + "\fLoginRequest\x12\x1a\n" + "\bsetupKey\x18\x01 \x01(\tR\bsetupKey\x12&\n" + "\fpreSharedKey\x18\x02 \x01(\tB\x02\x18\x01R\fpreSharedKey\x12$\n" + @@ -3594,7 +4433,9 @@ const file_daemon_proto_rawDesc = "" + "dns_labels\x18\x1a \x03(\tR\tdnsLabels\x12&\n" + "\x0ecleanDNSLabels\x18\x1b \x01(\bR\x0ecleanDNSLabels\x129\n" + "\x15lazyConnectionEnabled\x18\x1c \x01(\bH\x0fR\x15lazyConnectionEnabled\x88\x01\x01\x12(\n" + - "\rblock_inbound\x18\x1d \x01(\bH\x10R\fblockInbound\x88\x01\x01B\x13\n" + + "\rblock_inbound\x18\x1d \x01(\bH\x10R\fblockInbound\x88\x01\x01\x12%\n" + + "\vprofileName\x18\x1e \x01(\tH\x11R\vprofileName\x88\x01\x01\x12\x1f\n" + + "\busername\x18\x1f \x01(\tH\x12R\busername\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -3611,7 +4452,9 @@ const file_daemon_proto_rawDesc = "" + "\x11_block_lan_accessB\x18\n" + "\x16_disable_notificationsB\x18\n" + "\x16_lazyConnectionEnabledB\x10\n" + - "\x0e_block_inbound\"\xb5\x01\n" + + "\x0e_block_inboundB\x0e\n" + + "\f_profileNameB\v\n" + + "\t_username\"\xb5\x01\n" + "\rLoginResponse\x12$\n" + "\rneedsSSOLogin\x18\x01 \x01(\bR\rneedsSSOLogin\x12\x1a\n" + "\buserCode\x18\x02 \x01(\tR\buserCode\x12(\n" + @@ -3619,9 +4462,14 @@ const file_daemon_proto_rawDesc = "" + "\x17verificationURIComplete\x18\x04 \x01(\tR\x17verificationURIComplete\"M\n" + "\x13WaitSSOLoginRequest\x12\x1a\n" + "\buserCode\x18\x01 \x01(\tR\buserCode\x12\x1a\n" + - "\bhostname\x18\x02 \x01(\tR\bhostname\"\x16\n" + - "\x14WaitSSOLoginResponse\"\v\n" + - "\tUpRequest\"\f\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\",\n" + + "\x14WaitSSOLoginResponse\x12\x14\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"p\n" + + "\tUpRequest\x12%\n" + + "\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" + + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + + "\f_profileNameB\v\n" + + "\t_username\"\f\n" + "\n" + "UpResponse\"g\n" + "\rStatusRequest\x12,\n" + @@ -3634,8 +4482,10 @@ const file_daemon_proto_rawDesc = "" + "fullStatus\x12$\n" + "\rdaemonVersion\x18\x03 \x01(\tR\rdaemonVersion\"\r\n" + "\vDownRequest\"\x0e\n" + - "\fDownResponse\"\x12\n" + - "\x10GetConfigRequest\"\xa3\x06\n" + + "\fDownResponse\"P\n" + + "\x10GetConfigRequest\x12 \n" + + "\vprofileName\x18\x01 \x01(\tR\vprofileName\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\"\xa3\x06\n" + "\x11GetConfigResponse\x12$\n" + "\rmanagementUrl\x18\x01 \x01(\tR\rmanagementUrl\x12\x1e\n" + "\n" + @@ -3853,7 +4703,82 @@ const file_daemon_proto_rawDesc = "" + "\x06SYSTEM\x10\x04\"\x12\n" + "\x10GetEventsRequest\"@\n" + "\x11GetEventsResponse\x12+\n" + - "\x06events\x18\x01 \x03(\v2\x13.daemon.SystemEventR\x06events*b\n" + + "\x06events\x18\x01 \x03(\v2\x13.daemon.SystemEventR\x06events\"{\n" + + "\x14SwitchProfileRequest\x12%\n" + + "\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" + + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + + "\f_profileNameB\v\n" + + "\t_username\"\x17\n" + + "\x15SwitchProfileResponse\"\xef\f\n" + + "\x10SetConfigRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + + "\vprofileName\x18\x02 \x01(\tR\vprofileName\x12$\n" + + "\rmanagementUrl\x18\x03 \x01(\tR\rmanagementUrl\x12\x1a\n" + + "\badminURL\x18\x04 \x01(\tR\badminURL\x12/\n" + + "\x10rosenpassEnabled\x18\x05 \x01(\bH\x00R\x10rosenpassEnabled\x88\x01\x01\x12)\n" + + "\rinterfaceName\x18\x06 \x01(\tH\x01R\rinterfaceName\x88\x01\x01\x12)\n" + + "\rwireguardPort\x18\a \x01(\x03H\x02R\rwireguardPort\x88\x01\x01\x127\n" + + "\x14optionalPreSharedKey\x18\b \x01(\tH\x03R\x14optionalPreSharedKey\x88\x01\x01\x123\n" + + "\x12disableAutoConnect\x18\t \x01(\bH\x04R\x12disableAutoConnect\x88\x01\x01\x12/\n" + + "\x10serverSSHAllowed\x18\n" + + " \x01(\bH\x05R\x10serverSSHAllowed\x88\x01\x01\x125\n" + + "\x13rosenpassPermissive\x18\v \x01(\bH\x06R\x13rosenpassPermissive\x88\x01\x01\x12+\n" + + "\x0enetworkMonitor\x18\f \x01(\bH\aR\x0enetworkMonitor\x88\x01\x01\x127\n" + + "\x15disable_client_routes\x18\r \x01(\bH\bR\x13disableClientRoutes\x88\x01\x01\x127\n" + + "\x15disable_server_routes\x18\x0e \x01(\bH\tR\x13disableServerRoutes\x88\x01\x01\x12$\n" + + "\vdisable_dns\x18\x0f \x01(\bH\n" + + "R\n" + + "disableDns\x88\x01\x01\x12.\n" + + "\x10disable_firewall\x18\x10 \x01(\bH\vR\x0fdisableFirewall\x88\x01\x01\x12-\n" + + "\x10block_lan_access\x18\x11 \x01(\bH\fR\x0eblockLanAccess\x88\x01\x01\x128\n" + + "\x15disable_notifications\x18\x12 \x01(\bH\rR\x14disableNotifications\x88\x01\x01\x129\n" + + "\x15lazyConnectionEnabled\x18\x13 \x01(\bH\x0eR\x15lazyConnectionEnabled\x88\x01\x01\x12(\n" + + "\rblock_inbound\x18\x14 \x01(\bH\x0fR\fblockInbound\x88\x01\x01\x12&\n" + + "\x0enatExternalIPs\x18\x15 \x03(\tR\x0enatExternalIPs\x120\n" + + "\x13cleanNATExternalIPs\x18\x16 \x01(\bR\x13cleanNATExternalIPs\x12*\n" + + "\x10customDNSAddress\x18\x17 \x01(\fR\x10customDNSAddress\x120\n" + + "\x13extraIFaceBlacklist\x18\x18 \x03(\tR\x13extraIFaceBlacklist\x12\x1d\n" + + "\n" + + "dns_labels\x18\x19 \x03(\tR\tdnsLabels\x12&\n" + + "\x0ecleanDNSLabels\x18\x1a \x01(\bR\x0ecleanDNSLabels\x12J\n" + + "\x10dnsRouteInterval\x18\x1b \x01(\v2\x19.google.protobuf.DurationH\x10R\x10dnsRouteInterval\x88\x01\x01B\x13\n" + + "\x11_rosenpassEnabledB\x10\n" + + "\x0e_interfaceNameB\x10\n" + + "\x0e_wireguardPortB\x17\n" + + "\x15_optionalPreSharedKeyB\x15\n" + + "\x13_disableAutoConnectB\x13\n" + + "\x11_serverSSHAllowedB\x16\n" + + "\x14_rosenpassPermissiveB\x11\n" + + "\x0f_networkMonitorB\x18\n" + + "\x16_disable_client_routesB\x18\n" + + "\x16_disable_server_routesB\x0e\n" + + "\f_disable_dnsB\x13\n" + + "\x11_disable_firewallB\x13\n" + + "\x11_block_lan_accessB\x18\n" + + "\x16_disable_notificationsB\x18\n" + + "\x16_lazyConnectionEnabledB\x10\n" + + "\x0e_block_inboundB\x13\n" + + "\x11_dnsRouteInterval\"\x13\n" + + "\x11SetConfigResponse\"Q\n" + + "\x11AddProfileRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + + "\vprofileName\x18\x02 \x01(\tR\vprofileName\"\x14\n" + + "\x12AddProfileResponse\"T\n" + + "\x14RemoveProfileRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + + "\vprofileName\x18\x02 \x01(\tR\vprofileName\"\x17\n" + + "\x15RemoveProfileResponse\"1\n" + + "\x13ListProfilesRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\"C\n" + + "\x14ListProfilesResponse\x12+\n" + + "\bprofiles\x18\x01 \x03(\v2\x0f.daemon.ProfileR\bprofiles\":\n" + + "\aProfile\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" + + "\tis_active\x18\x02 \x01(\bR\bisActive\"\x19\n" + + "\x17GetActiveProfileRequest\"X\n" + + "\x18GetActiveProfileResponse\x12 \n" + + "\vprofileName\x18\x01 \x01(\tR\vprofileName\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername*b\n" + "\bLogLevel\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05PANIC\x10\x01\x12\t\n" + @@ -3862,7 +4787,7 @@ const file_daemon_proto_rawDesc = "" + "\x04WARN\x10\x04\x12\b\n" + "\x04INFO\x10\x05\x12\t\n" + "\x05DEBUG\x10\x06\x12\t\n" + - "\x05TRACE\x10\a2\xb3\v\n" + + "\x05TRACE\x10\a2\x84\x0f\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -3885,7 +4810,14 @@ const file_daemon_proto_rawDesc = "" + "\x18SetNetworkMapPersistence\x12'.daemon.SetNetworkMapPersistenceRequest\x1a(.daemon.SetNetworkMapPersistenceResponse\"\x00\x12H\n" + "\vTracePacket\x12\x1a.daemon.TracePacketRequest\x1a\x1b.daemon.TracePacketResponse\"\x00\x12D\n" + "\x0fSubscribeEvents\x12\x18.daemon.SubscribeRequest\x1a\x13.daemon.SystemEvent\"\x000\x01\x12B\n" + - "\tGetEvents\x12\x18.daemon.GetEventsRequest\x1a\x19.daemon.GetEventsResponse\"\x00B\bZ\x06/protob\x06proto3" + "\tGetEvents\x12\x18.daemon.GetEventsRequest\x1a\x19.daemon.GetEventsResponse\"\x00\x12N\n" + + "\rSwitchProfile\x12\x1c.daemon.SwitchProfileRequest\x1a\x1d.daemon.SwitchProfileResponse\"\x00\x12B\n" + + "\tSetConfig\x12\x18.daemon.SetConfigRequest\x1a\x19.daemon.SetConfigResponse\"\x00\x12E\n" + + "\n" + + "AddProfile\x12\x19.daemon.AddProfileRequest\x1a\x1a.daemon.AddProfileResponse\"\x00\x12N\n" + + "\rRemoveProfile\x12\x1c.daemon.RemoveProfileRequest\x1a\x1d.daemon.RemoveProfileResponse\"\x00\x12K\n" + + "\fListProfiles\x12\x1b.daemon.ListProfilesRequest\x1a\x1c.daemon.ListProfilesResponse\"\x00\x12W\n" + + "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -3900,7 +4832,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 55) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 68) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity @@ -3957,18 +4889,31 @@ var file_daemon_proto_goTypes = []any{ (*SystemEvent)(nil), // 52: daemon.SystemEvent (*GetEventsRequest)(nil), // 53: daemon.GetEventsRequest (*GetEventsResponse)(nil), // 54: daemon.GetEventsResponse - nil, // 55: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 56: daemon.PortInfo.Range - nil, // 57: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 58: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 59: google.protobuf.Timestamp + (*SwitchProfileRequest)(nil), // 55: daemon.SwitchProfileRequest + (*SwitchProfileResponse)(nil), // 56: daemon.SwitchProfileResponse + (*SetConfigRequest)(nil), // 57: daemon.SetConfigRequest + (*SetConfigResponse)(nil), // 58: daemon.SetConfigResponse + (*AddProfileRequest)(nil), // 59: daemon.AddProfileRequest + (*AddProfileResponse)(nil), // 60: daemon.AddProfileResponse + (*RemoveProfileRequest)(nil), // 61: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 62: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 63: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 64: daemon.ListProfilesResponse + (*Profile)(nil), // 65: daemon.Profile + (*GetActiveProfileRequest)(nil), // 66: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 67: daemon.GetActiveProfileResponse + nil, // 68: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 69: daemon.PortInfo.Range + nil, // 70: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 71: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 72: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 58, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 71, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 22, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 59, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 59, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 58, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 72, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 72, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 71, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 19, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 18, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState 17, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState @@ -3977,8 +4922,8 @@ var file_daemon_proto_depIdxs = []int32{ 21, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState 52, // 11: daemon.FullStatus.events:type_name -> daemon.SystemEvent 28, // 12: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 55, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 56, // 14: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 68, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 69, // 14: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 29, // 15: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 29, // 16: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 30, // 17: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule @@ -3989,55 +4934,69 @@ var file_daemon_proto_depIdxs = []int32{ 49, // 22: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 1, // 23: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 2, // 24: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 59, // 25: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 57, // 26: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 72, // 25: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 70, // 26: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 52, // 27: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 27, // 28: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 4, // 29: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 6, // 30: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 8, // 31: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 10, // 32: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 12, // 33: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 14, // 34: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 23, // 35: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 25, // 36: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 25, // 37: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 3, // 38: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest - 32, // 39: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 34, // 40: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 36, // 41: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 39, // 42: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 41, // 43: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 43, // 44: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 45, // 45: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest - 48, // 46: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 51, // 47: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest - 53, // 48: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest - 5, // 49: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 7, // 50: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 9, // 51: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 11, // 52: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 13, // 53: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 15, // 54: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 24, // 55: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 26, // 56: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 26, // 57: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 31, // 58: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 33, // 59: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 35, // 60: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 37, // 61: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 40, // 62: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 42, // 63: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 44, // 64: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 46, // 65: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse - 50, // 66: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 52, // 67: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 54, // 68: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 49, // [49:69] is the sub-list for method output_type - 29, // [29:49] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 71, // 28: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 65, // 29: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 27, // 30: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 4, // 31: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 6, // 32: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 8, // 33: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 10, // 34: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 12, // 35: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 14, // 36: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 23, // 37: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 25, // 38: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 25, // 39: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 3, // 40: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest + 32, // 41: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 34, // 42: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 36, // 43: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 39, // 44: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 41, // 45: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 43, // 46: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 45, // 47: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest + 48, // 48: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 51, // 49: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest + 53, // 50: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest + 55, // 51: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest + 57, // 52: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest + 59, // 53: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest + 61, // 54: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 63, // 55: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 66, // 56: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 5, // 57: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 7, // 58: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 9, // 59: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 11, // 60: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 13, // 61: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 15, // 62: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 24, // 63: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 26, // 64: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 26, // 65: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 31, // 66: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 33, // 67: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 35, // 68: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 37, // 69: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 40, // 70: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 42, // 71: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 44, // 72: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 46, // 73: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 50, // 74: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 52, // 75: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 54, // 76: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 56, // 77: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 58, // 78: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 60, // 79: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 62, // 80: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 64, // 81: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 67, // 82: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 57, // [57:83] is the sub-list for method output_type + 31, // [31:57] is the sub-list for method input_type + 31, // [31:31] is the sub-list for extension type_name + 31, // [31:31] is the sub-list for extension extendee + 0, // [0:31] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -4046,19 +5005,22 @@ func file_daemon_proto_init() { return } file_daemon_proto_msgTypes[1].OneofWrappers = []any{} + file_daemon_proto_msgTypes[5].OneofWrappers = []any{} file_daemon_proto_msgTypes[26].OneofWrappers = []any{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } file_daemon_proto_msgTypes[45].OneofWrappers = []any{} file_daemon_proto_msgTypes[46].OneofWrappers = []any{} + file_daemon_proto_msgTypes[52].OneofWrappers = []any{} + file_daemon_proto_msgTypes[54].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 3, - NumMessages: 55, + NumMessages: 68, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 462555c82..c25503df9 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -67,6 +67,18 @@ service DaemonService { rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {} rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {} + + rpc SwitchProfile(SwitchProfileRequest) returns (SwitchProfileResponse) {} + + rpc SetConfig(SetConfigRequest) returns (SetConfigResponse) {} + + rpc AddProfile(AddProfileRequest) returns (AddProfileResponse) {} + + rpc RemoveProfile(RemoveProfileRequest) returns (RemoveProfileResponse) {} + + rpc ListProfiles(ListProfilesRequest) returns (ListProfilesResponse) {} + + rpc GetActiveProfile(GetActiveProfileRequest) returns (GetActiveProfileResponse) {} } @@ -136,6 +148,9 @@ message LoginRequest { optional bool lazyConnectionEnabled = 28; optional bool block_inbound = 29; + + optional string profileName = 30; + optional string username = 31; } message LoginResponse { @@ -150,9 +165,14 @@ message WaitSSOLoginRequest { string hostname = 2; } -message WaitSSOLoginResponse {} +message WaitSSOLoginResponse { + string email = 1; +} -message UpRequest {} +message UpRequest { + optional string profileName = 1; + optional string username = 2; +} message UpResponse {} @@ -173,7 +193,10 @@ message DownRequest {} message DownResponse {} -message GetConfigRequest {} +message GetConfigRequest { + string profileName = 1; + string username = 2; +} message GetConfigResponse { // managementUrl settings value. @@ -497,3 +520,98 @@ message GetEventsRequest {} message GetEventsResponse { repeated SystemEvent events = 1; } + +message SwitchProfileRequest { + optional string profileName = 1; + optional string username = 2; +} + +message SwitchProfileResponse {} + +message SetConfigRequest { + string username = 1; + string profileName = 2; + // managementUrl to authenticate. + string managementUrl = 3; + + // adminUrl to manage keys. + string adminURL = 4; + + optional bool rosenpassEnabled = 5; + + optional string interfaceName = 6; + + optional int64 wireguardPort = 7; + + optional string optionalPreSharedKey = 8; + + optional bool disableAutoConnect = 9; + + optional bool serverSSHAllowed = 10; + + optional bool rosenpassPermissive = 11; + + optional bool networkMonitor = 12; + + optional bool disable_client_routes = 13; + optional bool disable_server_routes = 14; + optional bool disable_dns = 15; + optional bool disable_firewall = 16; + optional bool block_lan_access = 17; + + optional bool disable_notifications = 18; + + optional bool lazyConnectionEnabled = 19; + + optional bool block_inbound = 20; + + repeated string natExternalIPs = 21; + bool cleanNATExternalIPs = 22; + + bytes customDNSAddress = 23; + + repeated string extraIFaceBlacklist = 24; + + repeated string dns_labels = 25; + // cleanDNSLabels clean map list of DNS labels. + bool cleanDNSLabels = 26; + + optional google.protobuf.Duration dnsRouteInterval = 27; + +} + +message SetConfigResponse{} + +message AddProfileRequest { + string username = 1; + string profileName = 2; +} + +message AddProfileResponse {} + +message RemoveProfileRequest { + string username = 1; + string profileName = 2; +} + +message RemoveProfileResponse {} + +message ListProfilesRequest { + string username = 1; +} + +message ListProfilesResponse { + repeated Profile profiles = 1; +} + +message Profile { + string name = 1; + bool is_active = 2; +} + +message GetActiveProfileRequest {} + +message GetActiveProfileResponse { + string profileName = 1; + string username = 2; +} \ No newline at end of file diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 6251f7c52..669083168 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -55,6 +55,12 @@ type DaemonServiceClient interface { TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) + SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error) + SetConfig(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*SetConfigResponse, error) + AddProfile(ctx context.Context, in *AddProfileRequest, opts ...grpc.CallOption) (*AddProfileResponse, error) + RemoveProfile(ctx context.Context, in *RemoveProfileRequest, opts ...grpc.CallOption) (*RemoveProfileResponse, error) + ListProfiles(ctx context.Context, in *ListProfilesRequest, opts ...grpc.CallOption) (*ListProfilesResponse, error) + GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error) } type daemonServiceClient struct { @@ -268,6 +274,60 @@ func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsReques return out, nil } +func (c *daemonServiceClient) SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error) { + out := new(SwitchProfileResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SwitchProfile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) SetConfig(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*SetConfigResponse, error) { + out := new(SetConfigResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetConfig", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) AddProfile(ctx context.Context, in *AddProfileRequest, opts ...grpc.CallOption) (*AddProfileResponse, error) { + out := new(AddProfileResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/AddProfile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) RemoveProfile(ctx context.Context, in *RemoveProfileRequest, opts ...grpc.CallOption) (*RemoveProfileResponse, error) { + out := new(RemoveProfileResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/RemoveProfile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) ListProfiles(ctx context.Context, in *ListProfilesRequest, opts ...grpc.CallOption) (*ListProfilesResponse, error) { + out := new(ListProfilesResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListProfiles", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error) { + out := new(GetActiveProfileResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetActiveProfile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -309,6 +369,12 @@ type DaemonServiceServer interface { TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) + SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error) + SetConfig(context.Context, *SetConfigRequest) (*SetConfigResponse, error) + AddProfile(context.Context, *AddProfileRequest) (*AddProfileResponse, error) + RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error) + ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error) + GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -376,6 +442,24 @@ func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, Daemo func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented") } +func (UnimplementedDaemonServiceServer) SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SwitchProfile not implemented") +} +func (UnimplementedDaemonServiceServer) SetConfig(context.Context, *SetConfigRequest) (*SetConfigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetConfig not implemented") +} +func (UnimplementedDaemonServiceServer) AddProfile(context.Context, *AddProfileRequest) (*AddProfileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddProfile not implemented") +} +func (UnimplementedDaemonServiceServer) RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveProfile not implemented") +} +func (UnimplementedDaemonServiceServer) ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListProfiles not implemented") +} +func (UnimplementedDaemonServiceServer) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetActiveProfile not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -752,6 +836,114 @@ func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _DaemonService_SwitchProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SwitchProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).SwitchProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/SwitchProfile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).SwitchProfile(ctx, req.(*SwitchProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_SetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).SetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/SetConfig", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).SetConfig(ctx, req.(*SetConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_AddProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).AddProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/AddProfile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).AddProfile(ctx, req.(*AddProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_RemoveProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).RemoveProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/RemoveProfile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).RemoveProfile(ctx, req.(*RemoveProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_ListProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).ListProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/ListProfiles", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).ListProfiles(ctx, req.(*ListProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_GetActiveProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetActiveProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).GetActiveProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/GetActiveProfile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).GetActiveProfile(ctx, req.(*GetActiveProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -835,6 +1027,30 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetEvents", Handler: _DaemonService_GetEvents_Handler, }, + { + MethodName: "SwitchProfile", + Handler: _DaemonService_SwitchProfile_Handler, + }, + { + MethodName: "SetConfig", + Handler: _DaemonService_SetConfig_Handler, + }, + { + MethodName: "AddProfile", + Handler: _DaemonService_AddProfile_Handler, + }, + { + MethodName: "RemoveProfile", + Handler: _DaemonService_RemoveProfile_Handler, + }, + { + MethodName: "ListProfiles", + Handler: _DaemonService_ListProfiles_Handler, + }, + { + MethodName: "GetActiveProfile", + Handler: _DaemonService_GetActiveProfile_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/client/server/panic_windows.go b/client/server/panic_windows.go index c5e73be7c..f441ec9ea 100644 --- a/client/server/panic_windows.go +++ b/client/server/panic_windows.go @@ -1,3 +1,6 @@ +//go:build windows +// +build windows + package server import ( diff --git a/client/server/server.go b/client/server/server.go index e3ce1a2b4..f3414888d 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -22,6 +22,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/management/domain" @@ -50,14 +51,12 @@ type Server struct { rootCtx context.Context actCancel context.CancelFunc - latestConfigInput internal.ConfigInput - logFile string oauthAuthFlow oauthAuthFlow mutex sync.Mutex - config *internal.Config + config *profilemanager.Config proto.UnimplementedDaemonServiceServer connectClient *internal.ConnectClient @@ -68,6 +67,8 @@ type Server struct { lastProbe time.Time persistNetworkMap bool isSessionActive atomic.Bool + + profileManager profilemanager.ServiceManager } type oauthAuthFlow struct { @@ -78,15 +79,13 @@ type oauthAuthFlow struct { } // New server instance constructor. -func New(ctx context.Context, configPath, logFile string) *Server { +func New(ctx context.Context, logFile string) *Server { return &Server{ - rootCtx: ctx, - latestConfigInput: internal.ConfigInput{ - ConfigPath: configPath, - }, + rootCtx: ctx, logFile: logFile, persistNetworkMap: true, statusRecorder: peer.NewRecorder(""), + profileManager: profilemanager.ServiceManager{}, } } @@ -99,7 +98,7 @@ func (s *Server) Start() error { log.Warnf("failed to redirect stderr: %v", err) } - if err := restoreResidualState(s.rootCtx); err != nil { + if err := restoreResidualState(s.rootCtx, s.profileManager.GetStatePath()); err != nil { log.Warnf(errRestoreResidualState, err) } @@ -118,25 +117,41 @@ func (s *Server) Start() error { ctx, cancel := context.WithCancel(s.rootCtx) s.actCancel = cancel - // if configuration exists, we just start connections. if is new config we skip and set status NeedsLogin - // on failure we return error to retry - config, err := internal.UpdateConfig(s.latestConfigInput) - if errorStatus, ok := gstatus.FromError(err); ok && errorStatus.Code() == codes.NotFound { - s.config, err = internal.UpdateOrCreateConfig(s.latestConfigInput) - if err != nil { - log.Warnf("unable to create configuration file: %v", err) - return err - } - state.Set(internal.StatusNeedsLogin) - return nil - } else if err != nil { - log.Warnf("unable to create configuration file: %v", err) - return err + // set the default config if not exists + if err := s.setDefaultConfigIfNotExists(ctx); err != nil { + log.Errorf("failed to set default config: %v", err) + return fmt.Errorf("failed to set default config: %w", err) } - // if configuration exists, we just start connections. - config, _ = internal.UpdateOldManagementURL(ctx, config, s.latestConfigInput.ConfigPath) + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + return fmt.Errorf("failed to get active profile state: %w", err) + } + cfgPath, err := activeProf.FilePath() + if err != nil { + log.Errorf("failed to get active profile file path: %v", err) + return fmt.Errorf("failed to get active profile file path: %w", err) + } + + config, err := profilemanager.GetConfig(cfgPath) + if err != nil { + log.Errorf("failed to get active profile config: %v", err) + + if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: "default", + Username: "", + }); err != nil { + log.Errorf("failed to set active profile state: %v", err) + return fmt.Errorf("failed to set active profile state: %w", err) + } + + config, err = profilemanager.GetConfig(s.profileManager.DefaultProfilePath()) + if err != nil { + log.Errorf("failed to get default profile config: %v", err) + return fmt.Errorf("failed to get default profile config: %w", err) + } + } s.config = config s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String()) @@ -157,10 +172,34 @@ func (s *Server) Start() error { return nil } +func (s *Server) setDefaultConfigIfNotExists(ctx context.Context) error { + ok, err := s.profileManager.CopyDefaultProfileIfNotExists() + if err != nil { + if err := s.profileManager.CreateDefaultProfile(); err != nil { + log.Errorf("failed to create default profile: %v", err) + return fmt.Errorf("failed to create default profile: %w", err) + } + + if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: "default", + Username: "", + }); err != nil { + log.Errorf("failed to set active profile state: %v", err) + return fmt.Errorf("failed to set active profile state: %w", err) + } + } + if ok { + state := internal.CtxGetState(ctx) + state.Set(internal.StatusNeedsLogin) + } + + return nil +} + // connectWithRetryRuns runs the client connection with a backoff strategy where we retry the operation as additional // mechanism to keep the client connected even when the connection is lost. // we cancel retry if the client receive a stop or down command, or if disable auto connect is configured. -func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Config, statusRecorder *peer.Status, +func (s *Server) connectWithRetryRuns(ctx context.Context, config *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}, ) { backOff := getConnectWithBackoff(ctx) @@ -276,6 +315,90 @@ func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (i return "", nil } +// Login uses setup key to prepare configuration for the daemon. +func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigRequest) (*proto.SetConfigResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + profState := profilemanager.ActiveProfileState{ + Name: msg.ProfileName, + Username: msg.Username, + } + + profPath, err := profState.FilePath() + if err != nil { + log.Errorf("failed to get active profile file path: %v", err) + return nil, fmt.Errorf("failed to get active profile file path: %w", err) + } + + var config profilemanager.ConfigInput + + config.ConfigPath = profPath + + if msg.ManagementUrl != "" { + config.ManagementURL = msg.ManagementUrl + } + + if msg.AdminURL != "" { + config.AdminURL = msg.AdminURL + } + + if msg.InterfaceName != nil { + config.InterfaceName = msg.InterfaceName + } + + if msg.WireguardPort != nil { + wgPort := int(*msg.WireguardPort) + config.WireguardPort = &wgPort + } + + if msg.OptionalPreSharedKey != nil { + if *msg.OptionalPreSharedKey != "" { + config.PreSharedKey = msg.OptionalPreSharedKey + } + } + + if msg.CleanDNSLabels { + config.DNSLabels = domain.List{} + + } else if msg.DnsLabels != nil { + dnsLabels := domain.FromPunycodeList(msg.DnsLabels) + config.DNSLabels = dnsLabels + } + + if msg.CleanNATExternalIPs { + config.NATExternalIPs = make([]string, 0) + } else if msg.NatExternalIPs != nil { + config.NATExternalIPs = msg.NatExternalIPs + } + + config.CustomDNSAddress = msg.CustomDNSAddress + if string(msg.CustomDNSAddress) == "empty" { + config.CustomDNSAddress = []byte{} + } + + config.RosenpassEnabled = msg.RosenpassEnabled + config.RosenpassPermissive = msg.RosenpassPermissive + config.DisableAutoConnect = msg.DisableAutoConnect + config.ServerSSHAllowed = msg.ServerSSHAllowed + config.NetworkMonitor = msg.NetworkMonitor + config.DisableClientRoutes = msg.DisableClientRoutes + config.DisableServerRoutes = msg.DisableServerRoutes + config.DisableDNS = msg.DisableDns + config.DisableFirewall = msg.DisableFirewall + config.BlockLANAccess = msg.BlockLanAccess + config.DisableNotifications = msg.DisableNotifications + config.LazyConnectionEnabled = msg.LazyConnectionEnabled + config.BlockInbound = msg.BlockInbound + + if _, err := profilemanager.UpdateConfig(config); err != nil { + log.Errorf("failed to update profile config: %v", err) + return nil, fmt.Errorf("failed to update profile config: %w", err) + } + + return &proto.SetConfigResponse{}, nil +} + // Login uses setup key to prepare configuration for the daemon. func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*proto.LoginResponse, error) { s.mutex.Lock() @@ -292,7 +415,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro s.actCancel = cancel s.mutex.Unlock() - if err := restoreResidualState(ctx); err != nil { + if err := restoreResidualState(ctx, s.profileManager.GetStatePath()); err != nil { log.Warnf(errRestoreResidualState, err) } @@ -304,147 +427,62 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro } }() + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + + if msg.ProfileName != nil { + if *msg.ProfileName != "default" && (msg.Username == nil || *msg.Username == "") { + log.Errorf("profile name is set to %s, but username is not provided", *msg.ProfileName) + return nil, fmt.Errorf("profile name is set to %s, but username is not provided", *msg.ProfileName) + } + + var username string + if *msg.ProfileName != "default" { + username = *msg.Username + } + + if *msg.ProfileName != activeProf.Name && username != activeProf.Username { + log.Infof("switching to profile %s for user '%s'", *msg.ProfileName, username) + if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: *msg.ProfileName, + Username: username, + }); err != nil { + log.Errorf("failed to set active profile state: %v", err) + return nil, fmt.Errorf("failed to set active profile state: %w", err) + } + } + } + + activeProf, err = s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + + log.Infof("active profile: %s for %s", activeProf.Name, activeProf.Username) + s.mutex.Lock() - inputConfig := s.latestConfigInput - - if msg.ManagementUrl != "" { - inputConfig.ManagementURL = msg.ManagementUrl - s.latestConfigInput.ManagementURL = msg.ManagementUrl - } - - if msg.AdminURL != "" { - inputConfig.AdminURL = msg.AdminURL - s.latestConfigInput.AdminURL = msg.AdminURL - } - - if msg.CleanNATExternalIPs { - inputConfig.NATExternalIPs = make([]string, 0) - s.latestConfigInput.NATExternalIPs = nil - } else if msg.NatExternalIPs != nil { - inputConfig.NATExternalIPs = msg.NatExternalIPs - s.latestConfigInput.NATExternalIPs = msg.NatExternalIPs - } - - inputConfig.CustomDNSAddress = msg.CustomDNSAddress - s.latestConfigInput.CustomDNSAddress = msg.CustomDNSAddress - if string(msg.CustomDNSAddress) == "empty" { - inputConfig.CustomDNSAddress = []byte{} - s.latestConfigInput.CustomDNSAddress = []byte{} - } if msg.Hostname != "" { // nolint ctx = context.WithValue(ctx, system.DeviceNameCtxKey, msg.Hostname) } - - if msg.RosenpassEnabled != nil { - inputConfig.RosenpassEnabled = msg.RosenpassEnabled - s.latestConfigInput.RosenpassEnabled = msg.RosenpassEnabled - } - - if msg.RosenpassPermissive != nil { - inputConfig.RosenpassPermissive = msg.RosenpassPermissive - s.latestConfigInput.RosenpassPermissive = msg.RosenpassPermissive - } - - if msg.ServerSSHAllowed != nil { - inputConfig.ServerSSHAllowed = msg.ServerSSHAllowed - s.latestConfigInput.ServerSSHAllowed = msg.ServerSSHAllowed - } - - if msg.DisableAutoConnect != nil { - inputConfig.DisableAutoConnect = msg.DisableAutoConnect - s.latestConfigInput.DisableAutoConnect = msg.DisableAutoConnect - } - - if msg.InterfaceName != nil { - inputConfig.InterfaceName = msg.InterfaceName - s.latestConfigInput.InterfaceName = msg.InterfaceName - } - - if msg.WireguardPort != nil { - port := int(*msg.WireguardPort) - inputConfig.WireguardPort = &port - s.latestConfigInput.WireguardPort = &port - } - - if msg.NetworkMonitor != nil { - inputConfig.NetworkMonitor = msg.NetworkMonitor - s.latestConfigInput.NetworkMonitor = msg.NetworkMonitor - } - - if len(msg.ExtraIFaceBlacklist) > 0 { - inputConfig.ExtraIFaceBlackList = msg.ExtraIFaceBlacklist - s.latestConfigInput.ExtraIFaceBlackList = msg.ExtraIFaceBlacklist - } - - if msg.DnsRouteInterval != nil { - duration := msg.DnsRouteInterval.AsDuration() - inputConfig.DNSRouteInterval = &duration - s.latestConfigInput.DNSRouteInterval = &duration - } - - if msg.DisableClientRoutes != nil { - inputConfig.DisableClientRoutes = msg.DisableClientRoutes - s.latestConfigInput.DisableClientRoutes = msg.DisableClientRoutes - } - if msg.DisableServerRoutes != nil { - inputConfig.DisableServerRoutes = msg.DisableServerRoutes - s.latestConfigInput.DisableServerRoutes = msg.DisableServerRoutes - } - if msg.DisableDns != nil { - inputConfig.DisableDNS = msg.DisableDns - s.latestConfigInput.DisableDNS = msg.DisableDns - } - if msg.DisableFirewall != nil { - inputConfig.DisableFirewall = msg.DisableFirewall - s.latestConfigInput.DisableFirewall = msg.DisableFirewall - } - if msg.BlockLanAccess != nil { - inputConfig.BlockLANAccess = msg.BlockLanAccess - s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess - } - if msg.BlockInbound != nil { - inputConfig.BlockInbound = msg.BlockInbound - s.latestConfigInput.BlockInbound = msg.BlockInbound - } - - if msg.CleanDNSLabels { - inputConfig.DNSLabels = domain.List{} - s.latestConfigInput.DNSLabels = nil - } else if msg.DnsLabels != nil { - dnsLabels := domain.FromPunycodeList(msg.DnsLabels) - inputConfig.DNSLabels = dnsLabels - s.latestConfigInput.DNSLabels = dnsLabels - } - - if msg.DisableNotifications != nil { - inputConfig.DisableNotifications = msg.DisableNotifications - s.latestConfigInput.DisableNotifications = msg.DisableNotifications - } - - if msg.LazyConnectionEnabled != nil { - inputConfig.LazyConnectionEnabled = msg.LazyConnectionEnabled - s.latestConfigInput.LazyConnectionEnabled = msg.LazyConnectionEnabled - } - s.mutex.Unlock() - if msg.OptionalPreSharedKey != nil { - inputConfig.PreSharedKey = msg.OptionalPreSharedKey - } - - config, err := internal.UpdateOrCreateConfig(inputConfig) + cfgPath, err := activeProf.FilePath() if err != nil { - return nil, err + log.Errorf("failed to get active profile file path: %v", err) + return nil, fmt.Errorf("failed to get active profile file path: %w", err) } - if msg.ManagementUrl == "" { - config, _ = internal.UpdateOldManagementURL(ctx, config, s.latestConfigInput.ConfigPath) - s.config = config - s.latestConfigInput.ManagementURL = config.ManagementURL.String() + config, err := profilemanager.GetConfig(cfgPath) + if err != nil { + log.Errorf("failed to get active profile config: %v", err) + return nil, fmt.Errorf("failed to get active profile config: %w", err) } - s.mutex.Lock() s.config = config s.mutex.Unlock() @@ -586,15 +624,17 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin return nil, err } - return &proto.WaitSSOLoginResponse{}, nil + return &proto.WaitSSOLoginResponse{ + Email: tokenInfo.Email, + }, nil } // Up starts engine work in the daemon. -func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpResponse, error) { +func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() - if err := restoreResidualState(callerCtx); err != nil { + if err := restoreResidualState(callerCtx, s.profileManager.GetStatePath()); err != nil { log.Warnf(errRestoreResidualState, err) } @@ -628,6 +668,40 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes return nil, fmt.Errorf("config is not defined, please call login command first") } + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + + if msg != nil && msg.ProfileName != nil { + if err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil { + log.Errorf("failed to switch profile: %v", err) + return nil, fmt.Errorf("failed to switch profile: %w", err) + } + } + + activeProf, err = s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + + log.Infof("active profile: %s for %s", activeProf.Name, activeProf.Username) + + cfgPath, err := activeProf.FilePath() + if err != nil { + log.Errorf("failed to get active profile file path: %v", err) + return nil, fmt.Errorf("failed to get active profile file path: %w", err) + } + + config, err := profilemanager.GetConfig(cfgPath) + if err != nil { + log.Errorf("failed to get active profile config: %v", err) + return nil, fmt.Errorf("failed to get active profile config: %w", err) + } + s.config = config + s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) @@ -651,6 +725,70 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes } } +func (s *Server) switchProfileIfNeeded(profileName string, userName *string, activeProf *profilemanager.ActiveProfileState) error { + if profileName != "default" && (userName == nil || *userName == "") { + log.Errorf("profile name is set to %s, but username is not provided", profileName) + return fmt.Errorf("profile name is set to %s, but username is not provided", profileName) + } + + var username string + if profileName != "default" { + username = *userName + } + + if profileName != activeProf.Name || username != activeProf.Username { + log.Infof("switching to profile %s for user %s", profileName, username) + if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: profileName, + Username: username, + }); err != nil { + log.Errorf("failed to set active profile state: %v", err) + return fmt.Errorf("failed to set active profile state: %w", err) + } + } + + return nil +} + +// SwitchProfile switches the active profile in the daemon. +func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfileRequest) (*proto.SwitchProfileResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + + if msg != nil && msg.ProfileName != nil { + if err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil { + log.Errorf("failed to switch profile: %v", err) + return nil, fmt.Errorf("failed to switch profile: %w", err) + } + } + activeProf, err = s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + cfgPath, err := activeProf.FilePath() + if err != nil { + log.Errorf("failed to get active profile file path: %v", err) + return nil, fmt.Errorf("failed to get active profile file path: %w", err) + } + + config, err := profilemanager.GetConfig(cfgPath) + if err != nil { + log.Errorf("failed to get default profile config: %v", err) + return nil, fmt.Errorf("failed to get default profile config: %w", err) + } + + s.config = config + + return &proto.SwitchProfileResponse{}, nil +} + // Down engine work in the daemon. func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownResponse, error) { s.mutex.Lock() @@ -738,58 +876,65 @@ func (s *Server) runProbes() { } // GetConfig of the daemon. -func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { +func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() - managementURL := s.latestConfigInput.ManagementURL - adminURL := s.latestConfigInput.AdminURL - preSharedKey := "" + if ctx.Err() != nil { + return nil, ctx.Err() + } - if s.config != nil { - if managementURL == "" && s.config.ManagementURL != nil { - managementURL = s.config.ManagementURL.String() - } + prof := profilemanager.ActiveProfileState{ + Name: req.ProfileName, + Username: req.Username, + } - if s.config.AdminURL != nil { - adminURL = s.config.AdminURL.String() - } + cfgPath, err := prof.FilePath() + if err != nil { + log.Errorf("failed to get active profile file path: %v", err) + return nil, fmt.Errorf("failed to get active profile file path: %w", err) + } - preSharedKey = s.config.PreSharedKey - if preSharedKey != "" { - preSharedKey = "**********" - } + cfg, err := profilemanager.GetConfig(cfgPath) + if err != nil { + log.Errorf("failed to get active profile config: %v", err) + return nil, fmt.Errorf("failed to get active profile config: %w", err) + } + managementURL := cfg.ManagementURL + adminURL := cfg.AdminURL + + var preSharedKey = cfg.PreSharedKey + if preSharedKey != "" { + preSharedKey = "**********" } disableNotifications := true - if s.config.DisableNotifications != nil { - disableNotifications = *s.config.DisableNotifications + if cfg.DisableNotifications != nil { + disableNotifications = *cfg.DisableNotifications } networkMonitor := false - if s.config.NetworkMonitor != nil { - networkMonitor = *s.config.NetworkMonitor + if cfg.NetworkMonitor != nil { + networkMonitor = *cfg.NetworkMonitor } - disableDNS := s.config.DisableDNS - disableClientRoutes := s.config.DisableClientRoutes - disableServerRoutes := s.config.DisableServerRoutes - blockLANAccess := s.config.BlockLANAccess + disableDNS := cfg.DisableDNS + disableClientRoutes := cfg.DisableClientRoutes + disableServerRoutes := cfg.DisableServerRoutes + blockLANAccess := cfg.BlockLANAccess return &proto.GetConfigResponse{ - ManagementUrl: managementURL, - ConfigFile: s.latestConfigInput.ConfigPath, - LogFile: s.logFile, + ManagementUrl: managementURL.String(), PreSharedKey: preSharedKey, - AdminURL: adminURL, - InterfaceName: s.config.WgIface, - WireguardPort: int64(s.config.WgPort), - DisableAutoConnect: s.config.DisableAutoConnect, - ServerSSHAllowed: *s.config.ServerSSHAllowed, - RosenpassEnabled: s.config.RosenpassEnabled, - RosenpassPermissive: s.config.RosenpassPermissive, - LazyConnectionEnabled: s.config.LazyConnectionEnabled, - BlockInbound: s.config.BlockInbound, + AdminURL: adminURL.String(), + InterfaceName: cfg.WgIface, + WireguardPort: int64(cfg.WgPort), + DisableAutoConnect: cfg.DisableAutoConnect, + ServerSSHAllowed: *cfg.ServerSSHAllowed, + RosenpassEnabled: cfg.RosenpassEnabled, + RosenpassPermissive: cfg.RosenpassPermissive, + LazyConnectionEnabled: cfg.LazyConnectionEnabled, + BlockInbound: cfg.BlockInbound, DisableNotifications: disableNotifications, NetworkMonitor: networkMonitor, DisableDns: disableDNS, @@ -918,3 +1063,82 @@ func sendTerminalNotification() error { return wallCmd.Wait() } + +// AddProfile adds a new profile to the daemon. +func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) (*proto.AddProfileResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if msg.ProfileName == "" || msg.Username == "" { + return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided") + } + + if err := s.profileManager.AddProfile(msg.ProfileName, msg.Username); err != nil { + log.Errorf("failed to create profile: %v", err) + return nil, fmt.Errorf("failed to create profile: %w", err) + } + + return &proto.AddProfileResponse{}, nil +} + +// RemoveProfile removes a profile from the daemon. +func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequest) (*proto.RemoveProfileResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if msg.ProfileName == "" { + return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") + } + + if err := s.profileManager.RemoveProfile(msg.ProfileName, msg.Username); err != nil { + log.Errorf("failed to remove profile: %v", err) + return nil, fmt.Errorf("failed to remove profile: %w", err) + } + + return &proto.RemoveProfileResponse{}, nil +} + +// ListProfiles lists all profiles in the daemon. +func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesRequest) (*proto.ListProfilesResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if msg.Username == "" { + return nil, gstatus.Errorf(codes.InvalidArgument, "username must be provided") + } + + profiles, err := s.profileManager.ListProfiles(msg.Username) + if err != nil { + log.Errorf("failed to list profiles: %v", err) + return nil, fmt.Errorf("failed to list profiles: %w", err) + } + + response := &proto.ListProfilesResponse{ + Profiles: make([]*proto.Profile, len(profiles)), + } + for i, profile := range profiles { + response.Profiles[i] = &proto.Profile{ + Name: profile.Name, + IsActive: profile.IsActive, + } + } + + return response, nil +} + +// GetActiveProfile returns the active profile in the daemon. +func (s *Server) GetActiveProfile(ctx context.Context, msg *proto.GetActiveProfileRequest) (*proto.GetActiveProfileResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + activeProfile, err := s.profileManager.GetActiveProfileState() + if err != nil { + log.Errorf("failed to get active profile state: %v", err) + return nil, fmt.Errorf("failed to get active profile state: %w", err) + } + + return &proto.GetActiveProfileResponse{ + ProfileName: activeProfile.Name, + Username: activeProfile.Username, + }, nil +} diff --git a/client/server/server_test.go b/client/server/server_test.go index 11e4d3899..dda610076 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -4,6 +4,8 @@ import ( "context" "net" "net/url" + "os/user" + "path/filepath" "testing" "time" @@ -20,6 +22,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/profilemanager" daemonProto "github.com/netbirdio/netbird/client/proto" mgmtProto "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server" @@ -32,7 +35,6 @@ import ( "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" - "github.com/netbirdio/netbird/util" ) var ( @@ -70,12 +72,30 @@ func TestConnectWithRetryRuns(t *testing.T) { ctx, cancel := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) defer cancel() // create new server - s := New(ctx, t.TempDir()+"/config.json", "debug") - s.latestConfigInput.ManagementURL = "http://" + mgmtAddr - config, err := internal.UpdateOrCreateConfig(s.latestConfigInput) + ic := profilemanager.ConfigInput{ + ManagementURL: "http://" + mgmtAddr, + ConfigPath: t.TempDir() + "/test-profile.json", + } + + config, err := profilemanager.UpdateOrCreateConfig(ic) if err != nil { t.Fatalf("failed to create config: %v", err) } + + currUser, err := user.Current() + require.NoError(t, err) + + pm := profilemanager.ServiceManager{} + err = pm.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: "test-profile", + Username: currUser.Username, + }) + if err != nil { + t.Fatalf("failed to set active profile state: %v", err) + } + + s := New(ctx, "debug") + s.config = config s.statusRecorder = peer.NewRecorder(config.ManagementURL.String()) @@ -91,26 +111,67 @@ func TestConnectWithRetryRuns(t *testing.T) { } func TestServer_Up(t *testing.T) { + tempDir := t.TempDir() + origDefaultProfileDir := profilemanager.DefaultConfigPathDir + origDefaultConfigPath := profilemanager.DefaultConfigPath + profilemanager.ConfigDirOverride = tempDir + origActiveProfileStatePath := profilemanager.ActiveProfileStatePath + profilemanager.DefaultConfigPathDir = tempDir + profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json" + profilemanager.DefaultConfigPath = filepath.Join(tempDir, "default.json") + t.Cleanup(func() { + profilemanager.DefaultConfigPathDir = origDefaultProfileDir + profilemanager.ActiveProfileStatePath = origActiveProfileStatePath + profilemanager.DefaultConfigPath = origDefaultConfigPath + profilemanager.ConfigDirOverride = "" + }) + ctx := internal.CtxInitState(context.Background()) - s := New(ctx, t.TempDir()+"/config.json", util.LogConsole) + currUser, err := user.Current() + require.NoError(t, err) - err := s.Start() + profName := "default" + + ic := profilemanager.ConfigInput{ + ConfigPath: filepath.Join(tempDir, profName+".json"), + } + + _, err = profilemanager.UpdateOrCreateConfig(ic) + if err != nil { + t.Fatalf("failed to create config: %v", err) + } + + pm := profilemanager.ServiceManager{} + err = pm.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: profName, + Username: currUser.Username, + }) + if err != nil { + t.Fatalf("failed to set active profile state: %v", err) + } + + s := New(ctx, "console") + + err = s.Start() require.NoError(t, err) u, err := url.Parse("http://non-existent-url-for-testing.invalid:12345") require.NoError(t, err) - s.config = &internal.Config{ + s.config = &profilemanager.Config{ ManagementURL: u, } upCtx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() - upReq := &daemonProto.UpRequest{} + upReq := &daemonProto.UpRequest{ + ProfileName: &profName, + Username: &currUser.Username, + } _, err = s.Up(upCtx, upReq) - assert.Contains(t, err.Error(), "NeedsLogin") + assert.Contains(t, err.Error(), "context deadline exceeded") } type mockSubscribeEventsServer struct { @@ -129,16 +190,51 @@ func (m *mockSubscribeEventsServer) Context() context.Context { } func TestServer_SubcribeEvents(t *testing.T) { + tempDir := t.TempDir() + origDefaultProfileDir := profilemanager.DefaultConfigPathDir + origDefaultConfigPath := profilemanager.DefaultConfigPath + profilemanager.ConfigDirOverride = tempDir + origActiveProfileStatePath := profilemanager.ActiveProfileStatePath + profilemanager.DefaultConfigPathDir = tempDir + profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json" + profilemanager.DefaultConfigPath = filepath.Join(tempDir, "default.json") + t.Cleanup(func() { + profilemanager.DefaultConfigPathDir = origDefaultProfileDir + profilemanager.ActiveProfileStatePath = origActiveProfileStatePath + profilemanager.DefaultConfigPath = origDefaultConfigPath + profilemanager.ConfigDirOverride = "" + }) + ctx := internal.CtxInitState(context.Background()) + ic := profilemanager.ConfigInput{ + ConfigPath: tempDir + "/default.json", + } - s := New(ctx, t.TempDir()+"/config.json", util.LogConsole) + _, err := profilemanager.UpdateOrCreateConfig(ic) + if err != nil { + t.Fatalf("failed to create config: %v", err) + } - err := s.Start() + currUser, err := user.Current() + require.NoError(t, err) + + pm := profilemanager.ServiceManager{} + err = pm.SetActiveProfileState(&profilemanager.ActiveProfileState{ + Name: "default", + Username: currUser.Username, + }) + if err != nil { + t.Fatalf("failed to set active profile state: %v", err) + } + + s := New(ctx, "console") + + err = s.Start() require.NoError(t, err) u, err := url.Parse("http://non-existent-url-for-testing.invalid:12345") require.NoError(t, err) - s.config = &internal.Config{ + s.config = &profilemanager.Config{ ManagementURL: u, } diff --git a/client/server/state.go b/client/server/state.go index 222c7c7bd..107f55154 100644 --- a/client/server/state.go +++ b/client/server/state.go @@ -16,7 +16,7 @@ import ( // ListStates returns a list of all saved states func (s *Server) ListStates(_ context.Context, _ *proto.ListStatesRequest) (*proto.ListStatesResponse, error) { - mgr := statemanager.New(statemanager.GetDefaultStatePath()) + mgr := statemanager.New(s.profileManager.GetStatePath()) stateNames, err := mgr.GetSavedStateNames() if err != nil { @@ -41,14 +41,16 @@ func (s *Server) CleanState(ctx context.Context, req *proto.CleanStateRequest) ( return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.") } + statePath := s.profileManager.GetStatePath() + if req.All { // Reuse existing cleanup logic for all states - if err := restoreResidualState(ctx); err != nil { + if err := restoreResidualState(ctx, statePath); err != nil { return nil, status.Errorf(codes.Internal, "failed to clean all states: %v", err) } // Get count of cleaned states - mgr := statemanager.New(statemanager.GetDefaultStatePath()) + mgr := statemanager.New(statePath) stateNames, err := mgr.GetSavedStateNames() if err != nil { return nil, status.Errorf(codes.Internal, "failed to get state count: %v", err) @@ -60,7 +62,7 @@ func (s *Server) CleanState(ctx context.Context, req *proto.CleanStateRequest) ( } // Handle single state cleanup - mgr := statemanager.New(statemanager.GetDefaultStatePath()) + mgr := statemanager.New(statePath) registerStates(mgr) if err := mgr.CleanupStateByName(req.StateName); err != nil { @@ -82,7 +84,7 @@ func (s *Server) DeleteState(ctx context.Context, req *proto.DeleteStateRequest) return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.") } - mgr := statemanager.New(statemanager.GetDefaultStatePath()) + mgr := statemanager.New(s.profileManager.GetStatePath()) var count int var err error @@ -112,13 +114,12 @@ func (s *Server) DeleteState(ctx context.Context, req *proto.DeleteStateRequest) // restoreResidualState checks if the client was not shut down in a clean way and restores residual if required. // Otherwise, we might not be able to connect to the management server to retrieve new config. -func restoreResidualState(ctx context.Context) error { - path := statemanager.GetDefaultStatePath() - if path == "" { +func restoreResidualState(ctx context.Context, statePath string) error { + if statePath == "" { return nil } - mgr := statemanager.New(path) + mgr := statemanager.New(statePath) // register the states we are interested in restoring registerStates(mgr) diff --git a/client/status/status.go b/client/status/status.go index d28485bc0..722ee7e7c 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -98,9 +98,10 @@ type OutputOverview struct { NSServerGroups []NsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` Events []SystemEventOutput `json:"events" yaml:"events"` LazyConnectionEnabled bool `json:"lazyConnectionEnabled" yaml:"lazyConnectionEnabled"` + ProfileName string `json:"profileName" yaml:"profileName"` } -func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter string) OutputOverview { +func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter string, profName string) OutputOverview { pbFullStatus := resp.GetFullStatus() managementState := pbFullStatus.GetManagementState() @@ -138,6 +139,7 @@ func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, status NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), Events: mapEvents(pbFullStatus.GetEvents()), LazyConnectionEnabled: pbFullStatus.GetLazyConnectionEnabled(), + ProfileName: profName, } if anon { @@ -406,6 +408,7 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, "OS: %s\n"+ "Daemon version: %s\n"+ "CLI version: %s\n"+ + "Profile: %s\n"+ "Management: %s\n"+ "Signal: %s\n"+ "Relays: %s\n"+ @@ -421,6 +424,7 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, fmt.Sprintf("%s/%s%s", goos, goarch, goarm), overview.DaemonVersion, version.NetbirdVersion(), + overview.ProfileName, managementConnString, signalConnString, relaysString, diff --git a/client/status/status_test.go b/client/status/status_test.go index 5b5d23efd..660efd9ef 100644 --- a/client/status/status_test.go +++ b/client/status/status_test.go @@ -234,7 +234,7 @@ var overview = OutputOverview{ } func TestConversionFromFullStatusToOutputOverview(t *testing.T) { - convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil, "") + convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil, "", "") assert.Equal(t, overview, convertedResult) } @@ -384,7 +384,8 @@ func TestParsingToJSON(t *testing.T) { } ], "events": [], - "lazyConnectionEnabled": false + "lazyConnectionEnabled": false, + "profileName":"" }` // @formatter:on @@ -486,6 +487,7 @@ dnsServers: error: timeout events: [] lazyConnectionEnabled: false +profileName: "" ` assert.Equal(t, expectedYAML, yaml) @@ -538,6 +540,7 @@ Events: No events recorded OS: %s/%s Daemon version: 0.14.1 CLI version: %s +Profile: Management: Connected to my-awesome-management.com:443 Signal: Connected to my-awesome-signal.com:443 Relays: @@ -565,6 +568,7 @@ func TestParsingToShortVersion(t *testing.T) { expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` Daemon version: 0.14.1 CLI version: development +Profile: Management: Connected Signal: Connected Relays: 1/2 Available diff --git a/client/ui/assets/connected.png b/client/ui/assets/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd2ab01ae50f84f784803fa2318c58578e04369 GIT binary patch literal 4743 zcmb_g_d6S08>aeFVzsE%RJDYvRVjknRTQD-YmcTzZDPgNSXDDMVy{v!wPHlgH;Bew z66fpt8CY6t|$7&y)@Qt+wZAlPtP}dv+`dhTR^SwO<-mS>;QYv3RXO0Yl>g(Jd5tY^9yO7fm6hv* zc$X0R{hpY_q0ITgR7?G;nGP->=g%l2`Oo|aeIf~Xc1G;Xi)=?-%>8z6LFXZwgf7hH zPn|~dvN_t+53P0@X~cUIekvl`Hw!mwq_hTW=TD1#Rzs8xrYDurpb|kmI zyRUaJQ-_f=>qB2Y*}7iMMrI?=y#XZ3D#;mJ(!2%NspeGWZ2JP~Lp}n3ww5G7 z4vH=u0)$(rgXP(K^ZV=3Ic~=T%JjE7jQ0<7b;nOYKlpr_u9$& zBOb{3J$Rb)J=l5#x+GcE1uT7LTB@$0IAV|>23E?i16{y(L#An=Ny1Ig(4fEN@aCgf zc@Dn}uV=dHa2tP_P_dRKDW6rXv6BJiofB$!F~T}nY{yGK!`nszV<^}Cr39*f0Kp8x z2>Lq&Mf1E|x+)fhLdI@++ zeGA%}RfmHxt;Jykc`_@FTyn2?=H0jP* z8<%2=*sRug*Yve)JzxmY)WZnEBp|+j(gl0pD*C6f(a++W$sZ}Sj1iYX=5ho-&#Q`b z{t5mgOMtPfogoreE>Z?$Zh&IY0pgM@(DZs)=L$&c zTuc-y@r8B|*wy#3Hmq#?kV9v0j1dYx@|&47sc8Y^;oYRx0<92D5_LwiAxeP^em>F8 zbY}h+g>P|~2i#?NHBGz(M4VqC0C%P2wJ%Jp3R-_v)!_ zqag5JZUb6|D2k!k>==}!sY|~55GlztArNZcT5$`?L&pNVqhh`>l5=F%7L;Xo&ck_9V`Us=j!tcUL<5G{tU29`EET2QW2NKamcnnKP}um`91yx6 zh#r|VbS#cZZtg_ZVROP=LAI}w@Y6IUGJepBt?wSuKE7{e=R1s7v`Sl#YHp8mi@b|t zg-BU5=2@gKlu&rW5O(RQ~6pStHHor*Za&PNCrFKCbTaxEoLs$GovK{Kf zaHxVTKpb2J_i8WqMS8J%w8-I-EJcWj3tO6~E)!dx#X@0{1`X&bb#+_uIU0LA%({>L zxulevNcZxhMsC4)sW;B>5B6>to~2}OO@YH$D@}MJO8};1@6k%B!<86^+Ulz54y46A z9w43Ivxa(n!f!cOWM~hLJ)JLf)Zkni5bHKM|7Yn@U>7+&MPe{WvqhNIO(#vpmVb4- zWN81=dMk5Y@$8=GZt#Y*vl%Pkp+WZ3Djw$a9p<@O?w^}b4C&8ax1?usk{2HR?;B%z zQBk%Jv5fSHL;uf=^kUL%Hy*yOKuMORmoMon$8tEd*?|KfqKx`Kk(TYQ?eYqh>>rT1 z2J>#U$fCb?3)Z8%_cD3#HZTN40QC0P>md15jaTy# zL5*t4%)8-VrT5h53-(r+@+@JAb%UpV-n`7aS?^u`lI9)PwoqRezm&eSZKYjxoXW#m zGQAOIlc!d=`Fg+IUV!|wmIk$(%%k_cQh{~qcpoI=Em6M60ZUhqa~2>hX_ZvJ1Ou`2 z%KiPZ%FBOqMTK^6$()svSBKW`GxejjzulSQwrGL(ff~?p-7vXKuDqng7(xm+uJ#n0 zJ50Ok7tb!X=BRohBf2_}pjyk3%mAXl_syAh zpL@-)Uc)SO*79=!n0evb&UnqVVo%G0?>JD^G?uB00s2+S-gI*y8(XR8fk(%6q0`Nt zKzPmL8M3f2FJ%i8!D=z#L!1H#EFB}wy-Y0tKS;V3mg&6cufWGc=AnHDMne*##lle9!3b+y@|MiHJc0# z{}B|Wo%R&$dDNx4EUWtOWW}zUyxDhKiDJtD<=dG2e}229Wp_h|i(Pl@Kl>c)V+Zxb zLEzAGhGR1WDwf&@JFDM*Q;@E#QeYG3S8Y?{9mpSgBp*4Os+cYMzaBu{>Y?*aZZ;~u zg6__4v6rTzc*rNyg~D*!spN{rd6;KTfz5mANA-_QpQ=r#GT;T6@IFxyBT+%mF->Zh z8O~@Q!GmjsD#5JU{#~t}gtQWB5RAqSJauQayX(pmPw&Ua!J*Myt40$|ylO#X8TWvS z%E=_EBHi!5cWm2tQvv1a%z>+1Di0*Ylcb`<3)Z#MJcN{|(Y$B*i-v{kaGUQ-51brS zL_BwD*1P)5+)Pq@`P(hg==7z_n2xFnB2VWs>mJR!XoCYJE~|MD*-MIgI8tmmbx-st zz`{Y0!&O5}xbXgV+=Tup50R5D>NT-=z;?<Aje?*B`mM9Wl#e)nlmZx)LGS>E+eK64w{p@a#zR+i^ZbQSG5ZTr*?iu zRht5~_`eR?Yh(x5_r80WcL}N*ORgoF(1|;6gRONw*p!VWuS(@USM!yHP9SF%JcA4J z;Wmf4QtQkAeMu{tD0JNNv5In}9Lw~lKaGx#Dh&dCe5j@?$6{;y$^O}y!UEc@9y$`8U#Am4V3lHQ|4R*s%F%-`Z5B_Limg+a%>t6`$7V6;H0lF?7@9VUYJ^Nt6KT z!EU9`>iv_XGs8=|e3`HGMC2YCWf5M9aOEZ~+;tBDReJe;D!^6@Q@)p$mxavEkN-vn zJ=^ajMGI$d+Sm^VO+dWcAQ)b^AQdw_T1VnuXzrK&;Y+^rBBBF(OdWI^Z3hD_D3EnA zcSR}~)B%{ap}Rap7TFk>e&e783p)j`$TI1LoMYnqts6l5!aQvI&MK>IDD|Z>5$t5p zQwjWf?Uij=!l|@NriN+_nXe^HPUbx5xQI7p^@7!c#XJcM^@7(`LRbVARvinTA%cbk zN*FUAF7-b$qgXUW4yY;jX8*yd_0%cmaJ=EKLf6z=B-YFgr&~RQRVXgG;@o!3Fu-2+vg_V$N`9zuZr|BpA zLmpSMe9-oCudRX+6l1|+^@6wbOCWpMhosSI+%%_y!3g*X z3UIwkq#Wi8LKLHeh=@I;2LQu@A=vN6z;<;iAK5fdsvT&I`B%&VMCjsJ<*JZQL-qXH zFdUT#un%=PfiM`Q0X*TYM&$EvrH>ABmbJ9*j*x+zr zQJ%Pw^_(0b1+#w&Ppt-8B-fg@1juq~5BBnKb&gz{F1woFCK2<;B=@?>+))>omq-}B zn(hVeOL{3k${pFTJcXL%w;%e&#p9dNfIdpTJ%ViAGPgtzBSOObkc)6)pcE~lWib%!$ zMcXeg_dmV)d=AMkwOP=jPGKR#T@nw2ugmIB#*8ygn<5aNk&L&P-Z=PM?YYfrzChva z>Ld}!-+B<&8|>_+TVeVv0^oYp*+QyLZyjI>-^jn;Uvnmz?*~IZ!VgWyR(b7~Y`#U>erWEmPXcxu&LhM+ zJhoTTDHQR}bJg2+#K8ZP$7u+~Vg9AC+J@awXZ}`!u-IJ5u6F)_eAt10IPY`AVV;)w zQaf$--eUd!%<@NF5K_G}vRQJckEWXKh`!h3dNP4_DWxj9(``U!dMYg%kR~Cu_H6wo z^gI+`fO8)&iiqnn8cb70b4tNpi&IQHg%A%twx9;}3Nj)Cq-#LoRVM*@rY_Tx011n( zu|W=~JnkrP)^;YR;52ZfEd2cRG2w#AEAf1uESr&&hX4zhW)|BZybV+LW8_v^=rqcnQF@iHrNi%3C|!8k)D(G z`HE2Yv9IKK^0DOnMCirwSE3nrg0Z$eE;s!VIdCXlxXB{%#wv)4P#1KMLi_+ZmS|=y zr!2Q?v0b+o-=B`1LZF=PvgTEcLfi3D#-Sk|0f*)f+8gl*)6}7w;n1Q|D(R_ytK?9T s^lVYL-u_s(rGlM5fBhf-UMRZGdDvsbuOptA$+3yZ7oC&9ytgCfkZ4X zU%Una@d<-K;9q|T04=ic;bjm=nQnQ}^hUJX^5lUw7oWJjkjRUNbe=U|{p-!0km*BF zF0Y%L?v_gEG#$a2U%GyLehM})F`=-}?D3Vy5j9Td93DB%XN+E}I$!B@nIBmfhS%)M8vcE`t;z?abOYuV?Ga27(Q}2l1wHW z*jZaoRQO>m!u5;d#+QGwooYs{*lbek@ z72gr{jnm05Hc`u+-Us^BnIt0f)^KAcF{s3*bC>mDugvf{L8JY&9j)Ckf6ukJ$E2vl zZh%=-JO|S&emX!JT&R$HDMY@clTF|%>AAV+Z0s`jFgv>@YAE+~Ds#^>eSvmMUj;RZ z3p3PT4%QM% zT|xM8U+wI)$+|liyRbjt)#BO$gjtL|;_5cyMSD0D%BKa}+$&JCGwG|dBdvZ2zY)*H zYv-We>foGe2t7I!45zSc zA4bW8^3X(a7r)&tj&q$eF0h;R0j>1G2zxc!Azgfv)6FW|s9}}qQKrs)&87~1{}vv< zh2BBhK*hEf1{Y_??o1kW8{|ZJ6ZH-!iafO02MTjWTu`Y?W0&C^oiIurUMz)Z+xU*w z_^e@OV10h8W)UX6C&`m=v&vz4bfsjW;nMDcD9EeMpL|IM!ZLc6@M_!;+Bq^h|PYNC8^BG z7|qfh9BcVb7bJzT7Tjw8)KlwrFbunUck!6o+&+)_M$50?KbwiQTR{)l^*kMBgh7;4 zax-Lz?PA26!W3b|1M;cYvEEN=iG);UMI&r`J3P3KqQW#>Dt`N|&p-^+_aKx$=TPNP zbB3v4SUpZqdhc1p?f-{pBuJZhna*wtebk#zPu*N+4Xh>Y>})iVW{Lz01VO&Go6VlR zS&HMeuK2fw`Fk^K3ka58Tfm<~KF|Ym?%bm}{bnus!+7ji%-Hbo@Kn1Sv)Br)HaT_R zDQMyhH3hI1hjfmuF)9UetcQG8LI%AXkvXagC1NDeB)F{67P{T*j4v$ZJH}S zz;1lkZRr@f?Oq)bOV$o5EG&dfe?Sy$HGo0XqmPabO(DMbu+0~~?(8JWqF^TP?rw*v)P0n#2EI&B7Fc7Ny@E7?SC$vk3xw1plNY7X2hmZlcHoFf|$ zKRur$$lqvzh$lR{{pH0a=USH*R=N$@P{EF`j`;L_1*`1mUmcaI}+U=LB1X{mhA4tUb`u!xct zZNXgx!Iv3$b&|X8;8I*Nc)6;e9ASOT%V0Re6!hs9qCmH6n=3sZWZfOH1#gRSG}Us$ zp|tA4$g7(OL`$++3)*5gZ=Xk6HtrEx-Q8{AdbAA^6~8-O*>%YSu!b+u!*<-;-sBmDq1yYI8{1mncF)ke-m0MNUH4xmmP#~=6=|*^*4lFp zq=D2a{M_s>ux2bhY*F5L^=FaT5g!We>;{$<-6!x1AucW+)3lAlL@Ms@RL-^OZbC(-@4j)eA}Y2|`GKVFvkqTGdL#a(tVWQJFo`vvNR$^$_YQM> zJcPeKB^$rwQ~Hx4-Ra&_KD%K2;B%hiz`f?`!oTuIX=h{99j}1`PEhBP-}OGGn(Hwy zS5&wQOAid)%I$w&H>9_@SrX=wJ!flN4$3q2d6hf+O}l?C)Mc8E+P`{}b3T`Lo@!WN zh4tL1FsyYSVQ`2Co{SG?AV7i7LnAkdIVw?us|mtmBGPXi70_09ZPDHAa|pvdM<=(H zKl7*Y_jbPu&iFRZ3YD@BE)`RAZV50_dT$=WI8x8L2Ew=7S4 zwrwFTlIUWKrYilV#1`G|@@|6)@C*$<>ubFY?2$RimE0NTcfbU>77g^xadYUR!0|L6 zppM)`IKLga+x{Z9qX3Nb>+a8MI#2ZCVhp*1q&ZaR$m^TzkVw%ueLBj!B%P#o|4~42=va9SZ(kFNo2n> z5W>U%h(BGhe_H$WWLrme1a|$~8-H9C<*&}R>$btx>nm#}1*!#)LU$2HQi_w!q0VdS zZ=GtCIORlsxzRYXrqnSWaSw6!`#i2U-2j-mr+d9gL&aY<{AhMBPJ@K@^2g54}uwZ&w zlOJAzvD`ixxNbL50nQUAj=unoU#w?liy_>AO%jG$7CftNkz3ffFIzJF>(R|lT~g+^ z+q}4ALvAxpWRN)LpEmUx7}WHZ?|M>;I*^a~(k3D(t~f-?wu{?jn;}2rp=_PBW}m{n ztpRJ5#`~zBJjhf{In&vb-G3p8EL!8hXpz3r?SuVUC2wXu1j#uU?c1V%nh{EAG-jL= z()g2F`k%Em;n!Z8Wwq7G=rS0SLVr62=shl6 z5{D_@JfH43VrDrM?J(eXS*cu+s`&$XkH7C#N=k~6X0WK0NtIw$Swmd_%kIkdu)mDQ zCt>4%bRk`|rgW7PWi4Oput}&V#>PfQ?P2Yoz=3ZecPm;Xj*i5??y1IM%j#k+to5JoE@&*yH_G#MS}dSq{~o?t1q? zKoe_>Ha|bluX)SZUhIe^yIkFS?x#0X6C_hJF{7ARHW@FUO`@ov1=6=F)5R@>@ zc$O&I>YCKcr4OeEJ~MPCQ^grYt+O962bfc}t;xa9{v~vK!^PGV(#eCmEEwY5h7)(Vov67rJjA z9=fo&7$DX0^vzz@y91fE7*LW5`lc!ma!AX{4a{tO$-IBUFMd6BE#HTfyX<0&sfO?C z z#eaA5IT-)bQpK0K`iqZg-Pr&lX=*(n~xI>LoM2wtG zC!f9AW8!<1`Bv-Gql%Lg#I_&5f@o_S5scBz^aFVdMAxG@;scbs1lMly{&7p4QVuVB zLnE$xl=DO@M@&oaR0Sx^Ip&iwVHBve*x>!6P2&&h1Ffa$`?PHeeLG z7lvwH74nbVedA`Riw5VZ6LStHXGv`xYPVK4d=T2Z`P*_MOjpy@K|RzG^PJfIXSZ)xa57Q=9ClIS=SeJV zV<&UJnx&Qlt7R+evRfRsJ}8UZU1*Dxn8B*Chq?qX>7I45$;V2dT1$;Cem1*E)u?gOeb zSu?&|tm}Q)YAE1Fhx6?{4ucZpI;iYnon3J5rJ!%uPqI895sJt+rFaXdrWP@z_ot|;*t0BmGZIh!>X~Nm7#_2sP9p}7PuMAa>X!oB50&yiLjqlS%iA6VhGZJWq zL?PYUWbFar!oszg(E*!$dGe`_S-v%W&r2#2bppQD`xsRv944J;zbEkn8`k#Ptu0HN zl%w8m(B?vO9X#Xlp;3cbEYihDg1A^7xg=K%un6aQBm6NmFzgwiQw~|?Um6hd>@}nV z_+;+KmY}WAnU_>7v2IYI=^v4J6zpFi?s=ECULwC-8TwD_@dyTNf0}*Eg%dJKIxpEt z5GP9vM>C_gca>l8SoBOJr;pEkK7n7f*+v3B>bvj{ZNiM%aYIr=y~m z)`C7FOM$6QE;R!3hT=Jc*e4H>EYT7q%Zj75Q9@ZwH#?;;@tp9 zGI;5S!Ts(WAmN6wJ=SKAMXRCiX8{l zJaPfWGU8@b(Z)m5PY0wMFDv@VHhLz6WIoz>0_kD@cYwEieCzl!H~P2Wo+R)mQ?hoqXl+=n zUXCOb?%z!P?;uzjR5`Ly&`fYGB zADyv0sfwiw>aASk{ZakxvOaNO*^Z+^fNsKQLnHJiUP{HAWgnn);aef?fXaVxb+RSa z!X-3Lj6vEI@f%E0Sap_mqb6FdXH}eMf(kN8Xz#1VF6w|O_>5^3=Fg2Y^7p4*mY`}i zDrZzlRw+DgY|Gnpppl(P|C-)gfHEE7 zSy_&X)_kqeRG%`Y_KS4Lg$mZNpE---%>1k2c-`ra1ca zR}K7cjoV`yOMsuc-SZ-+45qF{KS{sOspc~e=*OHJkHSwA2a5;=73+%1997BNOQ2)|IU4~x6cmqyfBuBTc`f@*gr(+4M&xXuN-l8Y;vku zsB?O=`0MwdmP_HACtgFol9M2}Ex*l2;3t{1=E2{25d1FA}mS(+&0 zN$q|)G~$MsvPYVC!%Ch6g%_JD16!WB7!@R(yR{xZFfV-DPoXUs_2h ze??8>9Rhhd5~arrbLERKc)7&4j-ey8q)==&WFn(nC>e6c^4pE0QC~fimlu<4G?Ib5 z)e*j0eAe3(2#l3}n3u`2p*=_c4Z&0GLkGrmU9PL&1!B7UB87slQY9`U5(0{R_X?)r79u0reBanyOT(O?F z3e@BpKLfx>1$|G)CTt1N?o!4Wf8W_|In<5=>~ku%eh=@qlojqzblLPvx2Sf;4MbT0 znYVFn&kOE(iS;_hesxRSi#po=Ua(rlLj<4^f1@({->Wn>lg9cu571?g>EHTt))#<) z7M7s*Iu`Q85flUWM8);IK$khY0O51c1wjG}$_5wkfSltdk%5Olu^Ucx%Yv5oWrxDM zE3!AQiOG710tEHsTORL0s(K*j+kjU8r{xPMv#j7c^dK5u3~}p--85Q+@%M#Lx9KFs zTpO}%(Z`j6j4WttUuMukb5Oc*dcvl1YD_nvrr#lJY=bGE%%;^ECI=Z4K%4s`uS7!C zsmVuT%a&f2u5wD@-bL!EU?B0fWAn|6a?-%GC!y`(5^utzGmxua)%mYz)&of_< z6APxHLqC2n!~P(ATWxq)=6s9R?N1i<$sO+6p9goSbBn`6cA~diURfeYcNN}fp$@WR-jieOl zC{p3Z&2%Nea4JTz`vBPgw>WG^z5mcgKsR1EE3j3F%{Lb`S`EX-8CfOlKhE!g3%`@} z?xvZ*Lj_lN+_=$MBYd%ZC*Rd69{`o6Nbf7D6MU#3qHj>+(i&a~EC)QkQ{N>zeoiJy zk?IRE6L?hi$aS+hbQfS$yj-QhLN1%vUPta#)wPq3)AzC^|E$Ov?}eK$-5M12xPKaL z;Ufj)NTNiKW=*s26wc?&rsH^ht=UNc_4nX~fQ&I>#7Eg%Hydw~TYw!-G#lbmVb9>{ zEdZn58fT=H@W`J{r_&AKy4K|-yx$M13h_U2039Ab4fIi zdt~Ni%(Tzso?%=(kE1dE2NXCYUb69_1}LY*16or84vjFf2a@K-tIfW_(o}z9%I6M` zgB$IdhLM)-!jFR}W|xNAmL~!vrb;3RF-GE`q$3lWNP=*tSmgG)?<>L7vdxhZKu`gP z`#qE|9Ow{V5eml%XG;1c#10mXubu`Q-ord!%`pduuTmq(hpCIkl2)(JPGdvxO@$tRohP4KHSwU{NX zX#wxtsXk_yMRRd{c^!N8Ys^pTHQ=;;&0B`g6g;Bj$ZJVKIiM_Uoz}PGds!_bV;U2m zjvl=)?YN>^Vv2q1ayQ>|uJnLk`niqEtKY+|p$G9J{<0pQyn76F%@!t)PTx7*|B;wN za<{XqqCh*-&IGxO)?zlgZTGg8k;g5MkQ)Q-tb=;=r(U~_gR}yXNaTWWp`-859SfsO zg~#{c@cy@swJwMtv^^j8#l+`IAdSZYX{x^2j~)3$%OR;85w0B;7QOJ#-zvuz|GeVi zkb|vaU!l55Utro$V+G{wF3*kKgq4<%PVMQza0bKv(jHG$3XV>Pd z7^bKE$hmLUyQ<$smA6xBoN@Qs&B33{rLO_Tb%x*nrnK8{j`7U&;njxf_KG^dD)7jA zd}&4eVurzZ;aVfCr>VXO)R ztmJ2IaE`Iuz!xv+_*tG{N;M&7Cel?Zx1-ap|wW=o$kc|@#w^##}+l-m>EY~sgaX4 zm*O?O4_#?ZA>OornUwSmsyn|87#rXm;m^|vY_>BKm};mk&J2uC6qn7gp$_#LjQ9(A zOi361ftVKVsPp}U+jcB1D=G4>-n%b)LY)t^zG`PzDG?oij&@zWMhHFkfnuOmv`^wS z%hqi3{cGX(I3IHGR;KA`)gQzbV>%P4?KJiC)jtEm{J6K2@u{u`^uQ~@@+Nzd`K*Or zylyuM%4S)SiwvCyJ;rYYh{EDT$(9lS{T}be>pi=2$FwJmx>z&9gUtD8n{=oM zW?cl@O`&bf1z@IgBYt-Nj`wwkLGfN}VbVQKU3Ux^c_kfS;HtEzj|8L-+E|g*VEoR* zLi`Ex#7@~l;muy*>$t4Z*p+|6FC1eJS~^o_35fP%pp8kQfk@P4J4+RSX6I%C62+#LulxnIu@)p4c#nnl(xlH%CSKKWudlah`)2^u$p%= z7T4?!sg&%F^23f$YFB?mZPwmWmPg( zTP-I>#iiJ*h4B6ffm8b+<2VTV=3R@(w$C4^yBcW>dDX|4sH^$LXtytssqlm`#{p&a zAcpTDf_~y=HqzdzR4t=FsDi(uyqsI#X5e6w1kO{Wj9@Vkt>ys~#3;%42BY{xCNn7Iv ziQ7p3hwzH1fh3I=OXat5D}#y?0et`jrW7jy!8R$pwToZpJ0Hn7R>x?O?*X5wq9 z%dUp59Z(tqu`lTzxCfM|2BbH{fGQz%N<_{EJ!*IGnk)1mwWu-!p7_%dv16z}z&)*! z1JPQ9cWW9$zUn3JeUaV?>JxuQphkyKB%SCu;@s@(@Q@0W$s-B?`kyBsL!wz5m2%fX zeeTqWq#Bk&y%kinu5Z;95vtoVZZbFd$<Y{)ITj9jX}Xr1>Eo6RQougYIg!v zWNU3wR}!F&WBgLDQ^kcE%P+f6`LG)JLAYVgf^L~X_G2?1m1S62ec7G4M!5CE-0nO^ z#;4SN>Z)>-uM4`^jk~MHqf=Pv51jS*>%XuRt__4J%Or=&CC9P6*{rqo&CRs*{w|lw z*s^d_{WE(jQ<-zdJWf?=LS)zic#%*zz4huv;qjKB;^?XDx#^q?h(1BjvN8U}(2p|( zEv$$Icls|oUsnt&3OJ#^MuE7o>>K!YUuS@_oj!sE3`=v%!!$h~CscHe(a}d8Z}r`! z7CsD*(61nysLy)&U5ek8)LUtZ-`%Rw!CHbm9zxX%k}E4K@jm>k*^P)E@nN{ZF8k=f zBA0e0RmP|ncz@BM%97OMDtD6$$~SMvyFEdP4XQ8|WP>}wxYZ~8Ag@Y8(;N~Dct~`e z9p?YD)=W=^5Z_3yfuP=^D}r8%(J1OFa*`s@Kc;}`R+JZvAiRQ*Vg7QKQX@QK%l%E< z?oma9mF3{nBeC3lSd=w|HmRc6*xDKg0&i)e)KTeFnah&uZf$RZQulduv~mlRd8Lj* z$DVc4)7d$;_r}iJsNvxb1A?)>2}-r7pjL{dpf9HaydPn%ZkTUX>>%UPNx+^+Bm=Fn z8%AfkC#N~JQOow+okbIS(5*uP%nK9G$*6@{OTCU)Z7+@$@;B6u6Zq!bS=-ZntA*h( z9IE2|R!UTW${q^8TW~5T!)u^tl`yoxVhaut15sUTS!>yAojX&uKwcKxg2)Z~6EqXm z%=JnOJ?()+l1?nCf$7Va?cb^*XwfJE&BDa{b)(ZSJ?CVOWPH|rqDmVcTj!N*{ zL=Xsi4Zk-sVL*-EGo%}zw(FoCO~KLD9m>~VdI{9c4E7CDOCPgT(-sysx^ApQN6*fr z>?-Nyl8i|=WeKjE?J_xg?IrWf)9$-FrL1Y44W`<*;uJ2(%pp%g^_XPtTr=I@+^0aKF09V&_d){q(IzwI3KxGYcP>tU_i<{~U;ksC=l#2pg#F+5`0u~a V>cxt`kmog7Ub4Mdd%@$u{{j=u(k1`^ literal 0 HcmV?d00001 diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index c18d96dae..781ec56f8 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -8,8 +8,10 @@ import ( "errors" "flag" "fmt" + "net/url" "os" "os/exec" + "os/user" "path" "runtime" "strconv" @@ -34,11 +36,14 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ui/desktop" "github.com/netbirdio/netbird/client/ui/event" "github.com/netbirdio/netbird/client/ui/process" + "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/version" @@ -54,11 +59,11 @@ const ( ) func main() { - daemonAddr, showSettings, showNetworks, showLoginURL, showDebug, errorMsg, saveLogsInFile := parseFlags() + flags := parseFlags() // Initialize file logging if needed. var logFile string - if saveLogsInFile { + if flags.saveLogsInFile { file, err := initLogFile() if err != nil { log.Errorf("error while initializing log: %v", err) @@ -74,19 +79,28 @@ func main() { a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected)) // Show error message window if needed. - if errorMsg != "" { - showErrorMessage(errorMsg) + if flags.errorMsg != "" { + showErrorMessage(flags.errorMsg) return } // Create the service client (this also builds the settings or networks UI if requested). - client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showLoginURL, showDebug) + client := newServiceClient(&newServiceClientArgs{ + addr: flags.daemonAddr, + logFile: logFile, + app: a, + showSettings: flags.showSettings, + showNetworks: flags.showNetworks, + showLoginURL: flags.showLoginURL, + showDebug: flags.showDebug, + showProfiles: flags.showProfiles, + }) // Watch for theme/settings changes to update the icon. go watchSettingsChanges(a, client) // Run in window mode if any UI flag was set. - if showSettings || showNetworks || showDebug || showLoginURL { + if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles { a.Run() return } @@ -106,21 +120,35 @@ func main() { systray.Run(client.onTrayReady, client.onTrayExit) } +type cliFlags struct { + daemonAddr string + showSettings bool + showNetworks bool + showProfiles bool + showDebug bool + showLoginURL bool + errorMsg string + saveLogsInFile bool +} + // parseFlags reads and returns all needed command-line flags. -func parseFlags() (daemonAddr string, showSettings, showNetworks, showLoginURL, showDebug bool, errorMsg string, saveLogsInFile bool) { +func parseFlags() *cliFlags { + var flags cliFlags + defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" } - flag.StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") - flag.BoolVar(&showSettings, "settings", false, "run settings window") - flag.BoolVar(&showNetworks, "networks", false, "run networks window") - flag.BoolVar(&showLoginURL, "login-url", false, "show login URL in a popup window") - flag.BoolVar(&showDebug, "debug", false, "run debug window") - flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window") - flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) + flag.StringVar(&flags.daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") + flag.BoolVar(&flags.showSettings, "settings", false, "run settings window") + flag.BoolVar(&flags.showNetworks, "networks", false, "run networks window") + flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window") + flag.BoolVar(&flags.showDebug, "debug", false, "run debug window") + flag.StringVar(&flags.errorMsg, "error-msg", "", "displays an error message window") + flag.BoolVar(&flags.saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) + flag.BoolVar(&flags.showLoginURL, "login-url", false, "show login URL in a popup window") flag.Parse() - return + return &flags } // initLogFile initializes logging into a file. @@ -168,6 +196,12 @@ var iconConnectingMacOS []byte //go:embed assets/netbird-systemtray-error-macos.png var iconErrorMacOS []byte +//go:embed assets/connected.png +var iconConnectedDot []byte + +//go:embed assets/disconnected.png +var iconDisconnectedDot []byte + type serviceClient struct { ctx context.Context cancel context.CancelFunc @@ -176,9 +210,13 @@ type serviceClient struct { eventHandler *eventHandler + profileManager *profilemanager.ProfileManager + icAbout []byte icConnected []byte + icConnectedDot []byte icDisconnected []byte + icDisconnectedDot []byte icUpdateConnected []byte icUpdateDisconnected []byte icConnecting []byte @@ -189,6 +227,7 @@ type serviceClient struct { mUp *systray.MenuItem mDown *systray.MenuItem mSettings *systray.MenuItem + mProfile *profileMenu mAbout *systray.MenuItem mGitHub *systray.MenuItem mVersionUI *systray.MenuItem @@ -214,7 +253,6 @@ type serviceClient struct { // input elements for settings form iMngURL *widget.Entry - iConfigFile *widget.Entry iLogFile *widget.Entry iPreSharedKey *widget.Entry iInterfaceName *widget.Entry @@ -247,6 +285,7 @@ type serviceClient struct { isUpdateIconActive bool showNetworks bool wNetworks fyne.Window + wProfiles fyne.Window eventManager *event.Manager @@ -263,36 +302,50 @@ type menuHandler struct { cancel context.CancelFunc } +type newServiceClientArgs struct { + addr string + logFile string + app fyne.App + showSettings bool + showNetworks bool + showDebug bool + showLoginURL bool + showProfiles bool +} + // newServiceClient instance constructor // // This constructor also builds the UI elements for the settings window. -func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showLoginURL bool, showDebug bool) *serviceClient { +func newServiceClient(args *newServiceClientArgs) *serviceClient { ctx, cancel := context.WithCancel(context.Background()) s := &serviceClient{ ctx: ctx, cancel: cancel, - addr: addr, - app: a, - logFile: logFile, + addr: args.addr, + app: args.app, + logFile: args.logFile, sendNotification: false, - showAdvancedSettings: showSettings, - showNetworks: showNetworks, + showAdvancedSettings: args.showSettings, + showNetworks: args.showNetworks, update: version.NewUpdate("nb/client-ui"), } s.eventHandler = newEventHandler(s) + s.profileManager = profilemanager.NewProfileManager() s.setNewIcons() switch { - case showSettings: + case args.showSettings: s.showSettingsUI() - case showNetworks: + case args.showNetworks: s.showNetworksUI() - case showLoginURL: + case args.showLoginURL: s.showLoginURL() - case showDebug: + case args.showDebug: s.showDebugUI() + case args.showProfiles: + s.showProfilesUI() } return s @@ -300,6 +353,8 @@ func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool func (s *serviceClient) setNewIcons() { s.icAbout = iconAbout + s.icConnectedDot = iconConnectedDot + s.icDisconnectedDot = iconDisconnectedDot if s.app.Settings().ThemeVariant() == theme.VariantDark { s.icConnected = iconConnectedDark s.icDisconnected = iconDisconnected @@ -342,8 +397,7 @@ func (s *serviceClient) showSettingsUI() { s.wSettings.SetOnClosed(s.cancel) s.iMngURL = widget.NewEntry() - s.iConfigFile = widget.NewEntry() - s.iConfigFile.Disable() + s.iLogFile = widget.NewEntry() s.iLogFile.Disable() s.iPreSharedKey = widget.NewPasswordEntry() @@ -368,14 +422,22 @@ func (s *serviceClient) showSettingsUI() { // getSettingsForm to embed it into settings window. func (s *serviceClient) getSettingsForm() *widget.Form { + + var activeProfName string + activeProf, err := s.profileManager.GetActiveProfile() + if err != nil { + log.Errorf("get active profile: %v", err) + } else { + activeProfName = activeProf.Name + } return &widget.Form{ Items: []*widget.FormItem{ + {Text: "Profile", Widget: widget.NewLabel(activeProfName)}, {Text: "Quantum-Resistance", Widget: s.sRosenpassPermissive}, {Text: "Interface Name", Widget: s.iInterfaceName}, {Text: "Interface Port", Widget: s.iInterfacePort}, {Text: "Management URL", Widget: s.iMngURL}, {Text: "Pre-shared Key", Widget: s.iPreSharedKey}, - {Text: "Config File", Widget: s.iConfigFile}, {Text: "Log File", Widget: s.iLogFile}, {Text: "Network Monitor", Widget: s.sNetworkMonitor}, {Text: "Disable DNS", Widget: s.sDisableDNS}, @@ -416,27 +478,67 @@ func (s *serviceClient) getSettingsForm() *widget.Form { s.managementURL = iMngURL s.preSharedKey = s.iPreSharedKey.Text - loginRequest := proto.LoginRequest{ - ManagementUrl: iMngURL, - IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", - RosenpassPermissive: &s.sRosenpassPermissive.Checked, - InterfaceName: &s.iInterfaceName.Text, - WireguardPort: &port, - NetworkMonitor: &s.sNetworkMonitor.Checked, - DisableDns: &s.sDisableDNS.Checked, - DisableClientRoutes: &s.sDisableClientRoutes.Checked, - DisableServerRoutes: &s.sDisableServerRoutes.Checked, - BlockLanAccess: &s.sBlockLANAccess.Checked, - } - - if s.iPreSharedKey.Text != censoredPreSharedKey { - loginRequest.OptionalPreSharedKey = &s.iPreSharedKey.Text - } - - if err := s.restartClient(&loginRequest); err != nil { - log.Errorf("restarting client connection: %v", err) + currUser, err := user.Current() + if err != nil { + log.Errorf("get current user: %v", err) return } + + var req proto.SetConfigRequest + req.ProfileName = activeProf.Name + req.Username = currUser.Username + + if iMngURL != "" { + req.ManagementUrl = iMngURL + } + + req.RosenpassPermissive = &s.sRosenpassPermissive.Checked + req.InterfaceName = &s.iInterfaceName.Text + req.WireguardPort = &port + req.NetworkMonitor = &s.sNetworkMonitor.Checked + req.DisableDns = &s.sDisableDNS.Checked + req.DisableClientRoutes = &s.sDisableClientRoutes.Checked + req.DisableServerRoutes = &s.sDisableServerRoutes.Checked + req.BlockLanAccess = &s.sBlockLANAccess.Checked + + if s.iPreSharedKey.Text != censoredPreSharedKey { + req.OptionalPreSharedKey = &s.iPreSharedKey.Text + } + + conn, err := s.getSrvClient(failFastTimeout) + if err != nil { + log.Errorf("get client: %v", err) + dialog.ShowError(fmt.Errorf("Failed to connect to the service: %v", err), s.wSettings) + return + } + _, err = conn.SetConfig(s.ctx, &req) + if err != nil { + log.Errorf("set config: %v", err) + dialog.ShowError(fmt.Errorf("Failed to set configuration: %v", err), s.wSettings) + return + } + + status, err := conn.Status(s.ctx, &proto.StatusRequest{}) + if err != nil { + log.Errorf("get service status: %v", err) + dialog.ShowError(fmt.Errorf("Failed to get service status: %v", err), s.wSettings) + return + } + if status.Status == string(internal.StatusConnected) { + // run down & up + _, err = conn.Down(s.ctx, &proto.DownRequest{}) + if err != nil { + log.Errorf("down service: %v", err) + } + + _, err = conn.Up(s.ctx, &proto.UpRequest{}) + if err != nil { + log.Errorf("up service: %v", err) + dialog.ShowError(fmt.Errorf("Failed to reconnect: %v", err), s.wSettings) + return + } + } + } }, OnCancel: func() { @@ -452,8 +554,21 @@ func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) { return nil, err } + activeProf, err := s.profileManager.GetActiveProfile() + if err != nil { + log.Errorf("get active profile: %v", err) + return nil, err + } + + currUser, err := user.Current() + if err != nil { + return nil, fmt.Errorf("get current user: %w", err) + } + loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{ IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", + ProfileName: &activeProf.Name, + Username: &currUser.Username, }) if err != nil { log.Errorf("login to management URL with: %v", err) @@ -461,15 +576,9 @@ func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) { } if loginResp.NeedsSSOLogin && openURL { - err = open.Run(loginResp.VerificationURIComplete) + err = s.handleSSOLogin(loginResp, conn) if err != nil { - log.Errorf("opening the verification uri in the browser failed: %v", err) - return nil, err - } - - _, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) - if err != nil { - log.Errorf("waiting sso login failed with: %v", err) + log.Errorf("handle SSO login failed: %v", err) return nil, err } } @@ -477,6 +586,34 @@ func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) { return loginResp, nil } +func (s *serviceClient) handleSSOLogin(loginResp *proto.LoginResponse, conn proto.DaemonServiceClient) error { + err := open.Run(loginResp.VerificationURIComplete) + if err != nil { + log.Errorf("opening the verification uri in the browser failed: %v", err) + return err + } + + resp, err := conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) + if err != nil { + log.Errorf("waiting sso login failed with: %v", err) + return err + } + + if resp.Email != "" { + err := s.profileManager.SetActiveProfileState(&profilemanager.ProfileState{ + Email: resp.Email, + }) + if err != nil { + log.Warnf("failed to set profile state: %v", err) + } else { + s.mProfile.refresh() + } + + } + + return nil +} + func (s *serviceClient) menuUpClick() error { systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) @@ -575,6 +712,7 @@ func (s *serviceClient) updateStatus() error { } systray.SetTooltip("NetBird (Connected)") s.mStatus.SetTitle("Connected") + s.mStatus.SetIcon(s.icConnectedDot) s.mUp.Disable() s.mDown.Enable() s.mNetworks.Enable() @@ -634,6 +772,7 @@ func (s *serviceClient) setDisconnectedStatus() { } systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") + s.mStatus.SetIcon(s.icDisconnectedDot) s.mDown.Disable() s.mUp.Enable() s.mNetworks.Disable() @@ -658,7 +797,13 @@ func (s *serviceClient) onTrayReady() { // setup systray menu items s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected") + s.mStatus.SetIcon(s.icDisconnectedDot) s.mStatus.Disable() + + profileMenuItem := systray.AddMenuItem("", "") + emailMenuItem := systray.AddMenuItem("", "") + s.mProfile = newProfileMenu(s.ctx, s.profileManager, *s.eventHandler, profileMenuItem, emailMenuItem, s.menuDownClick, s.menuUpClick, s.getSrvClient, s.loadSettings) + systray.AddSeparator() s.mUp = systray.AddMenuItem("Connect", "Connect") s.mDown = systray.AddMenuItem("Disconnect", "Disconnect") @@ -790,7 +935,15 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService // getSrvConfig from the service to show it in the settings window. func (s *serviceClient) getSrvConfig() { - s.managementURL = internal.DefaultManagementURL + s.managementURL = profilemanager.DefaultManagementURL + + _, err := s.profileManager.GetActiveProfile() + if err != nil { + log.Errorf("get active profile: %v", err) + return + } + + var cfg *profilemanager.Config conn, err := s.getSrvClient(failFastTimeout) if err != nil { @@ -798,48 +951,63 @@ func (s *serviceClient) getSrvConfig() { return } - cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{}) + currUser, err := user.Current() + if err != nil { + log.Errorf("get current user: %v", err) + return + } + + activeProf, err := s.profileManager.GetActiveProfile() + if err != nil { + log.Errorf("get active profile: %v", err) + return + } + + srvCfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{ + ProfileName: activeProf.Name, + Username: currUser.Username, + }) if err != nil { log.Errorf("get config settings from server: %v", err) return } - if cfg.ManagementUrl != "" { - s.managementURL = cfg.ManagementUrl + cfg = protoConfigToConfig(srvCfg) + + if cfg.ManagementURL.String() != "" { + s.managementURL = cfg.ManagementURL.String() } s.preSharedKey = cfg.PreSharedKey s.RosenpassPermissive = cfg.RosenpassPermissive - s.interfaceName = cfg.InterfaceName - s.interfacePort = int(cfg.WireguardPort) + s.interfaceName = cfg.WgIface + s.interfacePort = cfg.WgPort - s.networkMonitor = cfg.NetworkMonitor - s.disableDNS = cfg.DisableDns + s.networkMonitor = *cfg.NetworkMonitor + s.disableDNS = cfg.DisableDNS s.disableClientRoutes = cfg.DisableClientRoutes s.disableServerRoutes = cfg.DisableServerRoutes - s.blockLANAccess = cfg.BlockLanAccess + s.blockLANAccess = cfg.BlockLANAccess if s.showAdvancedSettings { s.iMngURL.SetText(s.managementURL) - s.iConfigFile.SetText(cfg.ConfigFile) - s.iLogFile.SetText(cfg.LogFile) s.iPreSharedKey.SetText(cfg.PreSharedKey) - s.iInterfaceName.SetText(cfg.InterfaceName) - s.iInterfacePort.SetText(strconv.Itoa(int(cfg.WireguardPort))) + s.iInterfaceName.SetText(cfg.WgIface) + s.iInterfacePort.SetText(strconv.Itoa(cfg.WgPort)) s.sRosenpassPermissive.SetChecked(cfg.RosenpassPermissive) if !cfg.RosenpassEnabled { s.sRosenpassPermissive.Disable() } - s.sNetworkMonitor.SetChecked(cfg.NetworkMonitor) - s.sDisableDNS.SetChecked(cfg.DisableDns) + s.sNetworkMonitor.SetChecked(*cfg.NetworkMonitor) + s.sDisableDNS.SetChecked(cfg.DisableDNS) s.sDisableClientRoutes.SetChecked(cfg.DisableClientRoutes) s.sDisableServerRoutes.SetChecked(cfg.DisableServerRoutes) - s.sBlockLANAccess.SetChecked(cfg.BlockLanAccess) + s.sBlockLANAccess.SetChecked(cfg.BlockLANAccess) } if s.mNotifications == nil { return } - if cfg.DisableNotifications { + if cfg.DisableNotifications != nil && *cfg.DisableNotifications { s.mNotifications.Uncheck() } else { s.mNotifications.Check() @@ -849,6 +1017,58 @@ func (s *serviceClient) getSrvConfig() { } } +func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config { + + var config profilemanager.Config + + if cfg.ManagementUrl != "" { + parsed, err := url.Parse(cfg.ManagementUrl) + if err != nil { + log.Errorf("parse management URL: %v", err) + } else { + config.ManagementURL = parsed + } + } + + if cfg.PreSharedKey != "" { + if cfg.PreSharedKey != censoredPreSharedKey { + config.PreSharedKey = cfg.PreSharedKey + } else { + config.PreSharedKey = "" + } + } + if cfg.AdminURL != "" { + parsed, err := url.Parse(cfg.AdminURL) + if err != nil { + log.Errorf("parse admin URL: %v", err) + } else { + config.AdminURL = parsed + } + } + + config.WgIface = cfg.InterfaceName + if cfg.WireguardPort != 0 { + config.WgPort = int(cfg.WireguardPort) + } else { + config.WgPort = iface.DefaultWgPort + } + + config.DisableAutoConnect = cfg.DisableAutoConnect + config.ServerSSHAllowed = &cfg.ServerSSHAllowed + config.RosenpassEnabled = cfg.RosenpassEnabled + config.RosenpassPermissive = cfg.RosenpassPermissive + config.DisableNotifications = &cfg.DisableNotifications + config.LazyConnectionEnabled = cfg.LazyConnectionEnabled + config.BlockInbound = cfg.BlockInbound + config.NetworkMonitor = &cfg.NetworkMonitor + config.DisableDNS = cfg.DisableDns + config.DisableClientRoutes = cfg.DisableClientRoutes + config.DisableServerRoutes = cfg.DisableServerRoutes + config.BlockLANAccess = cfg.BlockLanAccess + + return &config +} + func (s *serviceClient) onUpdateAvailable() { s.updateIndicationLock.Lock() defer s.updateIndicationLock.Unlock() @@ -880,7 +1100,22 @@ func (s *serviceClient) loadSettings() { return } - cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{}) + currUser, err := user.Current() + if err != nil { + log.Errorf("get current user: %v", err) + return + } + + activeProf, err := s.profileManager.GetActiveProfile() + if err != nil { + log.Errorf("get active profile: %v", err) + return + } + + cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{ + ProfileName: activeProf.Name, + Username: currUser.Username, + }) if err != nil { log.Errorf("get config settings from server: %v", err) return @@ -936,41 +1171,37 @@ func (s *serviceClient) updateConfig() error { blockInbound := s.mBlockInbound.Checked() notificationsDisabled := !s.mNotifications.Checked() - loginRequest := proto.LoginRequest{ - IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", + activeProf, err := s.profileManager.GetActiveProfile() + if err != nil { + log.Errorf("get active profile: %v", err) + return err + } + + currUser, err := user.Current() + if err != nil { + log.Errorf("get current user: %v", err) + return err + } + + conn, err := s.getSrvClient(failFastTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return err + } + + req := proto.SetConfigRequest{ + ProfileName: activeProf.Name, + Username: currUser.Username, + DisableAutoConnect: &disableAutoStart, ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, - DisableAutoConnect: &disableAutoStart, - DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, BlockInbound: &blockInbound, + DisableNotifications: ¬ificationsDisabled, } - if err := s.restartClient(&loginRequest); err != nil { - log.Errorf("restarting client connection: %v", err) - return err - } - - return nil -} - -// restartClient restarts the client connection. -func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { - ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) - defer cancel() - - client, err := s.getSrvClient(failFastTimeout) - if err != nil { - return err - } - - _, err = client.Login(ctx, loginRequest) - if err != nil { - return err - } - - _, err = client.Up(ctx, &proto.UpRequest{}) - if err != nil { + if _, err := conn.SetConfig(s.ctx, &req); err != nil { + log.Errorf("set config settings on server: %v", err) return err } diff --git a/client/ui/const.go b/client/ui/const.go index 5a4b27f32..332282c17 100644 --- a/client/ui/const.go +++ b/client/ui/const.go @@ -2,6 +2,7 @@ package main const ( settingsMenuDescr = "Settings of the application" + profilesMenuDescr = "Manage your profiles" allowSSHMenuDescr = "Allow SSH connections" autoConnectMenuDescr = "Connect automatically when the service starts" quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass" diff --git a/client/ui/debug.go b/client/ui/debug.go index 55829de1e..a7f4868ac 100644 --- a/client/ui/debug.go +++ b/client/ui/debug.go @@ -433,7 +433,7 @@ func (s *serviceClient) collectDebugData( var postUpStatusOutput string if postUpStatus != nil { - overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil, "") + overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil, "", "") postUpStatusOutput = nbstatus.ParseToFullDetailSummary(overview) } headerPostUp := fmt.Sprintf("----- NetBird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) @@ -450,7 +450,7 @@ func (s *serviceClient) collectDebugData( var preDownStatusOutput string if preDownStatus != nil { - overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil, "") + overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil, "", "") preDownStatusOutput = nbstatus.ParseToFullDetailSummary(overview) } headerPreDown := fmt.Sprintf("----- NetBird pre-down - Timestamp: %s - Duration: %s", @@ -581,7 +581,7 @@ func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploa var statusOutput string if statusResp != nil { - overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil, "") + overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil, "", "") statusOutput = nbstatus.ParseToFullDetailSummary(overview) } diff --git a/client/ui/profile.go b/client/ui/profile.go new file mode 100644 index 000000000..142582c25 --- /dev/null +++ b/client/ui/profile.go @@ -0,0 +1,601 @@ +//go:build !(linux && 386) + +package main + +import ( + "context" + "errors" + "fmt" + "os/user" + "slices" + "sort" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" + "fyne.io/systray" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" +) + +// showProfilesUI creates and displays the Profiles window with a list of existing profiles, +// a button to add new profiles, allows removal, and lets the user switch the active profile. +func (s *serviceClient) showProfilesUI() { + + profiles, err := s.getProfiles() + if err != nil { + log.Errorf("get profiles: %v", err) + return + } + + var refresh func() + // List widget for profiles + list := widget.NewList( + func() int { return len(profiles) }, + func() fyne.CanvasObject { + // Each item: Selected indicator, Name, spacer, Select & Remove buttons + return container.NewHBox( + widget.NewLabel(""), // indicator + widget.NewLabel(""), // profile name + layout.NewSpacer(), + widget.NewButton("Select", nil), + widget.NewButton("Remove", nil), + ) + }, + func(i widget.ListItemID, item fyne.CanvasObject) { + // Populate each row + row := item.(*fyne.Container) + indicator := row.Objects[0].(*widget.Label) + nameLabel := row.Objects[1].(*widget.Label) + selectBtn := row.Objects[3].(*widget.Button) + removeBtn := row.Objects[4].(*widget.Button) + + profile := profiles[i] + // Show a checkmark if selected + if profile.IsActive { + indicator.SetText("✓") + } else { + indicator.SetText("") + } + nameLabel.SetText(profile.Name) + + // Configure Select/Active button + selectBtn.SetText(func() string { + if profile.IsActive { + return "Active" + } + return "Select" + }()) + selectBtn.OnTapped = func() { + if profile.IsActive { + return // already active + } + // confirm switch + dialog.ShowConfirm( + "Switch Profile", + fmt.Sprintf("Are you sure you want to switch to '%s'?", profile.Name), + func(confirm bool) { + if !confirm { + return + } + // switch + err = s.switchProfile(profile.Name) + if err != nil { + log.Errorf("failed to switch profile: %v", err) + dialog.ShowError(errors.New("failed to select profile"), s.wProfiles) + return + } + + dialog.ShowInformation( + "Profile Switched", + fmt.Sprintf("Profile '%s' switched successfully", profile.Name), + s.wProfiles, + ) + + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("failed to get daemon client: %v", err) + return + } + + status, err := conn.Status(context.Background(), &proto.StatusRequest{}) + if err != nil { + log.Errorf("failed to get status after switching profile: %v", err) + return + } + + if status.Status == string(internal.StatusConnected) { + if err := s.menuDownClick(); err != nil { + log.Errorf("failed to handle down click after switching profile: %v", err) + dialog.ShowError(fmt.Errorf("failed to handle down click"), s.wProfiles) + return + } + } + // update slice flags + refresh() + }, + s.wProfiles, + ) + } + + // Remove profile + removeBtn.SetText("Remove") + removeBtn.OnTapped = func() { + dialog.ShowConfirm( + "Delete Profile", + fmt.Sprintf("Are you sure you want to delete '%s'?", profile.Name), + func(confirm bool) { + if !confirm { + return + } + // remove + err = s.removeProfile(profile.Name) + if err != nil { + log.Errorf("failed to remove profile: %v", err) + dialog.ShowError(fmt.Errorf("failed to remove profile"), s.wProfiles) + return + } + dialog.ShowInformation( + "Profile Removed", + fmt.Sprintf("Profile '%s' removed successfully", profile.Name), + s.wProfiles, + ) + // update slice + refresh() + }, + s.wProfiles, + ) + } + }, + ) + + refresh = func() { + newProfiles, err := s.getProfiles() + if err != nil { + dialog.ShowError(err, s.wProfiles) + return + } + profiles = newProfiles // update the slice + list.Refresh() // tell Fyne to re-call length/update on every visible row + } + + // Button to add a new profile + newBtn := widget.NewButton("New Profile", func() { + nameEntry := widget.NewEntry() + nameEntry.SetPlaceHolder("Enter Profile Name") + + formItems := []*widget.FormItem{{Text: "Name:", Widget: nameEntry}} + dlg := dialog.NewForm( + "New Profile", + "Create", + "Cancel", + formItems, + func(confirm bool) { + if !confirm { + return + } + name := nameEntry.Text + if name == "" { + dialog.ShowError(errors.New("profile name cannot be empty"), s.wProfiles) + return + } + + // add profile + err = s.addProfile(name) + if err != nil { + log.Errorf("failed to create profile: %v", err) + dialog.ShowError(fmt.Errorf("failed to create profile"), s.wProfiles) + return + } + dialog.ShowInformation( + "Profile Created", + fmt.Sprintf("Profile '%s' created successfully", name), + s.wProfiles, + ) + // update slice + refresh() + }, + s.wProfiles, + ) + // make dialog wider + dlg.Resize(fyne.NewSize(350, 150)) + dlg.Show() + }) + + // Assemble window content + content := container.NewBorder(nil, newBtn, nil, nil, list) + s.wProfiles = s.app.NewWindow("NetBird Profiles") + s.wProfiles.SetContent(content) + s.wProfiles.Resize(fyne.NewSize(400, 300)) + s.wProfiles.SetOnClosed(s.cancel) + + s.wProfiles.Show() +} + +func (s *serviceClient) addProfile(profileName string) error { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf(getClientFMT, err) + } + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + _, err = conn.AddProfile(context.Background(), &proto.AddProfileRequest{ + ProfileName: profileName, + Username: currUser.Username, + }) + + if err != nil { + return fmt.Errorf("add profile: %w", err) + } + + return nil +} + +func (s *serviceClient) switchProfile(profileName string) error { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf(getClientFMT, err) + } + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + if _, err := conn.SwitchProfile(context.Background(), &proto.SwitchProfileRequest{ + ProfileName: &profileName, + Username: &currUser.Username, + }); err != nil { + return fmt.Errorf("switch profile failed: %w", err) + } + + err = s.profileManager.SwitchProfile(profileName) + if err != nil { + return fmt.Errorf("switch profile: %w", err) + } + + return nil +} + +func (s *serviceClient) removeProfile(profileName string) error { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf(getClientFMT, err) + } + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %w", err) + } + + _, err = conn.RemoveProfile(context.Background(), &proto.RemoveProfileRequest{ + ProfileName: profileName, + Username: currUser.Username, + }) + if err != nil { + return fmt.Errorf("remove profile: %w", err) + } + + return nil +} + +type Profile struct { + Name string + IsActive bool +} + +func (s *serviceClient) getProfiles() ([]Profile, error) { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return nil, fmt.Errorf(getClientFMT, err) + } + + currUser, err := user.Current() + if err != nil { + return nil, fmt.Errorf("get current user: %w", err) + } + profilesResp, err := conn.ListProfiles(context.Background(), &proto.ListProfilesRequest{ + Username: currUser.Username, + }) + if err != nil { + return nil, fmt.Errorf("list profiles: %w", err) + } + + var profiles []Profile + + for _, profile := range profilesResp.Profiles { + profiles = append(profiles, Profile{ + Name: profile.Name, + IsActive: profile.IsActive, + }) + } + + return profiles, nil +} + +type subItem struct { + *systray.MenuItem + ctx context.Context + cancel context.CancelFunc +} + +type profileMenu struct { + mu sync.Mutex + ctx context.Context + profileManager *profilemanager.ProfileManager + eventHandler eventHandler + profileMenuItem *systray.MenuItem + emailMenuItem *systray.MenuItem + profileSubItems []*subItem + manageProfilesSubItem *subItem + profilesState []Profile + downClickCallback func() error + upClickCallback func() error + getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) + loadSettingsCallback func() +} + +func newProfileMenu(ctx context.Context, profileManager *profilemanager.ProfileManager, + + eventHandler eventHandler, profileMenuItem, emailMenuItem *systray.MenuItem, + downClickCallback, upClickCallback func() error, + getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error), + loadSettingsCallback func()) *profileMenu { + p := profileMenu{ + ctx: ctx, + profileManager: profileManager, + eventHandler: eventHandler, + profileMenuItem: profileMenuItem, + emailMenuItem: emailMenuItem, + downClickCallback: downClickCallback, + upClickCallback: upClickCallback, + getSrvClientCallback: getSrvClientCallback, + loadSettingsCallback: loadSettingsCallback, + } + + p.emailMenuItem.Disable() + p.emailMenuItem.Hide() + p.refresh() + go p.updateMenu() + + return &p +} + +func (p *profileMenu) getProfiles() ([]Profile, error) { + conn, err := p.getSrvClientCallback(defaultFailTimeout) + if err != nil { + return nil, fmt.Errorf(getClientFMT, err) + } + currUser, err := user.Current() + if err != nil { + return nil, fmt.Errorf("get current user: %w", err) + } + + profilesResp, err := conn.ListProfiles(p.ctx, &proto.ListProfilesRequest{ + Username: currUser.Username, + }) + if err != nil { + return nil, fmt.Errorf("list profiles: %w", err) + } + + var profiles []Profile + + for _, profile := range profilesResp.Profiles { + profiles = append(profiles, Profile{ + Name: profile.Name, + IsActive: profile.IsActive, + }) + } + + return profiles, nil +} + +func (p *profileMenu) refresh() { + p.mu.Lock() + defer p.mu.Unlock() + + profiles, err := p.getProfiles() + if err != nil { + log.Errorf("failed to list profiles: %v", err) + return + } + + // Clear existing profile items + p.clear(profiles) + + currUser, err := user.Current() + if err != nil { + log.Errorf("failed to get current user: %v", err) + return + } + + conn, err := p.getSrvClientCallback(defaultFailTimeout) + if err != nil { + log.Errorf("failed to get daemon client: %v", err) + return + } + + activeProf, err := conn.GetActiveProfile(p.ctx, &proto.GetActiveProfileRequest{}) + if err != nil { + log.Errorf("failed to get active profile: %v", err) + return + } + + if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username { + activeProfState, err := p.profileManager.GetProfileState(activeProf.ProfileName) + if err != nil { + log.Warnf("failed to get active profile state: %v", err) + p.emailMenuItem.Hide() + } else if activeProfState.Email != "" { + p.emailMenuItem.SetTitle(fmt.Sprintf("(%s)", activeProfState.Email)) + p.emailMenuItem.Show() + } + } + + for _, profile := range profiles { + item := p.profileMenuItem.AddSubMenuItem(profile.Name, "") + if profile.IsActive { + item.Check() + } + + ctx, cancel := context.WithCancel(context.Background()) + p.profileSubItems = append(p.profileSubItems, &subItem{item, ctx, cancel}) + + go func() { + for { + select { + case <-ctx.Done(): + return // context cancelled + case _, ok := <-item.ClickedCh: + if !ok { + return // channel closed + } + + // Handle profile selection + if profile.IsActive { + log.Infof("Profile '%s' is already active", profile.Name) + return + } + conn, err := p.getSrvClientCallback(defaultFailTimeout) + if err != nil { + log.Errorf("failed to get daemon client: %v", err) + return + } + + _, err = conn.SwitchProfile(ctx, &proto.SwitchProfileRequest{ + ProfileName: &profile.Name, + Username: &currUser.Username, + }) + if err != nil { + log.Errorf("failed to switch profile: %v", err) + return + } + + err = p.profileManager.SwitchProfile(profile.Name) + if err != nil { + log.Errorf("failed to switch profile '%s': %v", profile.Name, err) + return + } + + log.Infof("Switched to profile '%s'", profile.Name) + + status, err := conn.Status(ctx, &proto.StatusRequest{}) + if err != nil { + log.Errorf("failed to get status after switching profile: %v", err) + return + } + + if status.Status == string(internal.StatusConnected) { + if err := p.downClickCallback(); err != nil { + log.Errorf("failed to handle down click after switching profile: %v", err) + } + } + + if err := p.upClickCallback(); err != nil { + log.Errorf("failed to handle up click after switching profile: %v", err) + } + + p.refresh() + p.loadSettingsCallback() + } + } + }() + + } + ctx, cancel := context.WithCancel(context.Background()) + manageItem := p.profileMenuItem.AddSubMenuItem("Manage Profiles", "") + p.manageProfilesSubItem = &subItem{manageItem, ctx, cancel} + + go func() { + for { + select { + case <-ctx.Done(): + return // context cancelled + case _, ok := <-manageItem.ClickedCh: + if !ok { + return // channel closed + } + // Handle manage profiles click + p.eventHandler.runSelfCommand(p.ctx, "profiles", "true") + p.refresh() + p.loadSettingsCallback() + } + } + }() + + if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username { + p.profileMenuItem.SetTitle(activeProf.ProfileName) + } else { + p.profileMenuItem.SetTitle(fmt.Sprintf("Profile: %s (User: %s)", activeProf.ProfileName, activeProf.Username)) + p.emailMenuItem.Hide() + } + +} + +func (p *profileMenu) clear(profiles []Profile) { + // Clear existing profile items + for _, item := range p.profileSubItems { + item.Remove() + item.cancel() + } + p.profileSubItems = make([]*subItem, 0, len(profiles)) + p.profilesState = profiles + + if p.manageProfilesSubItem != nil { + // Remove the manage profiles item if it exists + p.manageProfilesSubItem.Remove() + p.manageProfilesSubItem.cancel() + p.manageProfilesSubItem = nil + } +} + +func (p *profileMenu) updateMenu() { + // check every second + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + + // get profilesList + profiles, err := p.getProfiles() + if err != nil { + log.Errorf("failed to list profiles: %v", err) + continue + } + + sort.Slice(profiles, func(i, j int) bool { + return profiles[i].Name < profiles[j].Name + }) + + p.mu.Lock() + state := p.profilesState + p.mu.Unlock() + + sort.Slice(state, func(i, j int) bool { + return state[i].Name < state[j].Name + }) + + if slices.Equal(profiles, state) { + continue + } + + p.refresh() + case <-p.ctx.Done(): + return // context cancelled + + } + } +} diff --git a/util/file.go b/util/file.go index f7de7ede2..73ad05b18 100644 --- a/util/file.go +++ b/util/file.go @@ -9,6 +9,7 @@ import ( "io" "os" "path/filepath" + "sort" "strings" "text/template" @@ -200,6 +201,36 @@ func ReadJson(file string, res interface{}) (interface{}, error) { return res, nil } +// RemoveJson removes the specified JSON file if it exists +func RemoveJson(file string) error { + // Check if the file exists + if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { + return nil // File does not exist, nothing to remove + } + + // Attempt to remove the file + if err := os.Remove(file); err != nil { + return fmt.Errorf("failed to remove JSON file %s: %w", file, err) + } + + return nil +} + +// ListFiles returns the full paths of all files in dir that match pattern. +// Pattern uses shell-style globbing (e.g. "*.json"). +func ListFiles(dir, pattern string) ([]string, error) { + // glob pattern like "/path/to/dir/*.json" + globPattern := filepath.Join(dir, pattern) + + matches, err := filepath.Glob(globPattern) + if err != nil { + return nil, err + } + + sort.Strings(matches) + return matches, nil +} + // ReadJsonWithEnvSub reads JSON config file and maps to a provided interface with environment variable substitution func ReadJsonWithEnvSub(file string, res interface{}) (interface{}, error) { envVars := getEnvMap() From 3d9be5098ba392389ecb9210bee0bdb42d01dd65 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Fri, 25 Jul 2025 18:43:48 +0300 Subject: [PATCH 179/213] [client]: deprecate config flag (#4224) --- client/cmd/login.go | 15 +++++---------- client/cmd/root.go | 1 + client/cmd/service_installer.go | 2 -- client/cmd/up.go | 14 ++++---------- release_files/systemd/netbird@.service | 2 +- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/client/cmd/login.go b/client/cmd/login.go index 482e004d1..d6381f6e2 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -26,7 +26,7 @@ import ( func init() { loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) - loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Netbird config file location") + loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location") } var loginCmd = &cobra.Command{ @@ -228,15 +228,10 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, // update host's static platform and system information system.UpdateStaticInfo() - var configFilePath string - if configPath != "" { - configFilePath = configPath - } else { - var err error - configFilePath, err = activeProf.FilePath() - if err != nil { - return fmt.Errorf("get active profile file path: %v", err) - } + configFilePath, err := activeProf.FilePath() + if err != nil { + return fmt.Errorf("get active profile file path: %v", err) + } config, err := profilemanager.ReadConfig(configFilePath) diff --git a/client/cmd/root.go b/client/cmd/root.go index b22b850ee..8e8ee3280 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -126,6 +126,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.") rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device") rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output") + rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "(DEPRECATED) Netbird config file location") rootCmd.AddCommand(upCmd) rootCmd.AddCommand(downCmd) diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index c994801a6..be8a897dc 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -31,8 +31,6 @@ func buildServiceArguments() []string { args := []string{ "service", "run", - "--config", - configPath, "--log-level", logLevel, "--daemon-addr", diff --git a/client/cmd/up.go b/client/cmd/up.go index d1f8e67a1..98e1c02b3 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -78,7 +78,7 @@ func init() { upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) - upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Netbird config file location") + upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location") } @@ -155,15 +155,9 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr return err } - var configFilePath string - if configPath != "" { - configFilePath = configPath - } else { - var err error - configFilePath, err = activeProf.FilePath() - if err != nil { - return fmt.Errorf("get active profile file path: %v", err) - } + configFilePath, err := activeProf.FilePath() + if err != nil { + return fmt.Errorf("get active profile file path: %v", err) } ic, err := setupConfig(customDNSAddressConverted, cmd, configFilePath) diff --git a/release_files/systemd/netbird@.service b/release_files/systemd/netbird@.service index 095c3142d..48e8cc29d 100644 --- a/release_files/systemd/netbird@.service +++ b/release_files/systemd/netbird@.service @@ -7,7 +7,7 @@ Wants=network-online.target [Service] Type=simple EnvironmentFile=-/etc/default/netbird -ExecStart=/usr/bin/netbird service run --log-file /var/log/netbird/client-%i.log --config /etc/netbird/%i.json --daemon-addr unix:///var/run/netbird/%i.sock $FLAGS +ExecStart=/usr/bin/netbird service run --log-file /var/log/netbird/client-%i.log --daemon-addr unix:///var/run/netbird/%i.sock $FLAGS Restart=on-failure RestartSec=5 TimeoutStopSec=10 From d89e6151a477fb71bc8dc61294d2eb5c720f9140 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 25 Jul 2025 22:52:48 +0200 Subject: [PATCH 180/213] [client] Fix pre-shared key state in wg show (#4222) --- client/iface/configurer/usp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 1ff4d839c..171458e38 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -530,7 +530,7 @@ func parseStatus(deviceName, ipcStr string) (*Stats, error) { if currentPeer == nil { continue } - if val != "" { + if val != "" && val != "0000000000000000000000000000000000000000000000000000000000000000" { currentPeer.PresharedKey = true } } From e1c66a8124b40aa5963c3e3c3169d424df548404 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:36:48 +0300 Subject: [PATCH 181/213] [client] Fix profile directory path handling based on NB_STATE_DIR (#4229) [client] Fix profile directory path handling based on NB_STATE_DIR (#4229) --- client/internal/profilemanager/service.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/client/internal/profilemanager/service.go b/client/internal/profilemanager/service.go index 56198c4cc..520eef2e9 100644 --- a/client/internal/profilemanager/service.go +++ b/client/internal/profilemanager/service.go @@ -34,14 +34,18 @@ func init() { DefaultConfigPathDir = "/var/lib/netbird/" oldDefaultConfigPathDir = "/etc/netbird/" - switch runtime.GOOS { - case "windows": - oldDefaultConfigPathDir = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird") - DefaultConfigPathDir = oldDefaultConfigPathDir + if stateDir := os.Getenv("NB_STATE_DIR"); stateDir != "" { + DefaultConfigPathDir = stateDir + } else { + switch runtime.GOOS { + case "windows": + oldDefaultConfigPathDir = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird") + DefaultConfigPathDir = oldDefaultConfigPathDir - case "freebsd": - oldDefaultConfigPathDir = "/var/db/netbird/" - DefaultConfigPathDir = oldDefaultConfigPathDir + case "freebsd": + oldDefaultConfigPathDir = "/var/db/netbird/" + DefaultConfigPathDir = oldDefaultConfigPathDir + } } oldDefaultConfigPath = filepath.Join(oldDefaultConfigPathDir, "config.json") From 8c8473aed38631c63ddafd85f39c5fb99c42cb4f Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:03:15 +0300 Subject: [PATCH 182/213] [client] Add support for disabling profiles feature via command line flag (#4235) * Add support for disabling profiles feature via command line flag * Add profiles disabling flag to service command * Refactor profile menu initialization and enhance error notifications in event handlers --- client/cmd/root.go | 1 + client/cmd/service.go | 1 + client/cmd/service_controller.go | 2 +- client/cmd/testutil_test.go | 2 +- client/server/server.go | 34 ++++++++++++++++++++++-- client/server/server_test.go | 6 ++--- client/ui/client_ui.go | 16 +++++++++++- client/ui/event_handler.go | 45 +++++++++++++++++++++++++------- client/ui/profile.go | 42 ++++++++++++++++++----------- 9 files changed, 117 insertions(+), 32 deletions(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 8e8ee3280..e3ce79964 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -72,6 +72,7 @@ var ( anonymizeFlag bool dnsRouteInterval time.Duration lazyConnEnabled bool + profilesDisabled bool rootCmd = &cobra.Command{ Use: "netbird", diff --git a/client/cmd/service.go b/client/cmd/service.go index 178f4bf0e..d8745f1c4 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -42,6 +42,7 @@ func init() { } serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd) + serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile.") rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") serviceEnvDesc := `Sets extra environment variables for the service. ` + diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index cbffff797..6dc6bca9b 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles)) + serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), profilesDisabled) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index cf94754c1..5dbc8cd7f 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -134,7 +134,7 @@ func startClientDaemon( s := grpc.NewServer() server := client.New(ctx, - "") + "", false) if err := server.Start(); err != nil { t.Fatal(err) } diff --git a/client/server/server.go b/client/server/server.go index f3414888d..80cd6078f 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -44,6 +44,7 @@ const ( defaultRetryMultiplier = 1.7 errRestoreResidualState = "failed to restore residual state: %v" + errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled" ) // Server for service control. @@ -68,7 +69,8 @@ type Server struct { persistNetworkMap bool isSessionActive atomic.Bool - profileManager profilemanager.ServiceManager + profileManager profilemanager.ServiceManager + profilesDisabled bool } type oauthAuthFlow struct { @@ -79,13 +81,14 @@ type oauthAuthFlow struct { } // New server instance constructor. -func New(ctx context.Context, logFile string) *Server { +func New(ctx context.Context, logFile string, profilesDisabled bool) *Server { return &Server{ rootCtx: ctx, logFile: logFile, persistNetworkMap: true, statusRecorder: peer.NewRecorder(""), profileManager: profilemanager.ServiceManager{}, + profilesDisabled: profilesDisabled, } } @@ -320,6 +323,10 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques s.mutex.Lock() defer s.mutex.Unlock() + if s.checkProfilesDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + profState := profilemanager.ActiveProfileState{ Name: msg.ProfileName, Username: msg.Username, @@ -737,6 +744,11 @@ func (s *Server) switchProfileIfNeeded(profileName string, userName *string, act } if profileName != activeProf.Name || username != activeProf.Username { + if s.checkProfilesDisabled() { + log.Errorf("profiles are disabled, you cannot use this feature without profiles enabled") + return gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + log.Infof("switching to profile %s for user %s", profileName, username) if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ Name: profileName, @@ -1069,6 +1081,10 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) ( s.mutex.Lock() defer s.mutex.Unlock() + if s.checkProfilesDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + if msg.ProfileName == "" || msg.Username == "" { return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided") } @@ -1086,6 +1102,10 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ s.mutex.Lock() defer s.mutex.Unlock() + if s.checkProfilesDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + if msg.ProfileName == "" { return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") } @@ -1142,3 +1162,13 @@ func (s *Server) GetActiveProfile(ctx context.Context, msg *proto.GetActiveProfi Username: activeProfile.Username, }, nil } + +func (s *Server) checkProfilesDisabled() bool { + // Check if the environment variable is set to disable profiles + if s.profilesDisabled { + log.Warn("Profiles are disabled via NB_DISABLE_PROFILES environment variable") + return true + } + + return false +} diff --git a/client/server/server_test.go b/client/server/server_test.go index dda610076..afd38b4a4 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -94,7 +94,7 @@ func TestConnectWithRetryRuns(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "debug") + s := New(ctx, "debug", false) s.config = config @@ -151,7 +151,7 @@ func TestServer_Up(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console") + s := New(ctx, "console", false) err = s.Start() require.NoError(t, err) @@ -227,7 +227,7 @@ func TestServer_SubcribeEvents(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console") + s := New(ctx, "console", false) err = s.Start() require.NoError(t, err) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 781ec56f8..c74412c8b 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -802,7 +802,21 @@ func (s *serviceClient) onTrayReady() { profileMenuItem := systray.AddMenuItem("", "") emailMenuItem := systray.AddMenuItem("", "") - s.mProfile = newProfileMenu(s.ctx, s.profileManager, *s.eventHandler, profileMenuItem, emailMenuItem, s.menuDownClick, s.menuUpClick, s.getSrvClient, s.loadSettings) + + newProfileMenuArgs := &newProfileMenuArgs{ + ctx: s.ctx, + profileManager: s.profileManager, + eventHandler: s.eventHandler, + profileMenuItem: profileMenuItem, + emailMenuItem: emailMenuItem, + downClickCallback: s.menuDownClick, + upClickCallback: s.menuUpClick, + getSrvClientCallback: s.getSrvClient, + loadSettingsCallback: s.loadSettings, + app: s.app, + } + + s.mProfile = newProfileMenu(*newProfileMenuArgs) systray.AddSeparator() s.mUp = systray.AddMenuItem("Connect", "Connect") diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go index 39ea3867c..c0bc74a2c 100644 --- a/client/ui/event_handler.go +++ b/client/ui/event_handler.go @@ -86,35 +86,60 @@ func (h *eventHandler) handleDisconnectClick() { func (h *eventHandler) handleAllowSSHClick() { h.toggleCheckbox(h.client.mAllowSSH) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update SSH settings")) + } + } func (h *eventHandler) handleAutoConnectClick() { h.toggleCheckbox(h.client.mAutoConnect) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update auto-connect settings")) + } } func (h *eventHandler) handleRosenpassClick() { h.toggleCheckbox(h.client.mEnableRosenpass) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update Rosenpass settings")) + } } func (h *eventHandler) handleLazyConnectionClick() { h.toggleCheckbox(h.client.mLazyConnEnabled) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update lazy connection settings")) + } } func (h *eventHandler) handleBlockInboundClick() { h.toggleCheckbox(h.client.mBlockInbound) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update block inbound settings")) + } } func (h *eventHandler) handleNotificationsClick() { h.toggleCheckbox(h.client.mNotifications) - if h.client.eventManager != nil { + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update notifications settings")) + } else if h.client.eventManager != nil { h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked()) } - h.updateConfigWithErr() + } func (h *eventHandler) handleAdvancedSettingsClick() { @@ -166,10 +191,12 @@ func (h *eventHandler) toggleCheckbox(item *systray.MenuItem) { } } -func (h *eventHandler) updateConfigWithErr() { +func (h *eventHandler) updateConfigWithErr() error { if err := h.client.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) + return err } + + return nil } func (h *eventHandler) runSelfCommand(ctx context.Context, command, arg string) { diff --git a/client/ui/profile.go b/client/ui/profile.go index 142582c25..779f60aa4 100644 --- a/client/ui/profile.go +++ b/client/ui/profile.go @@ -334,7 +334,7 @@ type profileMenu struct { mu sync.Mutex ctx context.Context profileManager *profilemanager.ProfileManager - eventHandler eventHandler + eventHandler *eventHandler profileMenuItem *systray.MenuItem emailMenuItem *systray.MenuItem profileSubItems []*subItem @@ -344,24 +344,34 @@ type profileMenu struct { upClickCallback func() error getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) loadSettingsCallback func() + app fyne.App } -func newProfileMenu(ctx context.Context, profileManager *profilemanager.ProfileManager, +type newProfileMenuArgs struct { + ctx context.Context + profileManager *profilemanager.ProfileManager + eventHandler *eventHandler + profileMenuItem *systray.MenuItem + emailMenuItem *systray.MenuItem + downClickCallback func() error + upClickCallback func() error + getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) + loadSettingsCallback func() + app fyne.App +} - eventHandler eventHandler, profileMenuItem, emailMenuItem *systray.MenuItem, - downClickCallback, upClickCallback func() error, - getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error), - loadSettingsCallback func()) *profileMenu { +func newProfileMenu(args newProfileMenuArgs) *profileMenu { p := profileMenu{ - ctx: ctx, - profileManager: profileManager, - eventHandler: eventHandler, - profileMenuItem: profileMenuItem, - emailMenuItem: emailMenuItem, - downClickCallback: downClickCallback, - upClickCallback: upClickCallback, - getSrvClientCallback: getSrvClientCallback, - loadSettingsCallback: loadSettingsCallback, + ctx: args.ctx, + profileManager: args.profileManager, + eventHandler: args.eventHandler, + profileMenuItem: args.profileMenuItem, + emailMenuItem: args.emailMenuItem, + downClickCallback: args.downClickCallback, + upClickCallback: args.upClickCallback, + getSrvClientCallback: args.getSrvClientCallback, + loadSettingsCallback: args.loadSettingsCallback, + app: args.app, } p.emailMenuItem.Disable() @@ -479,6 +489,8 @@ func (p *profileMenu) refresh() { }) if err != nil { log.Errorf("failed to switch profile: %v", err) + // show notification dialog + p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile")) return } From 980a6eca8e5eaf35c216ac9b0840833ea6866eb0 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:37:18 +0200 Subject: [PATCH 183/213] [client] Disable the dns host manager properly if disabled through management (#4241) --- client/internal/dns/handler_chain.go | 2 +- client/internal/dns/server.go | 107 +++++++++++++++++++-------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 36da8fb78..439bcbb3c 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -15,7 +15,7 @@ const ( PriorityDNSRoute = 75 PriorityUpstream = 50 PriorityDefault = 1 - PriorityFallback = -100 + PriorityFallback = -100 ) type SubdomainMatcher interface { diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index f933c1de0..4ab9ef761 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -2,6 +2,7 @@ package dns import ( "context" + "errors" "fmt" "net/netip" "runtime" @@ -59,8 +60,10 @@ type hostManagerWithOriginalNS interface { // DefaultServer dns server object type DefaultServer struct { - ctx context.Context - ctxCancel context.CancelFunc + ctx context.Context + ctxCancel context.CancelFunc + // disableSys disables system DNS management (e.g., /etc/resolv.conf updates) while keeping the DNS service running. + // This is different from ServiceEnable=false from management which completely disables the DNS service. disableSys bool mux sync.Mutex service service @@ -187,6 +190,7 @@ func newDefaultServer( statusRecorder: statusRecorder, stateManager: stateManager, hostsDNSHolder: newHostsDNSHolder(), + hostManager: &noopHostConfigurator{}, } // register with root zone, handler chain takes care of the routing @@ -258,7 +262,8 @@ func (s *DefaultServer) Initialize() (err error) { s.mux.Lock() defer s.mux.Unlock() - if s.hostManager != nil { + if !s.isUsingNoopHostManager() { + // already initialized return nil } @@ -271,19 +276,19 @@ func (s *DefaultServer) Initialize() (err error) { s.stateManager.RegisterState(&ShutdownState{}) - // use noop host manager if requested or running in netstack mode. + // Keep using noop host manager if dns off requested or running in netstack mode. // Netstack mode currently doesn't have a way to receive DNS requests. // TODO: Use listener on localhost in netstack mode when running as root. if s.disableSys || netstack.IsEnabled() { log.Info("system DNS is disabled, not setting up host manager") - s.hostManager = &noopHostConfigurator{} return nil } - s.hostManager, err = s.initialize() + hostManager, err := s.initialize() if err != nil { return fmt.Errorf("initialize: %w", err) } + s.hostManager = hostManager return nil } @@ -297,28 +302,42 @@ func (s *DefaultServer) DnsIP() netip.Addr { // Stop stops the server func (s *DefaultServer) Stop() { - s.mux.Lock() - defer s.mux.Unlock() s.ctxCancel() - if s.hostManager != nil { - if srvs, ok := s.hostManager.(hostManagerWithOriginalNS); ok && len(srvs.getOriginalNameservers()) > 0 { - log.Debugf("deregistering original nameservers as fallback handlers") - s.deregisterHandler([]string{nbdns.RootZone}, PriorityFallback) - } + s.mux.Lock() + defer s.mux.Unlock() - if err := s.hostManager.restoreHostDNS(); err != nil { - log.Error("failed to restore host DNS settings: ", err) - } else if err := s.stateManager.DeleteState(&ShutdownState{}); err != nil { - log.Errorf("failed to delete shutdown dns state: %v", err) - } + if err := s.disableDNS(); err != nil { + log.Errorf("failed to disable DNS: %v", err) } - s.service.Stop() - maps.Clear(s.extraDomains) } +func (s *DefaultServer) disableDNS() error { + defer s.service.Stop() + + if s.isUsingNoopHostManager() { + return nil + } + + // Deregister original nameservers if they were registered as fallback + if srvs, ok := s.hostManager.(hostManagerWithOriginalNS); ok && len(srvs.getOriginalNameservers()) > 0 { + log.Debugf("deregistering original nameservers as fallback handlers") + s.deregisterHandler([]string{nbdns.RootZone}, PriorityFallback) + } + + if err := s.hostManager.restoreHostDNS(); err != nil { + log.Errorf("failed to restore host DNS settings: %v", err) + } else if err := s.stateManager.DeleteState(&ShutdownState{}); err != nil { + log.Errorf("failed to delete shutdown dns state: %v", err) + } + + s.hostManager = &noopHostConfigurator{} + + return nil +} + // OnUpdatedHostDNSServer update the DNS servers addresses for root zones // It will be applied if the mgm server do not enforce DNS settings for root zone func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { @@ -357,10 +376,6 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro s.mux.Lock() defer s.mux.Unlock() - if s.hostManager == nil { - return fmt.Errorf("dns service is not initialized yet") - } - hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{ ZeroNil: true, IgnoreZeroValue: true, @@ -418,13 +433,14 @@ func (s *DefaultServer) ProbeAvailability() { func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { // is the service should be Disabled, we stop the listener or fake resolver - // and proceed with a regular update to clean up the handlers and records if update.ServiceEnable { - if err := s.service.Listen(); err != nil { - log.Errorf("failed to start DNS service: %v", err) + if err := s.enableDNS(); err != nil { + log.Errorf("failed to enable DNS: %v", err) } } else if !s.permanent { - s.service.Stop() + if err := s.disableDNS(); err != nil { + log.Errorf("failed to disable DNS: %v", err) + } } localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones) @@ -469,11 +485,40 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { return nil } -func (s *DefaultServer) applyHostConfig() { - if s.hostManager == nil { - return +func (s *DefaultServer) isUsingNoopHostManager() bool { + _, isNoop := s.hostManager.(*noopHostConfigurator) + return isNoop +} + +func (s *DefaultServer) enableDNS() error { + if err := s.service.Listen(); err != nil { + return fmt.Errorf("start DNS service: %w", err) } + if !s.isUsingNoopHostManager() { + return nil + } + + if s.disableSys || netstack.IsEnabled() { + return nil + } + + log.Info("DNS service re-enabled, initializing host manager") + + if !s.service.RuntimeIP().IsValid() { + return errors.New("DNS service runtime IP is invalid") + } + + hostManager, err := s.initialize() + if err != nil { + return fmt.Errorf("initialize host manager: %w", err) + } + s.hostManager = hostManager + + return nil +} + +func (s *DefaultServer) applyHostConfig() { // prevent reapplying config if we're shutting down if s.ctx.Err() != nil { return From a72ef1af39f49d508b01aab90ab46cd18aa9c27e Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:38:44 +0300 Subject: [PATCH 184/213] [client] Fix error handling for set config request on CLI (#4237) [client] Fix error handling for set config request on CLI (#4237) --- client/cmd/up.go | 7 ++++++- client/server/server.go | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/cmd/up.go b/client/cmd/up.go index 98e1c02b3..a0c26a207 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -13,6 +13,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" @@ -242,7 +243,11 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager // set the new config req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.Name, username.Username) if _, err := client.SetConfig(ctx, req); err != nil { - return fmt.Errorf("call service set config method: %v", err) + if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable { + log.Warnf("setConfig method is not available in the daemon") + } else { + return fmt.Errorf("call service setConfig method: %v", err) + } } if err := doDaemonUp(ctx, cmd, client, pm, activeProf, customDNSAddressConverted, username.Username); err != nil { diff --git a/client/server/server.go b/client/server/server.go index 80cd6078f..3cb173881 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -452,6 +452,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro } if *msg.ProfileName != activeProf.Name && username != activeProf.Username { + if s.checkProfilesDisabled() { + log.Errorf("profiles are disabled, you cannot use this feature without profiles enabled") + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + log.Infof("switching to profile %s for user '%s'", *msg.ProfileName, username) if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ Name: *msg.ProfileName, From 34042b8171b6a8432b9a08c39cccd9fa692729ae Mon Sep 17 00:00:00 2001 From: Bilgeworth <47156222+Bilgeworth@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:52:18 -0400 Subject: [PATCH 185/213] [misc] devcontainer Dockerfile: pin gopls to v0.18.1 (latest that supports golang 1.23) (#4240) Container will fail to build with newer versions of gopls unless golang is updated to 1.24. The latest stable version supporting 1.23 is gopls v0.18.1 --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 4697acf20..9e5e97a31 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ libayatana-appindicator3-dev=0.5.5-2+deb11u2 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ - && go install -v golang.org/x/tools/gopls@latest + && go install -v golang.org/x/tools/gopls@v0.18.1 WORKDIR /app From 541e258639650254359aa0c4ce83b60877a681f1 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:49:50 +0200 Subject: [PATCH 186/213] [management] add account deleted event (#4255) --- management/server/account.go | 3 +++ management/server/activity/codes.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/management/server/account.go b/management/server/account.go index cd0c933f0..52b625da1 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -718,6 +718,9 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u // cancel peer login expiry job am.peerLoginExpiry.Cancel(ctx, []string{account.Id}) + meta := map[string]any{"account_id": account.Id, "domain": account.Domain, "created_at": account.CreatedAt} + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDeleted, meta) + log.WithContext(ctx).Debugf("account %s deleted", accountID) return nil } diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index d9f56f097..23ddd1dd5 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -174,6 +174,8 @@ const ( AccountLazyConnectionEnabled Activity = 85 AccountLazyConnectionDisabled Activity = 86 + + AccountDeleted Activity = 99999 ) var activityMap = map[Activity]Code{ @@ -182,6 +184,7 @@ var activityMap = map[Activity]Code{ UserJoined: {"User joined", "user.join"}, UserInvited: {"User invited", "user.invite"}, AccountCreated: {"Account created", "account.create"}, + AccountDeleted: {"Account deleted", "account.delete"}, PeerRemovedByUser: {"Peer deleted", "user.peer.delete"}, RuleAdded: {"Rule added", "rule.add"}, RuleUpdated: {"Rule updated", "rule.update"}, From 5de61f3081bc0d8178b8909f89d28069dd94d4f8 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:28:19 +0200 Subject: [PATCH 187/213] [client] Fix dns ipv6 upstream (#4257) --- client/internal/dns/server.go | 15 ++++++--- client/internal/dns/server_test.go | 53 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 4ab9ef761..e5f29d807 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -586,10 +586,7 @@ func (s *DefaultServer) registerFallback(config HostDNSConfig) { continue } - ns = fmt.Sprintf("%s:%d", ns, defaultPort) - if ip, err := netip.ParseAddr(ns); err == nil && ip.Is6() { - ns = fmt.Sprintf("[%s]:%d", ns, defaultPort) - } + ns = formatAddr(ns, defaultPort) handler.upstreamServers = append(handler.upstreamServers, ns) } @@ -774,7 +771,15 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) { } func getNSHostPort(ns nbdns.NameServer) string { - return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port) + return formatAddr(ns.IP.String(), ns.Port) +} + +// formatAddr formats a nameserver address with port, handling IPv6 addresses properly +func formatAddr(address string, port int) string { + if ip, err := netip.ParseAddr(address); err == nil && ip.Is6() { + return fmt.Sprintf("[%s]:%d", address, port) + } + return fmt.Sprintf("%s:%d", address, port) } // upstreamCallbacks returns two functions, the first one is used to deactivate diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 3cab4517a..50444a86f 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -2053,3 +2053,56 @@ func TestLocalResolverPriorityConstants(t *testing.T) { assert.Equal(t, PriorityLocal, localMuxUpdates[0].priority, "Local handler should use PriorityLocal") assert.Equal(t, "local.example.com", localMuxUpdates[0].domain) } + +func TestFormatAddr(t *testing.T) { + tests := []struct { + name string + address string + port int + expected string + }{ + { + name: "IPv4 address", + address: "8.8.8.8", + port: 53, + expected: "8.8.8.8:53", + }, + { + name: "IPv4 address with custom port", + address: "1.1.1.1", + port: 5353, + expected: "1.1.1.1:5353", + }, + { + name: "IPv6 address", + address: "fd78:94bf:7df8::1", + port: 53, + expected: "[fd78:94bf:7df8::1]:53", + }, + { + name: "IPv6 address with custom port", + address: "2001:db8::1", + port: 5353, + expected: "[2001:db8::1]:5353", + }, + { + name: "IPv6 localhost", + address: "::1", + port: 53, + expected: "[::1]:53", + }, + { + name: "Invalid address treated as hostname", + address: "dns.example.com", + port: 53, + expected: "dns.example.com:53", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatAddr(tt.address, tt.port) + assert.Equal(t, tt.expected, result) + }) + } +} From 71bb09d870e29f33ab56dc6cff9ccecc36f8f186 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:36:30 +0200 Subject: [PATCH 188/213] [client] Improve userspace filter logging performance (#4221) --- client/firewall/uspfilter/conntrack/icmp.go | 6 +- client/firewall/uspfilter/conntrack/tcp.go | 14 +- client/firewall/uspfilter/conntrack/udp.go | 4 +- client/firewall/uspfilter/filter.go | 20 +- .../firewall/uspfilter/forwarder/endpoint.go | 2 +- client/firewall/uspfilter/forwarder/icmp.go | 16 +- client/firewall/uspfilter/forwarder/tcp.go | 18 +- client/firewall/uspfilter/forwarder/udp.go | 34 +-- client/firewall/uspfilter/log/log.go | 205 +++++++++++++++--- client/firewall/uspfilter/log/log_test.go | 15 +- client/firewall/uspfilter/nat.go | 8 +- 11 files changed, 238 insertions(+), 104 deletions(-) diff --git a/client/firewall/uspfilter/conntrack/icmp.go b/client/firewall/uspfilter/conntrack/icmp.go index 509c1549b..50b663642 100644 --- a/client/firewall/uspfilter/conntrack/icmp.go +++ b/client/firewall/uspfilter/conntrack/icmp.go @@ -221,7 +221,7 @@ func (t *ICMPTracker) track( // non echo requests don't need tracking if typ != uint8(layers.ICMPv4TypeEchoRequest) { - t.logger.Trace("New %s ICMP connection %s - %s", direction, key, icmpInfo) + t.logger.Trace3("New %s ICMP connection %s - %s", direction, key, icmpInfo) t.sendStartEvent(direction, srcIP, dstIP, typ, code, ruleId, size) return } @@ -243,7 +243,7 @@ func (t *ICMPTracker) track( t.connections[key] = conn t.mutex.Unlock() - t.logger.Trace("New %s ICMP connection %s - %s", direction, key, icmpInfo) + t.logger.Trace3("New %s ICMP connection %s - %s", direction, key, icmpInfo) t.sendEvent(nftypes.TypeStart, conn, ruleId) } @@ -294,7 +294,7 @@ func (t *ICMPTracker) cleanup() { if conn.timeoutExceeded(t.timeout) { delete(t.connections, key) - t.logger.Trace("Removed ICMP connection %s (timeout) [in: %d Pkts/%d B out: %d Pkts/%d B]", + t.logger.Trace5("Removed ICMP connection %s (timeout) [in: %d Pkts/%d B out: %d Pkts/%d B]", key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) t.sendEvent(nftypes.TypeEnd, conn, nil) } diff --git a/client/firewall/uspfilter/conntrack/tcp.go b/client/firewall/uspfilter/conntrack/tcp.go index 2d42ea32e..a2355e5c7 100644 --- a/client/firewall/uspfilter/conntrack/tcp.go +++ b/client/firewall/uspfilter/conntrack/tcp.go @@ -211,7 +211,7 @@ func (t *TCPTracker) track(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, fla conn.tombstone.Store(false) conn.state.Store(int32(TCPStateNew)) - t.logger.Trace("New %s TCP connection: %s", direction, key) + t.logger.Trace2("New %s TCP connection: %s", direction, key) t.updateState(key, conn, flags, direction, size) t.mutex.Lock() @@ -240,7 +240,7 @@ func (t *TCPTracker) IsValidInbound(srcIP, dstIP netip.Addr, srcPort, dstPort ui currentState := conn.GetState() if !t.isValidStateForFlags(currentState, flags) { - t.logger.Warn("TCP state %s is not valid with flags %x for connection %s", currentState, flags, key) + t.logger.Warn3("TCP state %s is not valid with flags %x for connection %s", currentState, flags, key) // allow all flags for established for now if currentState == TCPStateEstablished { return true @@ -262,7 +262,7 @@ func (t *TCPTracker) updateState(key ConnKey, conn *TCPConnTrack, flags uint8, p if flags&TCPRst != 0 { if conn.CompareAndSwapState(currentState, TCPStateClosed) { conn.SetTombstone() - t.logger.Trace("TCP connection reset: %s (dir: %s) [in: %d Pkts/%d B, out: %d Pkts/%d B]", + t.logger.Trace6("TCP connection reset: %s (dir: %s) [in: %d Pkts/%d B, out: %d Pkts/%d B]", key, packetDir, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) t.sendEvent(nftypes.TypeEnd, conn, nil) } @@ -340,17 +340,17 @@ func (t *TCPTracker) updateState(key ConnKey, conn *TCPConnTrack, flags uint8, p } if newState != 0 && conn.CompareAndSwapState(currentState, newState) { - t.logger.Trace("TCP connection %s transitioned from %s to %s (dir: %s)", key, currentState, newState, packetDir) + t.logger.Trace4("TCP connection %s transitioned from %s to %s (dir: %s)", key, currentState, newState, packetDir) switch newState { case TCPStateTimeWait: - t.logger.Trace("TCP connection %s completed [in: %d Pkts/%d B, out: %d Pkts/%d B]", + t.logger.Trace5("TCP connection %s completed [in: %d Pkts/%d B, out: %d Pkts/%d B]", key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) t.sendEvent(nftypes.TypeEnd, conn, nil) case TCPStateClosed: conn.SetTombstone() - t.logger.Trace("TCP connection %s closed gracefully [in: %d Pkts/%d, B out: %d Pkts/%d B]", + t.logger.Trace5("TCP connection %s closed gracefully [in: %d Pkts/%d, B out: %d Pkts/%d B]", key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) t.sendEvent(nftypes.TypeEnd, conn, nil) } @@ -438,7 +438,7 @@ func (t *TCPTracker) cleanup() { if conn.timeoutExceeded(timeout) { delete(t.connections, key) - t.logger.Trace("Cleaned up timed-out TCP connection %s (%s) [in: %d Pkts/%d, B out: %d Pkts/%d B]", + t.logger.Trace6("Cleaned up timed-out TCP connection %s (%s) [in: %d Pkts/%d, B out: %d Pkts/%d B]", key, conn.GetState(), conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) // event already handled by state change diff --git a/client/firewall/uspfilter/conntrack/udp.go b/client/firewall/uspfilter/conntrack/udp.go index 000eaa1b6..e7f49c46f 100644 --- a/client/firewall/uspfilter/conntrack/udp.go +++ b/client/firewall/uspfilter/conntrack/udp.go @@ -116,7 +116,7 @@ func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, d t.connections[key] = conn t.mutex.Unlock() - t.logger.Trace("New %s UDP connection: %s", direction, key) + t.logger.Trace2("New %s UDP connection: %s", direction, key) t.sendEvent(nftypes.TypeStart, conn, ruleID) } @@ -165,7 +165,7 @@ func (t *UDPTracker) cleanup() { if conn.timeoutExceeded(t.timeout) { delete(t.connections, key) - t.logger.Trace("Removed UDP connection %s (timeout) [in: %d Pkts/%d B, out: %d Pkts/%d B]", + t.logger.Trace5("Removed UDP connection %s (timeout) [in: %d Pkts/%d B, out: %d Pkts/%d B]", key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) t.sendEvent(nftypes.TypeEnd, conn, nil) } diff --git a/client/firewall/uspfilter/filter.go b/client/firewall/uspfilter/filter.go index 7120d7d64..fdc026b88 100644 --- a/client/firewall/uspfilter/filter.go +++ b/client/firewall/uspfilter/filter.go @@ -601,7 +601,7 @@ func (m *Manager) filterOutbound(packetData []byte, size int) bool { srcIP, dstIP := m.extractIPs(d) if !srcIP.IsValid() { - m.logger.Error("Unknown network layer: %v", d.decoded[0]) + m.logger.Error1("Unknown network layer: %v", d.decoded[0]) return false } @@ -727,13 +727,13 @@ func (m *Manager) filterInbound(packetData []byte, size int) bool { srcIP, dstIP := m.extractIPs(d) if !srcIP.IsValid() { - m.logger.Error("Unknown network layer: %v", d.decoded[0]) + m.logger.Error1("Unknown network layer: %v", d.decoded[0]) return true } // TODO: pass fragments of routed packets to forwarder if fragment { - m.logger.Trace("packet is a fragment: src=%v dst=%v id=%v flags=%v", + m.logger.Trace4("packet is a fragment: src=%v dst=%v id=%v flags=%v", srcIP, dstIP, d.ip4.Id, d.ip4.Flags) return false } @@ -741,7 +741,7 @@ func (m *Manager) filterInbound(packetData []byte, size int) bool { if translated := m.translateInboundReverse(packetData, d); translated { // Re-decode after translation to get original addresses if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { - m.logger.Error("Failed to re-decode packet after reverse DNAT: %v", err) + m.logger.Error1("Failed to re-decode packet after reverse DNAT: %v", err) return true } srcIP, dstIP = m.extractIPs(d) @@ -766,7 +766,7 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet _, pnum := getProtocolFromPacket(d) srcPort, dstPort := getPortsFromPacket(d) - m.logger.Trace("Dropping local packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", + m.logger.Trace6("Dropping local packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", ruleID, pnum, srcIP, srcPort, dstIP, dstPort) m.flowLogger.StoreEvent(nftypes.EventFields{ @@ -807,7 +807,7 @@ func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool { } if err := fwd.InjectIncomingPacket(packetData); err != nil { - m.logger.Error("Failed to inject local packet: %v", err) + m.logger.Error1("Failed to inject local packet: %v", err) } // don't process this packet further @@ -819,7 +819,7 @@ func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool { func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) bool { // Drop if routing is disabled if !m.routingEnabled.Load() { - m.logger.Trace("Dropping routed packet (routing disabled): src=%s dst=%s", + m.logger.Trace2("Dropping routed packet (routing disabled): src=%s dst=%s", srcIP, dstIP) return true } @@ -835,7 +835,7 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) if !pass { - m.logger.Trace("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", + m.logger.Trace6("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", ruleID, pnum, srcIP, srcPort, dstIP, dstPort) m.flowLogger.StoreEvent(nftypes.EventFields{ @@ -863,7 +863,7 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe fwd.RegisterRuleID(srcIP, dstIP, srcPort, dstPort, ruleID) if err := fwd.InjectIncomingPacket(packetData); err != nil { - m.logger.Error("Failed to inject routed packet: %v", err) + m.logger.Error1("Failed to inject routed packet: %v", err) fwd.DeleteRuleID(srcIP, dstIP, srcPort, dstPort) } } @@ -901,7 +901,7 @@ func getPortsFromPacket(d *decoder) (srcPort, dstPort uint16) { // It returns true, true if the packet is a fragment and valid. func (m *Manager) isValidPacket(d *decoder, packetData []byte) (bool, bool) { if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { - m.logger.Trace("couldn't decode packet, err: %s", err) + m.logger.Trace1("couldn't decode packet, err: %s", err) return false, false } diff --git a/client/firewall/uspfilter/forwarder/endpoint.go b/client/firewall/uspfilter/forwarder/endpoint.go index e18c083b9..f91291ea8 100644 --- a/client/firewall/uspfilter/forwarder/endpoint.go +++ b/client/firewall/uspfilter/forwarder/endpoint.go @@ -57,7 +57,7 @@ func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) address := netHeader.DestinationAddress() err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice()) if err != nil { - e.logger.Error("CreateOutboundPacket: %v", err) + e.logger.Error1("CreateOutboundPacket: %v", err) continue } written++ diff --git a/client/firewall/uspfilter/forwarder/icmp.go b/client/firewall/uspfilter/forwarder/icmp.go index 08d77ed05..939c04789 100644 --- a/client/firewall/uspfilter/forwarder/icmp.go +++ b/client/firewall/uspfilter/forwarder/icmp.go @@ -34,14 +34,14 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf // TODO: support non-root conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0") if err != nil { - f.logger.Error("forwarder: Failed to create ICMP socket for %v: %v", epID(id), err) + f.logger.Error2("forwarder: Failed to create ICMP socket for %v: %v", epID(id), err) // This will make netstack reply on behalf of the original destination, that's ok for now return false } defer func() { if err := conn.Close(); err != nil { - f.logger.Debug("forwarder: Failed to close ICMP socket: %v", err) + f.logger.Debug1("forwarder: Failed to close ICMP socket: %v", err) } }() @@ -52,11 +52,11 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf payload := fullPacket.AsSlice() if _, err = conn.WriteTo(payload, dst); err != nil { - f.logger.Error("forwarder: Failed to write ICMP packet for %v: %v", epID(id), err) + f.logger.Error2("forwarder: Failed to write ICMP packet for %v: %v", epID(id), err) return true } - f.logger.Trace("forwarder: Forwarded ICMP packet %v type %v code %v", + f.logger.Trace3("forwarder: Forwarded ICMP packet %v type %v code %v", epID(id), icmpHdr.Type(), icmpHdr.Code()) // For Echo Requests, send and handle response @@ -72,7 +72,7 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) int { if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { - f.logger.Error("forwarder: Failed to set read deadline for ICMP response: %v", err) + f.logger.Error1("forwarder: Failed to set read deadline for ICMP response: %v", err) return 0 } @@ -80,7 +80,7 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketCon n, _, err := conn.ReadFrom(response) if err != nil { if !isTimeout(err) { - f.logger.Error("forwarder: Failed to read ICMP response: %v", err) + f.logger.Error1("forwarder: Failed to read ICMP response: %v", err) } return 0 } @@ -101,12 +101,12 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketCon fullPacket = append(fullPacket, response[:n]...) if err := f.InjectIncomingPacket(fullPacket); err != nil { - f.logger.Error("forwarder: Failed to inject ICMP response: %v", err) + f.logger.Error1("forwarder: Failed to inject ICMP response: %v", err) return 0 } - f.logger.Trace("forwarder: Forwarded ICMP echo reply for %v type %v code %v", + f.logger.Trace3("forwarder: Forwarded ICMP echo reply for %v type %v code %v", epID(id), icmpHdr.Type(), icmpHdr.Code()) return len(fullPacket) diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go index aa42f811b..aef420061 100644 --- a/client/firewall/uspfilter/forwarder/tcp.go +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -38,7 +38,7 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr) if err != nil { r.Complete(true) - f.logger.Trace("forwarder: dial error for %v: %v", epID(id), err) + f.logger.Trace2("forwarder: dial error for %v: %v", epID(id), err) return } @@ -47,9 +47,9 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { ep, epErr := r.CreateEndpoint(&wq) if epErr != nil { - f.logger.Error("forwarder: failed to create TCP endpoint: %v", epErr) + f.logger.Error1("forwarder: failed to create TCP endpoint: %v", epErr) if err := outConn.Close(); err != nil { - f.logger.Debug("forwarder: outConn close error: %v", err) + f.logger.Debug1("forwarder: outConn close error: %v", err) } r.Complete(true) return @@ -61,7 +61,7 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { inConn := gonet.NewTCPConn(&wq, ep) success = true - f.logger.Trace("forwarder: established TCP connection %v", epID(id)) + f.logger.Trace1("forwarder: established TCP connection %v", epID(id)) go f.proxyTCP(id, inConn, outConn, ep, flowID) } @@ -75,10 +75,10 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn <-ctx.Done() // Close connections and endpoint. if err := inConn.Close(); err != nil && !isClosedError(err) { - f.logger.Debug("forwarder: inConn close error: %v", err) + f.logger.Debug1("forwarder: inConn close error: %v", err) } if err := outConn.Close(); err != nil && !isClosedError(err) { - f.logger.Debug("forwarder: outConn close error: %v", err) + f.logger.Debug1("forwarder: outConn close error: %v", err) } ep.Close() @@ -111,12 +111,12 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn if errInToOut != nil { if !isClosedError(errInToOut) { - f.logger.Error("proxyTCP: copy error (in → out) for %s: %v", epID(id), errInToOut) + f.logger.Error2("proxyTCP: copy error (in → out) for %s: %v", epID(id), errInToOut) } } if errOutToIn != nil { if !isClosedError(errOutToIn) { - f.logger.Error("proxyTCP: copy error (out → in) for %s: %v", epID(id), errOutToIn) + f.logger.Error2("proxyTCP: copy error (out → in) for %s: %v", epID(id), errOutToIn) } } @@ -127,7 +127,7 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn txPackets = tcpStats.SegmentsReceived.Value() } - f.logger.Trace("forwarder: Removed TCP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesFromOutToIn, txPackets, bytesFromInToOut) + f.logger.Trace5("forwarder: Removed TCP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesFromOutToIn, txPackets, bytesFromInToOut) f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets) } diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go index 3a761d06b..d146de5e4 100644 --- a/client/firewall/uspfilter/forwarder/udp.go +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -78,10 +78,10 @@ func (f *udpForwarder) Stop() { for id, conn := range f.conns { conn.cancel() if err := conn.conn.Close(); err != nil { - f.logger.Debug("forwarder: UDP conn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP conn close error for %v: %v", epID(id), err) } if err := conn.outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err) } conn.ep.Close() @@ -112,10 +112,10 @@ func (f *udpForwarder) cleanup() { for _, idle := range idleConns { idle.conn.cancel() if err := idle.conn.conn.Close(); err != nil { - f.logger.Debug("forwarder: UDP conn close error for %v: %v", epID(idle.id), err) + f.logger.Debug2("forwarder: UDP conn close error for %v: %v", epID(idle.id), err) } if err := idle.conn.outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(idle.id), err) + f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(idle.id), err) } idle.conn.ep.Close() @@ -124,7 +124,7 @@ func (f *udpForwarder) cleanup() { delete(f.conns, idle.id) f.Unlock() - f.logger.Trace("forwarder: cleaned up idle UDP connection %v", epID(idle.id)) + f.logger.Trace1("forwarder: cleaned up idle UDP connection %v", epID(idle.id)) } } } @@ -143,7 +143,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { _, exists := f.udpForwarder.conns[id] f.udpForwarder.RUnlock() if exists { - f.logger.Trace("forwarder: existing UDP connection for %v", epID(id)) + f.logger.Trace1("forwarder: existing UDP connection for %v", epID(id)) return } @@ -160,7 +160,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr) if err != nil { - f.logger.Debug("forwarder: UDP dial error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP dial error for %v: %v", epID(id), err) // TODO: Send ICMP error message return } @@ -169,9 +169,9 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { wq := waiter.Queue{} ep, epErr := r.CreateEndpoint(&wq) if epErr != nil { - f.logger.Debug("forwarder: failed to create UDP endpoint: %v", epErr) + f.logger.Debug1("forwarder: failed to create UDP endpoint: %v", epErr) if err := outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err) } return } @@ -194,10 +194,10 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { f.udpForwarder.Unlock() pConn.cancel() if err := inConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP inConn close error for %v: %v", epID(id), err) } if err := outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err) } return } @@ -205,7 +205,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { f.udpForwarder.Unlock() success = true - f.logger.Trace("forwarder: established UDP connection %v", epID(id)) + f.logger.Trace1("forwarder: established UDP connection %v", epID(id)) go f.proxyUDP(connCtx, pConn, id, ep) } @@ -220,10 +220,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack pConn.cancel() if err := pConn.conn.Close(); err != nil && !isClosedError(err) { - f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP inConn close error for %v: %v", epID(id), err) } if err := pConn.outConn.Close(); err != nil && !isClosedError(err) { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) + f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err) } ep.Close() @@ -250,10 +250,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack wg.Wait() if outboundErr != nil && !isClosedError(outboundErr) { - f.logger.Error("proxyUDP: copy error (outbound→inbound) for %s: %v", epID(id), outboundErr) + f.logger.Error2("proxyUDP: copy error (outbound→inbound) for %s: %v", epID(id), outboundErr) } if inboundErr != nil && !isClosedError(inboundErr) { - f.logger.Error("proxyUDP: copy error (inbound→outbound) for %s: %v", epID(id), inboundErr) + f.logger.Error2("proxyUDP: copy error (inbound→outbound) for %s: %v", epID(id), inboundErr) } var rxPackets, txPackets uint64 @@ -263,7 +263,7 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack txPackets = udpStats.PacketsReceived.Value() } - f.logger.Trace("forwarder: Removed UDP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, rxBytes, txPackets, txBytes) + f.logger.Trace5("forwarder: Removed UDP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, rxBytes, txPackets, txBytes) f.udpForwarder.Lock() delete(f.udpForwarder.conns, id) diff --git a/client/firewall/uspfilter/log/log.go b/client/firewall/uspfilter/log/log.go index d22421e2d..5614e2ec3 100644 --- a/client/firewall/uspfilter/log/log.go +++ b/client/firewall/uspfilter/log/log.go @@ -44,7 +44,12 @@ var levelStrings = map[Level]string{ type logMessage struct { level Level format string - args []any + arg1 any + arg2 any + arg3 any + arg4 any + arg5 any + arg6 any } // Logger is a high-performance, non-blocking logger @@ -89,62 +94,198 @@ func (l *Logger) SetLevel(level Level) { log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level]) } -func (l *Logger) log(level Level, format string, args ...any) { - select { - case l.msgChannel <- logMessage{level: level, format: format, args: args}: - default: - } -} -// Error logs a message at error level -func (l *Logger) Error(format string, args ...any) { +func (l *Logger) Error(format string) { if l.level.Load() >= uint32(LevelError) { - l.log(LevelError, format, args...) + select { + case l.msgChannel <- logMessage{level: LevelError, format: format}: + default: + } } } -// Warn logs a message at warning level -func (l *Logger) Warn(format string, args ...any) { +func (l *Logger) Warn(format string) { if l.level.Load() >= uint32(LevelWarn) { - l.log(LevelWarn, format, args...) + select { + case l.msgChannel <- logMessage{level: LevelWarn, format: format}: + default: + } } } -// Info logs a message at info level -func (l *Logger) Info(format string, args ...any) { +func (l *Logger) Info(format string) { if l.level.Load() >= uint32(LevelInfo) { - l.log(LevelInfo, format, args...) + select { + case l.msgChannel <- logMessage{level: LevelInfo, format: format}: + default: + } } } -// Debug logs a message at debug level -func (l *Logger) Debug(format string, args ...any) { +func (l *Logger) Debug(format string) { if l.level.Load() >= uint32(LevelDebug) { - l.log(LevelDebug, format, args...) + select { + case l.msgChannel <- logMessage{level: LevelDebug, format: format}: + default: + } } } -// Trace logs a message at trace level -func (l *Logger) Trace(format string, args ...any) { +func (l *Logger) Trace(format string) { if l.level.Load() >= uint32(LevelTrace) { - l.log(LevelTrace, format, args...) + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format}: + default: + } } } -func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...any) { +func (l *Logger) Error1(format string, arg1 any) { + if l.level.Load() >= uint32(LevelError) { + select { + case l.msgChannel <- logMessage{level: LevelError, format: format, arg1: arg1}: + default: + } + } +} + +func (l *Logger) Error2(format string, arg1, arg2 any) { + if l.level.Load() >= uint32(LevelError) { + select { + case l.msgChannel <- logMessage{level: LevelError, format: format, arg1: arg1, arg2: arg2}: + default: + } + } +} + +func (l *Logger) Warn3(format string, arg1, arg2, arg3 any) { + if l.level.Load() >= uint32(LevelWarn) { + select { + case l.msgChannel <- logMessage{level: LevelWarn, format: format, arg1: arg1, arg2: arg2, arg3: arg3}: + default: + } + } +} + +func (l *Logger) Debug1(format string, arg1 any) { + if l.level.Load() >= uint32(LevelDebug) { + select { + case l.msgChannel <- logMessage{level: LevelDebug, format: format, arg1: arg1}: + default: + } + } +} + +func (l *Logger) Debug2(format string, arg1, arg2 any) { + if l.level.Load() >= uint32(LevelDebug) { + select { + case l.msgChannel <- logMessage{level: LevelDebug, format: format, arg1: arg1, arg2: arg2}: + default: + } + } +} + +func (l *Logger) Trace1(format string, arg1 any) { + if l.level.Load() >= uint32(LevelTrace) { + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1}: + default: + } + } +} + +func (l *Logger) Trace2(format string, arg1, arg2 any) { + if l.level.Load() >= uint32(LevelTrace) { + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2}: + default: + } + } +} + +func (l *Logger) Trace3(format string, arg1, arg2, arg3 any) { + if l.level.Load() >= uint32(LevelTrace) { + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3}: + default: + } + } +} + +func (l *Logger) Trace4(format string, arg1, arg2, arg3, arg4 any) { + if l.level.Load() >= uint32(LevelTrace) { + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4}: + default: + } + } +} + +func (l *Logger) Trace5(format string, arg1, arg2, arg3, arg4, arg5 any) { + if l.level.Load() >= uint32(LevelTrace) { + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4, arg5: arg5}: + default: + } + } +} + +func (l *Logger) Trace6(format string, arg1, arg2, arg3, arg4, arg5, arg6 any) { + if l.level.Load() >= uint32(LevelTrace) { + select { + case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4, arg5: arg5, arg6: arg6}: + default: + } + } +} + +func (l *Logger) formatMessage(buf *[]byte, msg logMessage) { *buf = (*buf)[:0] *buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00") *buf = append(*buf, ' ') - *buf = append(*buf, levelStrings[level]...) + *buf = append(*buf, levelStrings[msg.level]...) *buf = append(*buf, ' ') - var msg string - if len(args) > 0 { - msg = fmt.Sprintf(format, args...) - } else { - msg = format + // Count non-nil arguments for switch + argCount := 0 + if msg.arg1 != nil { + argCount++ + if msg.arg2 != nil { + argCount++ + if msg.arg3 != nil { + argCount++ + if msg.arg4 != nil { + argCount++ + if msg.arg5 != nil { + argCount++ + if msg.arg6 != nil { + argCount++ + } + } + } + } + } } - *buf = append(*buf, msg...) + + var formatted string + switch argCount { + case 0: + formatted = msg.format + case 1: + formatted = fmt.Sprintf(msg.format, msg.arg1) + case 2: + formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2) + case 3: + formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3) + case 4: + formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4) + case 5: + formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5) + case 6: + formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5, msg.arg6) + } + + *buf = append(*buf, formatted...) *buf = append(*buf, '\n') if len(*buf) > maxMessageSize { @@ -157,7 +298,7 @@ func (l *Logger) processMessage(msg logMessage, buffer *[]byte) { bufp := l.bufPool.Get().(*[]byte) defer l.bufPool.Put(bufp) - l.formatMessage(bufp, msg.level, msg.format, msg.args...) + l.formatMessage(bufp, msg) if len(*buffer)+len(*bufp) > maxBatchSize { _, _ = l.output.Write(*buffer) @@ -249,4 +390,4 @@ func (l *Logger) Stop(ctx context.Context) error { case <-done: return nil } -} +} \ No newline at end of file diff --git a/client/firewall/uspfilter/log/log_test.go b/client/firewall/uspfilter/log/log_test.go index e7da9a8e9..0c221c262 100644 --- a/client/firewall/uspfilter/log/log_test.go +++ b/client/firewall/uspfilter/log/log_test.go @@ -19,22 +19,17 @@ func (d *discard) Write(p []byte) (n int, err error) { func BenchmarkLogger(b *testing.B) { simpleMessage := "Connection established" - conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d" srcIP := "192.168.1.1" srcPort := uint16(12345) dstIP := "10.0.0.1" dstPort := uint16(443) state := 4 // TCPStateEstablished - complexMessage := "Packet inspection result: protocol=%s, direction=%s, flags=0x%x, sequence=%d, acknowledged=%d, payload_size=%d, fragmented=%v, connection_id=%s" protocol := "TCP" direction := "outbound" flags := uint16(0x18) // ACK + PSH sequence := uint32(123456789) acknowledged := uint32(987654321) - payloadSize := 1460 - fragmented := false - connID := "f7a12b3e-c456-7890-d123-456789abcdef" b.Run("SimpleMessage", func(b *testing.B) { logger := createTestLogger() @@ -52,7 +47,7 @@ func BenchmarkLogger(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) + logger.Trace5("TCP connection %s:%d → %s:%d state %d", srcIP, srcPort, dstIP, dstPort, state) } }) @@ -62,7 +57,7 @@ func BenchmarkLogger(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - logger.Trace(complexMessage, protocol, direction, flags, sequence, acknowledged, payloadSize, fragmented, connID) + logger.Trace6("Complex trace: proto=%s dir=%s flags=%d seq=%d ack=%d size=%d", protocol, direction, flags, sequence, acknowledged, 1460) } }) } @@ -72,7 +67,6 @@ func BenchmarkLoggerParallel(b *testing.B) { logger := createTestLogger() defer cleanupLogger(logger) - conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d" srcIP := "192.168.1.1" srcPort := uint16(12345) dstIP := "10.0.0.1" @@ -82,7 +76,7 @@ func BenchmarkLoggerParallel(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) + logger.Trace5("TCP connection %s:%d → %s:%d state %d", srcIP, srcPort, dstIP, dstPort, state) } }) } @@ -92,7 +86,6 @@ func BenchmarkLoggerBurst(b *testing.B) { logger := createTestLogger() defer cleanupLogger(logger) - conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d" srcIP := "192.168.1.1" srcPort := uint16(12345) dstIP := "10.0.0.1" @@ -102,7 +95,7 @@ func BenchmarkLoggerBurst(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < 100; j++ { - logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) + logger.Trace5("TCP connection %s:%d → %s:%d state %d", srcIP, srcPort, dstIP, dstPort, state) } } } diff --git a/client/firewall/uspfilter/nat.go b/client/firewall/uspfilter/nat.go index 4539f7da5..27b752531 100644 --- a/client/firewall/uspfilter/nat.go +++ b/client/firewall/uspfilter/nat.go @@ -211,11 +211,11 @@ func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool { } if err := m.rewritePacketDestination(packetData, d, translatedIP); err != nil { - m.logger.Error("Failed to rewrite packet destination: %v", err) + m.logger.Error1("Failed to rewrite packet destination: %v", err) return false } - m.logger.Trace("DNAT: %s -> %s", dstIP, translatedIP) + m.logger.Trace2("DNAT: %s -> %s", dstIP, translatedIP) return true } @@ -237,11 +237,11 @@ func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool { } if err := m.rewritePacketSource(packetData, d, originalIP); err != nil { - m.logger.Error("Failed to rewrite packet source: %v", err) + m.logger.Error1("Failed to rewrite packet source: %v", err) return false } - m.logger.Trace("Reverse DNAT: %s -> %s", srcIP, originalIP) + m.logger.Trace2("Reverse DNAT: %s -> %s", srcIP, originalIP) return true } From 552dc605479eb92822df107eb78e0c1326290bc2 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:22:07 +0200 Subject: [PATCH 189/213] [management] migrate group peers into seperate table (#4096) --- management/server/account.go | 95 +++--- management/server/account/manager.go | 6 +- management/server/account_test.go | 31 +- management/server/dns_test.go | 8 +- management/server/group.go | 230 +++++++++++-- management/server/group_test.go | 323 ++++++++++++++++-- .../http/handlers/groups/groups_handler.go | 4 +- management/server/migration/migration.go | 64 ++++ management/server/mock_server/account_mock.go | 28 ++ management/server/nameserver_test.go | 24 +- management/server/peer.go | 56 ++- management/server/peer_test.go | 33 +- management/server/policy_test.go | 10 +- management/server/posture_checks_test.go | 25 +- management/server/route_test.go | 21 +- management/server/setupkey_test.go | 16 +- management/server/store/sql_store.go | 304 +++++++++++++---- management/server/store/sql_store_test.go | 113 ++++-- management/server/store/store.go | 23 +- management/server/types/account.go | 2 +- management/server/types/group.go | 32 +- management/server/types/setupkey.go | 2 +- management/server/user.go | 106 +----- management/server/user_test.go | 4 +- 24 files changed, 1139 insertions(+), 421 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 52b625da1..d392cd0b9 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1368,7 +1368,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth return nil } - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, newGroupsToCreate); err != nil { + if err = transaction.CreateGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, newGroupsToCreate); err != nil { return fmt.Errorf("error saving groups: %w", err) } @@ -1382,28 +1382,22 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth // Propagate changes to peers if group propagation is enabled if settings.GroupsPropagationEnabled { - groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId) - if err != nil { - return fmt.Errorf("error getting account groups: %w", err) - } - - groupsMap := make(map[string]*types.Group, len(groups)) - for _, group := range groups { - groupsMap[group.ID] = group - } - peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, userAuth.AccountId, userAuth.UserId) if err != nil { return fmt.Errorf("error getting user peers: %w", err) } - updatedGroups, err := updateUserPeersInGroups(groupsMap, peers, addNewGroups, removeOldGroups) - if err != nil { - return fmt.Errorf("error modifying user peers in groups: %w", err) - } - - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, updatedGroups); err != nil { - return fmt.Errorf("error saving groups: %w", err) + for _, peer := range peers { + for _, g := range addNewGroups { + if err := transaction.AddPeerToGroup(ctx, userAuth.AccountId, peer.ID, g); err != nil { + return fmt.Errorf("error adding peer %s to group %s: %w", peer.ID, g, err) + } + } + for _, g := range removeOldGroups { + if err := transaction.RemovePeerFromGroup(ctx, peer.ID, g); err != nil { + return fmt.Errorf("error removing peer %s from group %s: %w", peer.ID, g, err) + } + } } if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, userAuth.AccountId); err != nil { @@ -1971,53 +1965,56 @@ func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, acc // propagateUserGroupMemberships propagates all account users' group memberships to their peers. // Returns true if any groups were modified, true if those updates affect peers and an error. func propagateUserGroupMemberships(ctx context.Context, transaction store.Store, accountID string) (groupsUpdated bool, peersAffected bool, err error) { - groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return false, false, err - } - - groupsMap := make(map[string]*types.Group, len(groups)) - for _, group := range groups { - groupsMap[group.ID] = group - } - users, err := transaction.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) if err != nil { return false, false, err } - groupsToUpdate := make(map[string]*types.Group) + accountGroupPeers, err := transaction.GetAccountGroupPeers(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return false, false, fmt.Errorf("error getting account group peers: %w", err) + } + accountGroups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return false, false, fmt.Errorf("error getting account groups: %w", err) + } + + for _, group := range accountGroups { + if _, exists := accountGroupPeers[group.ID]; !exists { + accountGroupPeers[group.ID] = make(map[string]struct{}) + } + } + + updatedGroups := []string{} for _, user := range users { userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, user.Id) if err != nil { return false, false, err } - updatedGroups, err := updateUserPeersInGroups(groupsMap, userPeers, user.AutoGroups, nil) - if err != nil { - return false, false, err - } - - for _, group := range updatedGroups { - groupsToUpdate[group.ID] = group - groupsMap[group.ID] = group + for _, peer := range userPeers { + for _, groupID := range user.AutoGroups { + if _, exists := accountGroupPeers[groupID]; !exists { + // we do not wanna create the groups here + log.WithContext(ctx).Warnf("group %s does not exist for user group propagation", groupID) + continue + } + if _, exists := accountGroupPeers[groupID][peer.ID]; exists { + continue + } + if err := transaction.AddPeerToGroup(ctx, accountID, peer.ID, groupID); err != nil { + return false, false, fmt.Errorf("error adding peer %s to group %s: %w", peer.ID, groupID, err) + } + updatedGroups = append(updatedGroups, groupID) + } } } - if len(groupsToUpdate) == 0 { - return false, false, nil - } - - peersAffected, err = areGroupChangesAffectPeers(ctx, transaction, accountID, maps.Keys(groupsToUpdate)) + peersAffected, err = areGroupChangesAffectPeers(ctx, transaction, accountID, updatedGroups) if err != nil { - return false, false, err + return false, false, fmt.Errorf("error checking if group changes affect peers: %w", err) } - err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, maps.Values(groupsToUpdate)) - if err != nil { - return false, false, err - } - - return true, peersAffected, nil + return len(updatedGroups) > 0, peersAffected, nil } diff --git a/management/server/account/manager.go b/management/server/account/manager.go index 8c7e95e3d..0cd1c6637 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -62,8 +62,10 @@ type Manager interface { GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) - SaveGroup(ctx context.Context, accountID, userID string, group *types.Group, create bool) error - SaveGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group, create bool) error + CreateGroup(ctx context.Context, accountID, userID string, group *types.Group) error + UpdateGroup(ctx context.Context, accountID, userID string, group *types.Group) error + CreateGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error + UpdateGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error DeleteGroup(ctx context.Context, accountId, userId, groupID string) error DeleteGroups(ctx context.Context, accountId, userId string, groupIDs []string) error GroupAddPeer(ctx context.Context, accountId, groupID, peerID string) error diff --git a/management/server/account_test.go b/management/server/account_test.go index b65dffe6c..1dd74104b 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1159,7 +1159,7 @@ func TestAccountManager_NetworkUpdates_SaveGroup(t *testing.T) { Name: "GroupA", Peers: []string{}, } - if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil { + if err := manager.CreateGroup(context.Background(), account.Id, userID, &group); err != nil { t.Errorf("save group: %v", err) return } @@ -1194,7 +1194,7 @@ func TestAccountManager_NetworkUpdates_SaveGroup(t *testing.T) { }() group.Peers = []string{peer1.ID, peer2.ID, peer3.ID} - if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil { + if err := manager.UpdateGroup(context.Background(), account.Id, userID, &group); err != nil { t.Errorf("save group: %v", err) return } @@ -1240,11 +1240,12 @@ func TestAccountManager_NetworkUpdates_SavePolicy(t *testing.T) { manager, account, peer1, peer2, _ := setupNetworkMapTest(t) group := types.Group{ - ID: "groupA", - Name: "GroupA", - Peers: []string{peer1.ID, peer2.ID}, + AccountID: account.Id, + ID: "groupA", + Name: "GroupA", + Peers: []string{peer1.ID, peer2.ID}, } - if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil { + if err := manager.CreateGroup(context.Background(), account.Id, userID, &group); err != nil { t.Errorf("save group: %v", err) return } @@ -1292,7 +1293,7 @@ func TestAccountManager_NetworkUpdates_DeletePeer(t *testing.T) { Name: "GroupA", Peers: []string{peer1.ID, peer3.ID}, } - if err := manager.SaveGroup(context.Background(), account.Id, userID, &group, true); err != nil { + if err := manager.CreateGroup(context.Background(), account.Id, userID, &group); err != nil { t.Errorf("save group: %v", err) return } @@ -1343,11 +1344,11 @@ func TestAccountManager_NetworkUpdates_DeleteGroup(t *testing.T) { updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) defer manager.peersUpdateManager.CloseChannel(context.Background(), peer1.ID) - err := manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err := manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupA", Name: "GroupA", Peers: []string{peer1.ID, peer2.ID, peer3.ID}, - }, true) + }) require.NoError(t, err, "failed to save group") @@ -1672,9 +1673,10 @@ func TestAccount_Copy(t *testing.T) { }, Groups: map[string]*types.Group{ "group1": { - ID: "group1", - Peers: []string{"peer1"}, - Resources: []types.Resource{}, + ID: "group1", + Peers: []string{"peer1"}, + Resources: []types.Resource{}, + GroupPeers: []types.GroupPeer{}, }, }, Policies: []*types.Policy{ @@ -2616,6 +2618,7 @@ func TestAccount_GetNextInactivePeerExpiration(t *testing.T) { } func TestAccount_SetJWTGroups(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", "postgres") manager, err := createManager(t) require.NoError(t, err, "unable to create account manager") @@ -3360,7 +3363,7 @@ func TestPropagateUserGroupMemberships(t *testing.T) { t.Run("should update membership but no account peers update for unused groups", func(t *testing.T) { group1 := &types.Group{ID: "group1", Name: "Group 1", AccountID: account.Id} - require.NoError(t, manager.Store.SaveGroup(ctx, store.LockingStrengthUpdate, group1)) + require.NoError(t, manager.Store.CreateGroup(ctx, store.LockingStrengthUpdate, group1)) user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) require.NoError(t, err) @@ -3382,7 +3385,7 @@ func TestPropagateUserGroupMemberships(t *testing.T) { t.Run("should update membership and account peers for used groups", func(t *testing.T) { group2 := &types.Group{ID: "group2", Name: "Group 2", AccountID: account.Id} - require.NoError(t, manager.Store.SaveGroup(ctx, store.LockingStrengthUpdate, group2)) + require.NoError(t, manager.Store.CreateGroup(ctx, store.LockingStrengthUpdate, group2)) user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) require.NoError(t, err) diff --git a/management/server/dns_test.go b/management/server/dns_test.go index f2295450f..2af07d8e4 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -495,7 +495,7 @@ func TestToProtocolDNSConfigWithCache(t *testing.T) { func TestDNSAccountPeersUpdate(t *testing.T) { manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) - err := manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + err := manager.CreateGroups(context.Background(), account.Id, userID, []*types.Group{ { ID: "groupA", Name: "GroupA", @@ -506,7 +506,7 @@ func TestDNSAccountPeersUpdate(t *testing.T) { Name: "GroupB", Peers: []string{}, }, - }, true) + }) assert.NoError(t, err) updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) @@ -562,11 +562,11 @@ func TestDNSAccountPeersUpdate(t *testing.T) { // Creating DNS settings with groups that have peers should update account peers and send peer update t.Run("creating dns setting with used groups", func(t *testing.T) { - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupA", Name: "GroupA", Peers: []string{peer1.ID, peer2.ID, peer3.ID}, - }, true) + }) assert.NoError(t, err) done := make(chan struct{}) diff --git a/management/server/group.go b/management/server/group.go index 130a67145..95bed7d18 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -65,22 +65,144 @@ func (am *DefaultAccountManager) GetGroupByName(ctx context.Context, groupName, return am.Store.GetGroupByName(ctx, store.LockingStrengthShare, accountID, groupName) } -// SaveGroup object of the peers -func (am *DefaultAccountManager) SaveGroup(ctx context.Context, accountID, userID string, newGroup *types.Group, create bool) error { +// CreateGroup object of the peers +func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error { unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - return am.SaveGroups(ctx, accountID, userID, []*types.Group{newGroup}, create) + + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Create) + if err != nil { + return status.NewPermissionValidationError(err) + } + if !allowed { + return status.NewPermissionDeniedError() + } + + var eventsToStore []func() + var updateAccountPeers bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + if err = validateNewGroup(ctx, transaction, accountID, newGroup); err != nil { + return err + } + + newGroup.AccountID = accountID + + events := am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup) + eventsToStore = append(eventsToStore, events...) + + updateAccountPeers, err = areGroupChangesAffectPeers(ctx, transaction, accountID, []string{newGroup.ID}) + if err != nil { + return err + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + if err := transaction.CreateGroup(ctx, store.LockingStrengthUpdate, newGroup); err != nil { + return status.Errorf(status.Internal, "failed to create group: %v", err) + } + + for _, peerID := range newGroup.Peers { + if err := transaction.AddPeerToGroup(ctx, accountID, peerID, newGroup.ID); err != nil { + return status.Errorf(status.Internal, "failed to add peer %s to group %s: %v", peerID, newGroup.ID, err) + } + } + return nil + }) + if err != nil { + return err + } + + for _, storeEvent := range eventsToStore { + storeEvent() + } + + if updateAccountPeers { + am.UpdateAccountPeers(ctx, accountID) + } + + return nil } -// SaveGroups adds new groups to the account. +// UpdateGroup object of the peers +func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error { + unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) + defer unlock() + + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update) + if err != nil { + return status.NewPermissionValidationError(err) + } + if !allowed { + return status.NewPermissionDeniedError() + } + + var eventsToStore []func() + var updateAccountPeers bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + if err = validateNewGroup(ctx, transaction, accountID, newGroup); err != nil { + return err + } + + oldGroup, err := transaction.GetGroupByID(ctx, store.LockingStrengthShare, accountID, newGroup.ID) + if err != nil { + return status.Errorf(status.NotFound, "group with ID %s not found", newGroup.ID) + } + + peersToAdd := util.Difference(newGroup.Peers, oldGroup.Peers) + peersToRemove := util.Difference(oldGroup.Peers, newGroup.Peers) + + for _, peerID := range peersToAdd { + if err := transaction.AddPeerToGroup(ctx, accountID, peerID, newGroup.ID); err != nil { + return status.Errorf(status.Internal, "failed to add peer %s to group %s: %v", peerID, newGroup.ID, err) + } + } + for _, peerID := range peersToRemove { + if err := transaction.RemovePeerFromGroup(ctx, peerID, newGroup.ID); err != nil { + return status.Errorf(status.Internal, "failed to remove peer %s from group %s: %v", peerID, newGroup.ID, err) + } + } + + newGroup.AccountID = accountID + + events := am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup) + eventsToStore = append(eventsToStore, events...) + + updateAccountPeers, err = areGroupChangesAffectPeers(ctx, transaction, accountID, []string{newGroup.ID}) + if err != nil { + return err + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + return transaction.UpdateGroup(ctx, store.LockingStrengthUpdate, newGroup) + }) + if err != nil { + return err + } + + for _, storeEvent := range eventsToStore { + storeEvent() + } + + if updateAccountPeers { + am.UpdateAccountPeers(ctx, accountID) + } + + return nil +} + +// CreateGroups adds new groups to the account. // Note: This function does not acquire the global lock. // It is the caller's responsibility to ensure proper locking is in place before invoking this method. -func (am *DefaultAccountManager) SaveGroups(ctx context.Context, accountID, userID string, groups []*types.Group, create bool) error { - operation := operations.Create - if !create { - operation = operations.Update - } - allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operation) +// This method will not create group peer membership relations. Use AddPeerToGroup or RemovePeerFromGroup methods for that. +func (am *DefaultAccountManager) CreateGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error { + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Create) if err != nil { return status.NewPermissionValidationError(err) } @@ -116,7 +238,65 @@ func (am *DefaultAccountManager) SaveGroups(ctx context.Context, accountID, user return err } - return transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave) + return transaction.CreateGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave) + }) + if err != nil { + return err + } + + for _, storeEvent := range eventsToStore { + storeEvent() + } + + if updateAccountPeers { + am.UpdateAccountPeers(ctx, accountID) + } + + return nil +} + +// UpdateGroups updates groups in the account. +// Note: This function does not acquire the global lock. +// It is the caller's responsibility to ensure proper locking is in place before invoking this method. +// This method will not create group peer membership relations. Use AddPeerToGroup or RemovePeerFromGroup methods for that. +func (am *DefaultAccountManager) UpdateGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error { + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update) + if err != nil { + return status.NewPermissionValidationError(err) + } + if !allowed { + return status.NewPermissionDeniedError() + } + + var eventsToStore []func() + var groupsToSave []*types.Group + var updateAccountPeers bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + groupIDs := make([]string, 0, len(groups)) + for _, newGroup := range groups { + if err = validateNewGroup(ctx, transaction, accountID, newGroup); err != nil { + return err + } + + newGroup.AccountID = accountID + groupsToSave = append(groupsToSave, newGroup) + groupIDs = append(groupIDs, newGroup.ID) + + events := am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup) + eventsToStore = append(eventsToStore, events...) + } + + updateAccountPeers, err = areGroupChangesAffectPeers(ctx, transaction, accountID, groupIDs) + if err != nil { + return err + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + return transaction.UpdateGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave) }) if err != nil { return err @@ -265,20 +445,10 @@ func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, gr unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - var group *types.Group var updateAccountPeers bool var err error err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - group, err = transaction.GetGroupByID(context.Background(), store.LockingStrengthUpdate, accountID, groupID) - if err != nil { - return err - } - - if updated := group.AddPeer(peerID); !updated { - return nil - } - updateAccountPeers, err = areGroupChangesAffectPeers(ctx, transaction, accountID, []string{groupID}) if err != nil { return err @@ -288,7 +458,7 @@ func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, gr return err } - return transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + return transaction.AddPeerToGroup(ctx, accountID, peerID, groupID) }) if err != nil { return err @@ -329,7 +499,7 @@ func (am *DefaultAccountManager) GroupAddResource(ctx context.Context, accountID return err } - return transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + return transaction.UpdateGroup(ctx, store.LockingStrengthUpdate, group) }) if err != nil { return err @@ -347,20 +517,10 @@ func (am *DefaultAccountManager) GroupDeletePeer(ctx context.Context, accountID, unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - var group *types.Group var updateAccountPeers bool var err error err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - group, err = transaction.GetGroupByID(context.Background(), store.LockingStrengthUpdate, accountID, groupID) - if err != nil { - return err - } - - if updated := group.RemovePeer(peerID); !updated { - return nil - } - updateAccountPeers, err = areGroupChangesAffectPeers(ctx, transaction, accountID, []string{groupID}) if err != nil { return err @@ -370,7 +530,7 @@ func (am *DefaultAccountManager) GroupDeletePeer(ctx context.Context, accountID, return err } - return transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + return transaction.RemovePeerFromGroup(ctx, peerID, groupID) }) if err != nil { return err @@ -411,7 +571,7 @@ func (am *DefaultAccountManager) GroupDeleteResource(ctx context.Context, accoun return err } - return transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + return transaction.UpdateGroup(ctx, store.LockingStrengthUpdate, group) }) if err != nil { return err diff --git a/management/server/group_test.go b/management/server/group_test.go index 631fe3a71..51069dc56 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -2,14 +2,20 @@ package server import ( "context" + "encoding/binary" "errors" "fmt" + "net" "net/netip" + "strconv" + "sync" "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/groups" @@ -18,8 +24,10 @@ import ( "github.com/netbirdio/netbird/management/server/networks/routers" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" + peer2 "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" ) @@ -40,7 +48,8 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { } for _, group := range account.Groups { group.Issued = types.GroupIssuedIntegration - err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group, true) + group.ID = uuid.New().String() + err = am.CreateGroup(context.Background(), account.Id, groupAdminUserID, group) if err != nil { t.Errorf("should allow to create %s groups", types.GroupIssuedIntegration) } @@ -48,7 +57,8 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { for _, group := range account.Groups { group.Issued = types.GroupIssuedJWT - err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group, true) + group.ID = uuid.New().String() + err = am.CreateGroup(context.Background(), account.Id, groupAdminUserID, group) if err != nil { t.Errorf("should allow to create %s groups", types.GroupIssuedJWT) } @@ -56,7 +66,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { for _, group := range account.Groups { group.Issued = types.GroupIssuedAPI group.ID = "" - err = am.SaveGroup(context.Background(), account.Id, groupAdminUserID, group, true) + err = am.CreateGroup(context.Background(), account.Id, groupAdminUserID, group) if err == nil { t.Errorf("should not create api group with the same name, %s", group.Name) } @@ -162,7 +172,7 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) { } } - err = manager.SaveGroups(context.Background(), account.Id, groupAdminUserID, groups, true) + err = manager.CreateGroups(context.Background(), account.Id, groupAdminUserID, groups) assert.NoError(t, err, "Failed to save test groups") testCases := []struct { @@ -382,13 +392,13 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t return nil, nil, err } - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForRoute, true) - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForRoute2, true) - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForNameServerGroups, true) - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForPolicies, true) - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys, true) - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForUsers, true) - _ = am.SaveGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration, true) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRoute) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRoute2) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForNameServerGroups) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForPolicies) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForUsers) + _ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration) acc, err := am.Store.GetAccount(context.Background(), account.Id) if err != nil { @@ -400,7 +410,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t func TestGroupAccountPeersUpdate(t *testing.T) { manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) - err := manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + g := []*types.Group{ { ID: "groupA", Name: "GroupA", @@ -426,8 +436,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { Name: "GroupE", Peers: []string{peer2.ID}, }, - }, true) - assert.NoError(t, err) + } + for _, group := range g { + err := manager.CreateGroup(context.Background(), account.Id, userID, group) + assert.NoError(t, err) + } updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) t.Cleanup(func() { @@ -442,11 +455,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { close(done) }() - err := manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err := manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupB", Name: "GroupB", Peers: []string{peer1.ID, peer2.ID}, - }, true) + }) assert.NoError(t, err) select { @@ -513,7 +526,7 @@ func TestGroupAccountPeersUpdate(t *testing.T) { }) // adding a group to policy - _, err = manager.SavePolicy(context.Background(), account.Id, userID, &types.Policy{ + _, err := manager.SavePolicy(context.Background(), account.Id, userID, &types.Policy{ Enabled: true, Rules: []*types.PolicyRule{ { @@ -535,11 +548,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { close(done) }() - err := manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err := manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupA", Name: "GroupA", Peers: []string{peer1.ID, peer2.ID}, - }, true) + }) assert.NoError(t, err) select { @@ -604,11 +617,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { close(done) }() - err := manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err := manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupC", Name: "GroupC", Peers: []string{peer1.ID, peer3.ID}, - }, true) + }) assert.NoError(t, err) select { @@ -645,11 +658,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { close(done) }() - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupA", Name: "GroupA", Peers: []string{peer1.ID, peer2.ID, peer3.ID}, - }, true) + }) assert.NoError(t, err) select { @@ -672,11 +685,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { close(done) }() - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupD", Name: "GroupD", Peers: []string{peer1.ID}, - }, true) + }) assert.NoError(t, err) select { @@ -719,11 +732,11 @@ func TestGroupAccountPeersUpdate(t *testing.T) { close(done) }() - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupE", Name: "GroupE", Peers: []string{peer2.ID, peer3.ID}, - }, true) + }) assert.NoError(t, err) select { @@ -733,3 +746,259 @@ func TestGroupAccountPeersUpdate(t *testing.T) { } }) } + +func Test_AddPeerToGroup(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + accountID := "testaccount" + userID := "testuser" + + acc, err := createAccount(manager, accountID, userID, "domain.com") + if err != nil { + t.Fatal("error creating account") + return + } + + const totalPeers = 1000 + + var wg sync.WaitGroup + errs := make(chan error, totalPeers) + start := make(chan struct{}) + for i := 0; i < totalPeers; i++ { + wg.Add(1) + + go func(i int) { + defer wg.Done() + + <-start + + err = manager.Store.AddPeerToGroup(context.Background(), accountID, strconv.Itoa(i), acc.GroupsG[0].ID) + if err != nil { + errs <- fmt.Errorf("AddPeer failed for peer %d: %w", i, err) + return + } + + }(i) + } + startTime := time.Now() + + close(start) + wg.Wait() + close(errs) + + t.Logf("time since start: %s", time.Since(startTime)) + + for err := range errs { + t.Fatal(err) + } + + account, err := manager.Store.GetAccount(context.Background(), accountID) + if err != nil { + t.Fatalf("Failed to get account %s: %v", accountID, err) + } + + assert.Equal(t, totalPeers, len(maps.Values(account.Groups)[0].Peers), "Expected %d peers in group %s in account %s, got %d", totalPeers, maps.Values(account.Groups)[0].Name, accountID, len(account.Peers)) +} + +func Test_AddPeerToAll(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + accountID := "testaccount" + userID := "testuser" + + _, err = createAccount(manager, accountID, userID, "domain.com") + if err != nil { + t.Fatal("error creating account") + return + } + + const totalPeers = 1000 + + var wg sync.WaitGroup + errs := make(chan error, totalPeers) + start := make(chan struct{}) + for i := 0; i < totalPeers; i++ { + wg.Add(1) + + go func(i int) { + defer wg.Done() + + <-start + + err = manager.Store.AddPeerToAllGroup(context.Background(), accountID, strconv.Itoa(i)) + if err != nil { + errs <- fmt.Errorf("AddPeer failed for peer %d: %w", i, err) + return + } + + }(i) + } + startTime := time.Now() + + close(start) + wg.Wait() + close(errs) + + t.Logf("time since start: %s", time.Since(startTime)) + + for err := range errs { + t.Fatal(err) + } + + account, err := manager.Store.GetAccount(context.Background(), accountID) + if err != nil { + t.Fatalf("Failed to get account %s: %v", accountID, err) + } + + assert.Equal(t, totalPeers, len(maps.Values(account.Groups)[0].Peers), "Expected %d peers in group %s account %s, got %d", totalPeers, maps.Values(account.Groups)[0].Name, accountID, len(account.Peers)) +} + +func Test_AddPeerAndAddToAll(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + accountID := "testaccount" + userID := "testuser" + + _, err = createAccount(manager, accountID, userID, "domain.com") + if err != nil { + t.Fatal("error creating account") + return + } + + const totalPeers = 1000 + + var wg sync.WaitGroup + errs := make(chan error, totalPeers) + start := make(chan struct{}) + for i := 0; i < totalPeers; i++ { + wg.Add(1) + + go func(i int) { + defer wg.Done() + + <-start + + peer := &peer2.Peer{ + ID: strconv.Itoa(i), + AccountID: accountID, + DNSLabel: "peer" + strconv.Itoa(i), + IP: uint32ToIP(uint32(i)), + } + + err = manager.Store.ExecuteInTransaction(context.Background(), func(transaction store.Store) error { + err = transaction.AddPeerToAccount(context.Background(), store.LockingStrengthUpdate, peer) + if err != nil { + return fmt.Errorf("AddPeer failed for peer %d: %w", i, err) + } + err = transaction.AddPeerToAllGroup(context.Background(), accountID, peer.ID) + if err != nil { + return fmt.Errorf("AddPeer failed for peer %d: %w", i, err) + } + return nil + }) + if err != nil { + t.Errorf("AddPeer failed for peer %d: %v", i, err) + return + } + }(i) + } + startTime := time.Now() + + close(start) + wg.Wait() + close(errs) + + t.Logf("time since start: %s", time.Since(startTime)) + + for err := range errs { + t.Fatal(err) + } + + account, err := manager.Store.GetAccount(context.Background(), accountID) + if err != nil { + t.Fatalf("Failed to get account %s: %v", accountID, err) + } + + assert.Equal(t, totalPeers, len(maps.Values(account.Groups)[0].Peers), "Expected %d peers in group %s in account %s, got %d", totalPeers, maps.Values(account.Groups)[0].Name, accountID, len(account.Peers)) + assert.Equal(t, totalPeers, len(account.Peers), "Expected %d peers in account %s, got %d", totalPeers, accountID, len(account.Peers)) +} + +func uint32ToIP(n uint32) net.IP { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, n) + return ip +} + +func Test_IncrementNetworkSerial(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + accountID := "testaccount" + userID := "testuser" + + _, err = createAccount(manager, accountID, userID, "domain.com") + if err != nil { + t.Fatal("error creating account") + return + } + + const totalPeers = 1000 + + var wg sync.WaitGroup + errs := make(chan error, totalPeers) + start := make(chan struct{}) + for i := 0; i < totalPeers; i++ { + wg.Add(1) + + go func(i int) { + defer wg.Done() + + <-start + + err = manager.Store.ExecuteInTransaction(context.Background(), func(transaction store.Store) error { + err = transaction.IncrementNetworkSerial(context.Background(), store.LockingStrengthNone, accountID) + if err != nil { + return fmt.Errorf("failed to get account %s: %v", accountID, err) + } + return nil + }) + if err != nil { + t.Errorf("AddPeer failed for peer %d: %v", i, err) + return + } + }(i) + } + startTime := time.Now() + + close(start) + wg.Wait() + close(errs) + + t.Logf("time since start: %s", time.Since(startTime)) + + for err := range errs { + t.Fatal(err) + } + + account, err := manager.Store.GetAccount(context.Background(), accountID) + if err != nil { + t.Fatalf("Failed to get account %s: %v", accountID, err) + } + + assert.Equal(t, totalPeers, int(account.Network.Serial), "Expected %d serial increases in account %s, got %d", totalPeers, accountID, account.Network.Serial) +} diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index 3ae833dc0..bede652f5 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -143,7 +143,7 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) { IntegrationReference: existingGroup.IntegrationReference, } - if err := h.accountManager.SaveGroup(r.Context(), accountID, userID, &group, false); err != nil { + if err := h.accountManager.UpdateGroup(r.Context(), accountID, userID, &group); err != nil { log.WithContext(r.Context()).Errorf("failed updating group %s under account %s %v", groupID, accountID, err) util.WriteError(r.Context(), err, w) return @@ -203,7 +203,7 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) { Issued: types.GroupIssuedAPI, } - err = h.accountManager.SaveGroup(r.Context(), accountID, userID, &group, true) + err = h.accountManager.CreateGroup(r.Context(), accountID, userID, &group) if err != nil { util.WriteError(r.Context(), err, w) return diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go index c2f1a5abf..88af9a58f 100644 --- a/management/server/migration/migration.go +++ b/management/server/migration/migration.go @@ -39,6 +39,11 @@ func MigrateFieldFromGobToJSON[T any, S any](ctx context.Context, db *gorm.DB, f return nil } + if !db.Migrator().HasColumn(&model, fieldName) { + log.WithContext(ctx).Debugf("Table for %T does not have column %s, no migration needed", model, fieldName) + return nil + } + stmt := &gorm.Statement{DB: db} err := stmt.Parse(model) if err != nil { @@ -422,3 +427,62 @@ func CreateIndexIfNotExists[T any](ctx context.Context, db *gorm.DB, indexName s log.WithContext(ctx).Infof("successfully created index %s on table %s", indexName, tableName) return nil } + +func MigrateJsonToTable[T any](ctx context.Context, db *gorm.DB, columnName string, mapperFunc func(accountID string, id string, value string) any) error { + var model T + + if !db.Migrator().HasTable(&model) { + log.WithContext(ctx).Debugf("table for %T does not exist, no migration needed", model) + return nil + } + + stmt := &gorm.Statement{DB: db} + err := stmt.Parse(&model) + if err != nil { + return fmt.Errorf("parse model: %w", err) + } + tableName := stmt.Schema.Table + + if !db.Migrator().HasColumn(&model, columnName) { + log.WithContext(ctx).Debugf("column %s does not exist in table %s, no migration needed", columnName, tableName) + return nil + } + + if err := db.Transaction(func(tx *gorm.DB) error { + var rows []map[string]any + if err := tx.Table(tableName).Select("id", "account_id", columnName).Find(&rows).Error; err != nil { + return fmt.Errorf("find rows: %w", err) + } + + for _, row := range rows { + jsonValue, ok := row[columnName].(string) + if !ok || jsonValue == "" { + continue + } + + var data []string + if err := json.Unmarshal([]byte(jsonValue), &data); err != nil { + return fmt.Errorf("unmarshal json: %w", err) + } + + for _, value := range data { + if err := tx.Create( + mapperFunc(row["account_id"].(string), row["id"].(string), value), + ).Error; err != nil { + return fmt.Errorf("failed to insert id %v: %w", row["id"], err) + } + } + } + + if err := tx.Migrator().DropColumn(&model, columnName); err != nil { + return fmt.Errorf("drop column %s: %w", columnName, err) + } + + return nil + }); err != nil { + return err + } + + log.WithContext(ctx).Infof("Migration of JSON field %s from table %s into separate table completed", columnName, tableName) + return nil +} diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index a16e3652c..8c8fd19c9 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -124,6 +124,34 @@ type MockAccountManager struct { BufferUpdateAccountPeersFunc func(ctx context.Context, accountID string) } +func (am *MockAccountManager) CreateGroup(ctx context.Context, accountID, userID string, group *types.Group) error { + if am.SaveGroupFunc != nil { + return am.SaveGroupFunc(ctx, accountID, userID, group, true) + } + return status.Errorf(codes.Unimplemented, "method CreateGroup is not implemented") +} + +func (am *MockAccountManager) UpdateGroup(ctx context.Context, accountID, userID string, group *types.Group) error { + if am.SaveGroupFunc != nil { + return am.SaveGroupFunc(ctx, accountID, userID, group, false) + } + return status.Errorf(codes.Unimplemented, "method UpdateGroup is not implemented") +} + +func (am *MockAccountManager) CreateGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error { + if am.SaveGroupsFunc != nil { + return am.SaveGroupsFunc(ctx, accountID, userID, newGroups, true) + } + return status.Errorf(codes.Unimplemented, "method CreateGroups is not implemented") +} + +func (am *MockAccountManager) UpdateGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error { + if am.SaveGroupsFunc != nil { + return am.SaveGroupsFunc(ctx, accountID, userID, newGroups, false) + } + return status.Errorf(codes.Unimplemented, "method UpdateGroups is not implemented") +} + func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { if am.UpdateAccountPeersFunc != nil { am.UpdateAccountPeersFunc(ctx, accountID) diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 25eb03b83..959e7856a 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -980,18 +980,18 @@ func TestNameServerAccountPeersUpdate(t *testing.T) { var newNameServerGroupA *nbdns.NameServerGroup var newNameServerGroupB *nbdns.NameServerGroup - err := manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ - { - ID: "groupA", - Name: "GroupA", - Peers: []string{}, - }, - { - ID: "groupB", - Name: "GroupB", - Peers: []string{peer1.ID, peer2.ID, peer3.ID}, - }, - }, true) + err := manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ + ID: "groupA", + Name: "GroupA", + Peers: []string{}, + }) + assert.NoError(t, err) + + err = manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ + ID: "groupB", + Name: "GroupB", + Peers: []string{peer1.ID, peer2.ID, peer3.ID}, + }) assert.NoError(t, err) updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) diff --git a/management/server/peer.go b/management/server/peer.go index 3c40c6bb6..f954369d8 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -374,12 +374,20 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { - return err + if err = transaction.RemovePeerFromAllGroups(ctx, peer.ID); err != nil { + return fmt.Errorf("failed to remove peer from groups: %w", err) } eventsToStore, err = deletePeers(ctx, am, transaction, accountID, userID, []*nbpeer.Peer{peer}) - return err + if err != nil { + return fmt.Errorf("failed to delete peer: %w", err) + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return fmt.Errorf("failed to increment network serial: %w", err) + } + + return nil }) if err != nil { return err @@ -478,7 +486,6 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s } var newPeer *nbpeer.Peer - var updateAccountPeers bool var setupKeyID string var setupKeyName string @@ -615,20 +622,20 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s return err } - err = transaction.AddPeerToAllGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID) - if err != nil { - return fmt.Errorf("failed adding peer to All group: %w", err) - } - if len(groupsToAdd) > 0 { for _, g := range groupsToAdd { - err = transaction.AddPeerToGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID, g) + err = transaction.AddPeerToGroup(ctx, newPeer.AccountID, newPeer.ID, g) if err != nil { return err } } } + err = transaction.AddPeerToAllGroup(ctx, accountID, newPeer.ID) + if err != nil { + return fmt.Errorf("failed adding peer to All group: %w", err) + } + if addedByUser { err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin()) if err != nil { @@ -678,7 +685,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s return nil, nil, nil, fmt.Errorf("failed to add peer to database after %d attempts: %w", maxAttempts, err) } - updateAccountPeers, err = isPeerInActiveGroup(ctx, am.Store, accountID, newPeer.ID) + updateAccountPeers, err := isPeerInActiveGroup(ctx, am.Store, accountID, newPeer.ID) if err != nil { updateAccountPeers = true } @@ -1021,7 +1028,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is }() if isRequiresApproval { - network, err := am.Store.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID) + network, err := am.Store.GetAccountNetwork(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, nil, nil, err } @@ -1523,17 +1530,7 @@ func (am *DefaultAccountManager) GetPeerGroups(ctx context.Context, accountID, p // getPeerGroupIDs returns the IDs of the groups that the peer is part of. func getPeerGroupIDs(ctx context.Context, transaction store.Store, accountID string, peerID string) ([]string, error) { - groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthShare, accountID, peerID) - if err != nil { - return nil, err - } - - groupIDs := make([]string, 0, len(groups)) - for _, group := range groups { - groupIDs = append(groupIDs, group.ID) - } - - return groupIDs, err + return transaction.GetPeerGroupIDs(ctx, store.LockingStrengthShare, accountID, peerID) } // IsPeerInActiveGroup checks if the given peer is part of a group that is used @@ -1563,17 +1560,8 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto } for _, peer := range peers { - groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthUpdate, accountID, peer.ID) - if err != nil { - return nil, fmt.Errorf("failed to get peer groups: %w", err) - } - - for _, group := range groups { - group.RemovePeer(peer.ID) - err = transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) - if err != nil { - return nil, fmt.Errorf("failed to save group: %w", err) - } + if err := transaction.RemovePeerFromAllGroups(ctx, peer.ID); err != nil { + return nil, fmt.Errorf("failed to remove peer %s from groups", peer.ID) } if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID, settings.Extra); err != nil { diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 4f6ae500e..947e53a60 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -310,12 +310,12 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) { group1.Peers = append(group1.Peers, peer1.ID) group2.Peers = append(group2.Peers, peer2.ID) - err = manager.SaveGroup(context.Background(), account.Id, userID, &group1, true) + err = manager.CreateGroup(context.Background(), account.Id, userID, &group1) if err != nil { t.Errorf("expecting group1 to be added, got failure %v", err) return } - err = manager.SaveGroup(context.Background(), account.Id, userID, &group2, true) + err = manager.CreateGroup(context.Background(), account.Id, userID, &group2) if err != nil { t.Errorf("expecting group2 to be added, got failure %v", err) return @@ -1475,6 +1475,10 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { } func Test_RegisterPeerRollbackOnFailure(t *testing.T) { + engine := os.Getenv("NETBIRD_STORE_ENGINE") + if engine == "sqlite" || engine == "" { + t.Skip("Skipping test because sqlite test store is not respecting foreign keys") + } if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } @@ -1709,7 +1713,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) { err := manager.DeletePolicy(context.Background(), account.Id, account.Policies[0].ID, userID) require.NoError(t, err) - err = manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + g := []*types.Group{ { ID: "groupA", Name: "GroupA", @@ -1725,8 +1729,11 @@ func TestPeerAccountPeersUpdate(t *testing.T) { Name: "GroupC", Peers: []string{}, }, - }, true) - require.NoError(t, err) + } + for _, group := range g { + err = manager.CreateGroup(context.Background(), account.Id, userID, group) + require.NoError(t, err) + } // create a user with auto groups _, err = manager.SaveOrAddUsers(context.Background(), account.Id, userID, []*types.User{ @@ -1785,7 +1792,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) { t.Run("adding peer to unlinked group", func(t *testing.T) { done := make(chan struct{}) go func() { - peerShouldNotReceiveUpdate(t, updMsg) + peerShouldNotReceiveUpdate(t, updMsg) // close(done) }() @@ -2164,7 +2171,6 @@ func Test_IsUniqueConstraintError(t *testing.T) { } func Test_AddPeer(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(types.PostgresStoreEngine)) manager, err := createManager(t) if err != nil { t.Fatal(err) @@ -2176,7 +2182,7 @@ func Test_AddPeer(t *testing.T) { _, err = createAccount(manager, accountID, userID, "domain.com") if err != nil { - t.Fatal("error creating account") + t.Fatalf("error creating account: %v", err) return } @@ -2186,22 +2192,21 @@ func Test_AddPeer(t *testing.T) { return } - const totalPeers = 300 // totalPeers / differentHostnames should be less than 10 (due to concurrent retries) - const differentHostnames = 50 + const totalPeers = 300 var wg sync.WaitGroup - errs := make(chan error, totalPeers+differentHostnames) + errs := make(chan error, totalPeers) start := make(chan struct{}) for i := 0; i < totalPeers; i++ { wg.Add(1) - hostNameID := i % differentHostnames go func(i int) { defer wg.Done() newPeer := &nbpeer.Peer{ - Key: "key" + strconv.Itoa(i), - Meta: nbpeer.PeerSystemMeta{Hostname: "peer" + strconv.Itoa(hostNameID), GoOS: "linux"}, + AccountID: accountID, + Key: "key" + strconv.Itoa(i), + Meta: nbpeer.PeerSystemMeta{Hostname: "peer" + strconv.Itoa(i), GoOS: "linux"}, } <-start diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 4352f3cff..4a08f4c33 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -993,7 +993,7 @@ func sortFunc() func(a *types.FirewallRule, b *types.FirewallRule) int { func TestPolicyAccountPeersUpdate(t *testing.T) { manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) - err := manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + g := []*types.Group{ { ID: "groupA", Name: "GroupA", @@ -1014,8 +1014,11 @@ func TestPolicyAccountPeersUpdate(t *testing.T) { Name: "GroupD", Peers: []string{peer1.ID, peer2.ID}, }, - }, true) - assert.NoError(t, err) + } + for _, group := range g { + err := manager.CreateGroup(context.Background(), account.Id, userID, group) + assert.NoError(t, err) + } updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) t.Cleanup(func() { @@ -1025,6 +1028,7 @@ func TestPolicyAccountPeersUpdate(t *testing.T) { var policyWithGroupRulesNoPeers *types.Policy var policyWithDestinationPeersOnly *types.Policy var policyWithSourceAndDestinationPeers *types.Policy + var err error // Saving policy with rule groups with no peers should not update account's peers and not send peer update t.Run("saving policy with rule groups with no peers", func(t *testing.T) { diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go index f93467375..67760d55a 100644 --- a/management/server/posture_checks_test.go +++ b/management/server/posture_checks_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/posture" @@ -105,10 +105,14 @@ func initTestPostureChecksAccount(am *DefaultAccountManager) (*types.Account, er Id: regularUserID, Role: types.UserRoleUser, } + peer1 := &peer.Peer{ + ID: "peer1", + } account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, false) account.Users[admin.Id] = admin account.Users[user.Id] = user + account.Peers["peer1"] = peer1 err := am.Store.SaveAccount(context.Background(), account) if err != nil { @@ -121,7 +125,7 @@ func initTestPostureChecksAccount(am *DefaultAccountManager) (*types.Account, er func TestPostureCheckAccountPeersUpdate(t *testing.T) { manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) - err := manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + g := []*types.Group{ { ID: "groupA", Name: "GroupA", @@ -137,8 +141,11 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) { Name: "GroupC", Peers: []string{}, }, - }, true) - assert.NoError(t, err) + } + for _, group := range g { + err := manager.CreateGroup(context.Background(), account.Id, userID, group) + assert.NoError(t, err) + } updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1.ID) t.Cleanup(func() { @@ -156,7 +163,7 @@ func TestPostureCheckAccountPeersUpdate(t *testing.T) { }, }, } - postureCheckA, err = manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckA, true) + postureCheckA, err := manager.SavePostureChecks(context.Background(), account.Id, userID, postureCheckA, true) require.NoError(t, err) postureCheckB := &posture.Checks{ @@ -449,14 +456,16 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) { AccountID: account.Id, Peers: []string{"peer1"}, } + err = manager.CreateGroup(context.Background(), account.Id, adminUserID, groupA) + require.NoError(t, err, "failed to create groupA") groupB := &types.Group{ ID: "groupB", AccountID: account.Id, Peers: []string{}, } - err = manager.Store.SaveGroups(context.Background(), store.LockingStrengthUpdate, account.Id, []*types.Group{groupA, groupB}) - require.NoError(t, err, "failed to save groups") + err = manager.CreateGroup(context.Background(), account.Id, adminUserID, groupB) + require.NoError(t, err, "failed to create groupB") postureCheckA := &posture.Checks{ Name: "checkA", @@ -535,7 +544,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) { t.Run("posture check is linked to policy but no peers in groups", func(t *testing.T) { groupA.Peers = []string{} - err = manager.Store.SaveGroup(context.Background(), store.LockingStrengthUpdate, groupA) + err = manager.UpdateGroup(context.Background(), account.Id, adminUserID, groupA) require.NoError(t, err, "failed to save groups") result, err := arePostureCheckChangesAffectPeers(context.Background(), manager.Store, account.Id, postureCheckA.ID) diff --git a/management/server/route_test.go b/management/server/route_test.go index 37c37f624..ffd1a284b 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1215,7 +1215,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { Name: "peer1 group", Peers: []string{peer1ID}, } - err = am.SaveGroup(context.Background(), account.Id, userID, newGroup, true) + err = am.CreateGroup(context.Background(), account.Id, userID, newGroup) require.NoError(t, err) rules, err := am.ListPolicies(context.Background(), account.Id, "testingUser") @@ -1505,7 +1505,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou } for _, group := range newGroup { - err = am.SaveGroup(context.Background(), accountID, userID, group, true) + err = am.CreateGroup(context.Background(), accountID, userID, group) if err != nil { return nil, err } @@ -1953,7 +1953,7 @@ func TestRouteAccountPeersUpdate(t *testing.T) { account, err := initTestRouteAccount(t, manager) require.NoError(t, err, "failed to init testing account") - err = manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + g := []*types.Group{ { ID: "groupA", Name: "GroupA", @@ -1969,8 +1969,11 @@ func TestRouteAccountPeersUpdate(t *testing.T) { Name: "GroupC", Peers: []string{}, }, - }, true) - assert.NoError(t, err) + } + for _, group := range g { + err = manager.CreateGroup(context.Background(), account.Id, userID, group) + require.NoError(t, err, "failed to create group %s", group.Name) + } updMsg := manager.peersUpdateManager.CreateChannel(context.Background(), peer1ID) t.Cleanup(func() { @@ -2149,11 +2152,11 @@ func TestRouteAccountPeersUpdate(t *testing.T) { close(done) }() - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupB", Name: "GroupB", Peers: []string{peer1ID}, - }, true) + }) assert.NoError(t, err) select { @@ -2189,11 +2192,11 @@ func TestRouteAccountPeersUpdate(t *testing.T) { close(done) }() - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.UpdateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupC", Name: "GroupC", Peers: []string{peer1ID}, - }, true) + }) assert.NoError(t, err) select { diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index cecf55200..e55b33c94 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -29,7 +29,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { t.Fatal(err) } - err = manager.SaveGroups(context.Background(), account.Id, userID, []*types.Group{ + err = manager.CreateGroups(context.Background(), account.Id, userID, []*types.Group{ { ID: "group_1", Name: "group_name_1", @@ -40,7 +40,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { Name: "group_name_2", Peers: []string{}, }, - }, true) + }) if err != nil { t.Fatal(err) } @@ -104,20 +104,20 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { t.Fatal(err) } - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "group_1", Name: "group_name_1", Peers: []string{}, - }, true) + }) if err != nil { t.Fatal(err) } - err = manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err = manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "group_2", Name: "group_name_2", Peers: []string{}, - }, true) + }) if err != nil { t.Fatal(err) } @@ -398,11 +398,11 @@ func TestSetupKey_Copy(t *testing.T) { func TestSetupKeyAccountPeersUpdate(t *testing.T) { manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) - err := manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err := manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupA", Name: "GroupA", Peers: []string{peer1.ID, peer2.ID, peer3.ID}, - }, true) + }) assert.NoError(t, err) policy := &types.Policy{ diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index e380a7da7..c2f0dff6d 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -96,7 +96,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met return nil, fmt.Errorf("migratePreAuto: %w", err) } err = db.AutoMigrate( - &types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.Group{}, + &types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.Group{}, &types.GroupPeer{}, &types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, &installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{}, &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{}, @@ -186,6 +186,10 @@ func (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) erro generateAccountSQLTypes(account) + for _, group := range account.GroupsG { + group.StoreGroupPeers() + } + err := s.db.Transaction(func(tx *gorm.DB) error { result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id) if result.Error != nil { @@ -247,7 +251,8 @@ func generateAccountSQLTypes(account *types.Account) { for id, group := range account.Groups { group.ID = id - account.GroupsG = append(account.GroupsG, *group) + group.AccountID = account.Id + account.GroupsG = append(account.GroupsG, group) } for id, route := range account.Routes { @@ -449,25 +454,56 @@ func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, u return nil } -// SaveGroups saves the given list of groups to the database. -func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error { +// CreateGroups creates the given list of groups to the database. +func (s *SqlStore) CreateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error { if len(groups) == 0 { return nil } - result := s.db. - Clauses( - clause.Locking{Strength: string(lockStrength)}, - clause.OnConflict{ - Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}}, - UpdateAll: true, - }, - ). - Create(&groups) - if result.Error != nil { - return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error) + return s.db.Transaction(func(tx *gorm.DB) error { + result := tx. + Clauses( + clause.Locking{Strength: string(lockStrength)}, + clause.OnConflict{ + Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}}, + UpdateAll: true, + }, + ). + Omit(clause.Associations). + Create(&groups) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save groups to store: %v", result.Error) + return status.Errorf(status.Internal, "failed to save groups to store") + } + + return nil + }) +} + +// UpdateGroups updates the given list of groups to the database. +func (s *SqlStore) UpdateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error { + if len(groups) == 0 { + return nil } - return nil + + return s.db.Transaction(func(tx *gorm.DB) error { + result := tx. + Clauses( + clause.Locking{Strength: string(lockStrength)}, + clause.OnConflict{ + Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}}, + UpdateAll: true, + }, + ). + Omit(clause.Associations). + Create(&groups) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save groups to store: %v", result.Error) + return status.Errorf(status.Internal, "failed to save groups to store") + } + + return nil + }) } // DeleteHashedPAT2TokenIDIndex is noop in SqlStore @@ -646,7 +682,7 @@ func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStr } var groups []*types.Group - result := tx.Find(&groups, accountIDCondition, accountID) + result := tx.Preload(clause.Associations).Find(&groups, accountIDCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "accountID not found: index lookup failed") @@ -655,6 +691,10 @@ func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStr return nil, status.Errorf(status.Internal, "failed to get account groups from the store") } + for _, g := range groups { + g.LoadGroupPeers() + } + return groups, nil } @@ -669,6 +709,7 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt likePattern := `%"ID":"` + resourceID + `"%` result := tx. + Preload(clause.Associations). Where("resources LIKE ?", likePattern). Find(&groups) @@ -679,6 +720,10 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt return nil, result.Error } + for _, g := range groups { + g.LoadGroupPeers() + } + return groups, nil } @@ -765,6 +810,7 @@ func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Acc var account types.Account result := s.db.Model(&account). + Omit("GroupsG"). Preload("UsersG.PATsG"). // have to be specifies as this is nester reference Preload(clause.Associations). First(&account, idQueryCondition, accountID) @@ -814,6 +860,17 @@ func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Acc } account.GroupsG = nil + var groupPeers []types.GroupPeer + s.db.Model(&types.GroupPeer{}).Where("account_id = ?", accountID). + Find(&groupPeers) + for _, groupPeer := range groupPeers { + if group, ok := account.Groups[groupPeer.GroupID]; ok { + group.Peers = append(group.Peers, groupPeer.PeerID) + } else { + log.WithContext(ctx).Warnf("group %s not found for group peer %s in account %s", groupPeer.GroupID, groupPeer.PeerID, accountID) + } + } + account.Routes = make(map[route.ID]*route.Route, len(account.RoutesG)) for _, route := range account.RoutesG { account.Routes[route.ID] = route.Copy() @@ -1311,55 +1368,76 @@ func (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string } // AddPeerToAllGroup adds a peer to the 'All' group. Method always needs to run in a transaction -func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error { - var group types.Group - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - First(&group, "account_id = ? AND name = ?", accountID, "All") - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return status.Errorf(status.NotFound, "group 'All' not found for account") - } - return status.Errorf(status.Internal, "issue finding group 'All': %s", result.Error) +func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error { + var groupID string + _ = s.db.Model(types.Group{}). + Select("id"). + Where("account_id = ? AND name = ?", accountID, "All"). + Limit(1). + Scan(&groupID) + + if groupID == "" { + return status.Errorf(status.NotFound, "group 'All' not found for account %s", accountID) } - for _, existingPeerID := range group.Peers { - if existingPeerID == peerID { - return nil - } - } + err := s.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "group_id"}, {Name: "peer_id"}}, + DoNothing: true, + }).Create(&types.GroupPeer{ + AccountID: accountID, + GroupID: groupID, + PeerID: peerID, + }).Error - group.Peers = append(group.Peers, peerID) - - if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&group).Error; err != nil { - return status.Errorf(status.Internal, "issue updating group 'All': %s", err) + if err != nil { + return status.Errorf(status.Internal, "error adding peer to group 'All': %v", err) } return nil } -// AddPeerToGroup adds a peer to a group. Method always needs to run in a transaction -func (s *SqlStore) AddPeerToGroup(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string, groupID string) error { - var group types.Group - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Where(accountAndIDQueryCondition, accountId, groupID). - First(&group) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return status.NewGroupNotFoundError(groupID) - } - - return status.Errorf(status.Internal, "issue finding group: %s", result.Error) +// AddPeerToGroup adds a peer to a group +func (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupID string) error { + peer := &types.GroupPeer{ + AccountID: accountID, + GroupID: groupID, + PeerID: peerID, } - for _, existingPeerID := range group.Peers { - if existingPeerID == peerId { - return nil - } + err := s.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "group_id"}, {Name: "peer_id"}}, + DoNothing: true, + }).Create(peer).Error + + if err != nil { + log.WithContext(ctx).Errorf("failed to add peer %s to group %s for account %s: %v", peerID, groupID, accountID, err) + return status.Errorf(status.Internal, "failed to add peer to group") } - group.Peers = append(group.Peers, peerId) + return nil +} - if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&group).Error; err != nil { - return status.Errorf(status.Internal, "issue updating group: %s", err) +// RemovePeerFromGroup removes a peer from a group +func (s *SqlStore) RemovePeerFromGroup(ctx context.Context, peerID string, groupID string) error { + err := s.db.WithContext(ctx). + Delete(&types.GroupPeer{}, "group_id = ? AND peer_id = ?", groupID, peerID).Error + + if err != nil { + log.WithContext(ctx).Errorf("failed to remove peer %s from group %s: %v", peerID, groupID, err) + return status.Errorf(status.Internal, "failed to remove peer from group") + } + + return nil +} + +// RemovePeerFromAllGroups removes a peer from all groups +func (s *SqlStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) error { + err := s.db.WithContext(ctx). + Delete(&types.GroupPeer{}, "peer_id = ?", peerID).Error + + if err != nil { + log.WithContext(ctx).Errorf("failed to remove peer %s from all groups: %v", peerID, err) + return status.Errorf(status.Internal, "failed to remove peer from all groups") } return nil @@ -1427,15 +1505,46 @@ func (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStreng var groups []*types.Group query := tx. - Find(&groups, "account_id = ? AND peers LIKE ?", accountId, fmt.Sprintf(`%%"%s"%%`, peerId)) + Joins("JOIN group_peers ON group_peers.group_id = groups.id"). + Where("group_peers.peer_id = ?", peerId). + Preload(clause.Associations). + Find(&groups) if query.Error != nil { return nil, query.Error } + for _, group := range groups { + group.LoadGroupPeers() + } + return groups, nil } +// GetPeerGroupIDs retrieves all group IDs assigned to a specific peer in a given account. +func (s *SqlStore) GetPeerGroupIDs(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]string, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + + var groupIDs []string + query := tx. + Model(&types.GroupPeer{}). + Where("account_id = ? AND peer_id = ?", accountId, peerId). + Pluck("group_id", &groupIDs) + + if query.Error != nil { + if errors.Is(query.Error, gorm.ErrRecordNotFound) { + return nil, status.Errorf(status.NotFound, "no groups found for peer %s in account %s", peerId, accountId) + } + log.WithContext(ctx).Errorf("failed to get group IDs for peer %s in account %s: %v", peerId, accountId, query.Error) + return nil, status.Errorf(status.Internal, "failed to get group IDs for peer from store") + } + + return groupIDs, nil +} + // GetAccountPeers retrieves peers for an account. func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) { var peers []*nbpeer.Peer @@ -1485,7 +1594,7 @@ func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrengt } func (s *SqlStore) AddPeerToAccount(ctx context.Context, lockStrength LockingStrength, peer *nbpeer.Peer) error { - if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Create(peer).Error; err != nil { + if err := s.db.Create(peer).Error; err != nil { return status.Errorf(status.Internal, "issue adding peer to account: %s", err) } @@ -1722,7 +1831,7 @@ func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrengt } var group *types.Group - result := tx.First(&group, accountAndIDQueryCondition, accountID, groupID) + result := tx.Preload(clause.Associations).First(&group, accountAndIDQueryCondition, accountID, groupID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewGroupNotFoundError(groupID) @@ -1731,15 +1840,14 @@ func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrengt return nil, status.Errorf(status.Internal, "failed to get group from store") } + group.LoadGroupPeers() + return group, nil } // GetGroupByName retrieves a group by name and account ID. func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error) { tx := s.db - if lockStrength != LockingStrengthNone { - tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) - } var group types.Group @@ -1747,16 +1855,14 @@ func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStren // we may need to reconsider changing the types. query := tx.Preload(clause.Associations) - switch s.storeEngine { - case types.PostgresStoreEngine: - query = query.Order("json_array_length(peers::json) DESC") - case types.MysqlStoreEngine: - query = query.Order("JSON_LENGTH(JSON_EXTRACT(peers, \"$\")) DESC") - default: - query = query.Order("json_array_length(peers) DESC") - } - - result := query.First(&group, "account_id = ? AND name = ?", accountID, groupName) + result := query. + Model(&types.Group{}). + Joins("LEFT JOIN group_peers ON group_peers.group_id = groups.id"). + Where("groups.account_id = ? AND groups.name = ?", accountID, groupName). + Group("groups.id"). + Order("COUNT(group_peers.peer_id) DESC"). + Limit(1). + First(&group) if err := result.Error; err != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewGroupNotFoundError(groupName) @@ -1764,6 +1870,9 @@ func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStren log.WithContext(ctx).Errorf("failed to get group by name from store: %v", result.Error) return nil, status.Errorf(status.Internal, "failed to get group by name from store") } + + group.LoadGroupPeers() + return &group, nil } @@ -1775,7 +1884,7 @@ func (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStren } var groups []*types.Group - result := tx.Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs) + result := tx.Preload(clause.Associations).Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs) if result.Error != nil { log.WithContext(ctx).Errorf("failed to get groups by ID's from store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get groups by ID's from store") @@ -1783,25 +1892,45 @@ func (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStren groupsMap := make(map[string]*types.Group) for _, group := range groups { + group.LoadGroupPeers() groupsMap[group.ID] = group } return groupsMap, nil } -// SaveGroup saves a group to the store. -func (s *SqlStore) SaveGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(group) - if result.Error != nil { - log.WithContext(ctx).Errorf("failed to save group to store: %v", result.Error) +// CreateGroup creates a group in the store. +func (s *SqlStore) CreateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error { + if group == nil { + return status.Errorf(status.InvalidArgument, "group is nil") + } + + if err := s.db.Omit(clause.Associations).Create(group).Error; err != nil { + log.WithContext(ctx).Errorf("failed to save group to store: %v", err) return status.Errorf(status.Internal, "failed to save group to store") } + + return nil +} + +// UpdateGroup updates a group in the store. +func (s *SqlStore) UpdateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error { + if group == nil { + return status.Errorf(status.InvalidArgument, "group is nil") + } + + if err := s.db.Omit(clause.Associations).Save(group).Error; err != nil { + log.WithContext(ctx).Errorf("failed to save group to store: %v", err) + return status.Errorf(status.Internal, "failed to save group to store") + } + return nil } // DeleteGroup deletes a group from the database. func (s *SqlStore) DeleteGroup(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) error { result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Select(clause.Associations). Delete(&types.Group{}, accountAndIDQueryCondition, accountID, groupID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to delete group from store: %s", result.Error) @@ -1818,6 +1947,7 @@ func (s *SqlStore) DeleteGroup(ctx context.Context, lockStrength LockingStrength // DeleteGroups deletes groups from the database. func (s *SqlStore) DeleteGroups(ctx context.Context, strength LockingStrength, accountID string, groupIDs []string) error { result := s.db.Clauses(clause.Locking{Strength: string(strength)}). + Select(clause.Associations). Delete(&types.Group{}, accountAndIDsQueryCondition, accountID, groupIDs) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete groups from store: %v", result.Error) @@ -2613,3 +2743,27 @@ func (s *SqlStore) CountAccountsByPrivateDomain(ctx context.Context, domain stri return count, nil } + +func (s *SqlStore) GetAccountGroupPeers(ctx context.Context, lockStrength LockingStrength, accountID string) (map[string]map[string]struct{}, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + + var peers []types.GroupPeer + result := tx.Find(&peers, accountIDCondition, accountID) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to get account group peers from store: %s", result.Error) + return nil, status.Errorf(status.Internal, "failed to get account group peers from store") + } + + groupPeers := make(map[string]map[string]struct{}) + for _, peer := range peers { + if _, exists := groupPeers[peer.GroupID]; !exists { + groupPeers[peer.GroupID] = make(map[string]struct{}) + } + groupPeers[peer.GroupID][peer.PeerID] = struct{}{} + } + + return groupPeers, nil +} diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 738c5a28c..44bb3f599 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" b64 "encoding/base64" + "encoding/binary" "fmt" "math/rand" "net" @@ -1187,7 +1188,7 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) { Peers: nil, } err = store.ExecuteInTransaction(context.Background(), func(transaction Store) error { - err := transaction.SaveGroup(context.Background(), LockingStrengthUpdate, group) + err := transaction.CreateGroup(context.Background(), LockingStrengthUpdate, group) if err != nil { t.Fatal("failed to save group") return err @@ -1348,7 +1349,8 @@ func TestSqlStore_GetGroupsByIDs(t *testing.T) { } } -func TestSqlStore_SaveGroup(t *testing.T) { +func TestSqlStore_CreateGroup(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", string(types.MysqlStoreEngine)) store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) require.NoError(t, err) @@ -1356,12 +1358,14 @@ func TestSqlStore_SaveGroup(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" group := &types.Group{ - ID: "group-id", - AccountID: accountID, - Issued: "api", - Peers: []string{"peer1", "peer2"}, + ID: "group-id", + AccountID: accountID, + Issued: "api", + Peers: []string{}, + Resources: []types.Resource{}, + GroupPeers: []types.GroupPeer{}, } - err = store.SaveGroup(context.Background(), LockingStrengthUpdate, group) + err = store.CreateGroup(context.Background(), LockingStrengthUpdate, group) require.NoError(t, err) savedGroup, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, "group-id") @@ -1369,7 +1373,7 @@ func TestSqlStore_SaveGroup(t *testing.T) { require.Equal(t, savedGroup, group) } -func TestSqlStore_SaveGroups(t *testing.T) { +func TestSqlStore_CreateUpdateGroups(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) require.NoError(t, err) @@ -1378,23 +1382,27 @@ func TestSqlStore_SaveGroups(t *testing.T) { groups := []*types.Group{ { - ID: "group-1", - AccountID: accountID, - Issued: "api", - Peers: []string{"peer1", "peer2"}, + ID: "group-1", + AccountID: accountID, + Issued: "api", + Peers: []string{}, + Resources: []types.Resource{}, + GroupPeers: []types.GroupPeer{}, }, { - ID: "group-2", - AccountID: accountID, - Issued: "integration", - Peers: []string{"peer3", "peer4"}, + ID: "group-2", + AccountID: accountID, + Issued: "integration", + Peers: []string{}, + Resources: []types.Resource{}, + GroupPeers: []types.GroupPeer{}, }, } - err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groups) + err = store.CreateGroups(context.Background(), LockingStrengthUpdate, accountID, groups) require.NoError(t, err) groups[1].Peers = []string{} - err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groups) + err = store.UpdateGroups(context.Background(), LockingStrengthUpdate, accountID, groups) require.NoError(t, err) group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID) @@ -2523,7 +2531,7 @@ func TestSqlStore_AddPeerToGroup(t *testing.T) { require.NoError(t, err, "failed to get group") require.Len(t, group.Peers, 0, "group should have 0 peers") - err = store.AddPeerToGroup(context.Background(), LockingStrengthUpdate, accountID, peerID, groupID) + err = store.AddPeerToGroup(context.Background(), accountID, peerID, groupID) require.NoError(t, err, "failed to add peer to group") group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) @@ -2554,7 +2562,7 @@ func TestSqlStore_AddPeerToAllGroup(t *testing.T) { err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) require.NoError(t, err, "failed to add peer to account") - err = store.AddPeerToAllGroup(context.Background(), LockingStrengthUpdate, accountID, peer.ID) + err = store.AddPeerToAllGroup(context.Background(), accountID, peer.ID) require.NoError(t, err, "failed to add peer to all group") group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) @@ -2640,7 +2648,7 @@ func TestSqlStore_GetPeerGroups(t *testing.T) { assert.Len(t, groups, 1) assert.Equal(t, groups[0].Name, "All") - err = store.AddPeerToGroup(context.Background(), LockingStrengthUpdate, accountID, peerID, "cfefqs706sqkneg59g4h") + err = store.AddPeerToGroup(context.Background(), accountID, peerID, "cfefqs706sqkneg59g4h") require.NoError(t, err) groups, err = store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peerID) @@ -3307,7 +3315,7 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { }) } - err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groupsToSave) + err = store.CreateGroups(context.Background(), LockingStrengthUpdate, accountID, groupsToSave) require.NoError(t, err) accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) @@ -3538,3 +3546,64 @@ func TestSqlStore_GetAnyAccountID(t *testing.T) { assert.Empty(t, accountID) }) } + +func BenchmarkGetAccountPeers(b *testing.B) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", b.TempDir()) + if err != nil { + b.Fatal(err) + } + b.Cleanup(cleanup) + + numberOfPeers := 1000 + numberOfGroups := 200 + numberOfPeersPerGroup := 500 + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + peers := make([]*nbpeer.Peer, 0, numberOfPeers) + for i := 0; i < numberOfPeers; i++ { + peer := &nbpeer.Peer{ + ID: fmt.Sprintf("peer-%d", i), + AccountID: accountID, + DNSLabel: fmt.Sprintf("peer%d.example.com", i), + IP: intToIPv4(uint32(i)), + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) + if err != nil { + b.Fatalf("Failed to add peer: %v", err) + } + peers = append(peers, peer) + } + + for i := 0; i < numberOfGroups; i++ { + groupID := fmt.Sprintf("group-%d", i) + group := &types.Group{ + ID: groupID, + AccountID: accountID, + } + err = store.CreateGroup(context.Background(), LockingStrengthUpdate, group) + if err != nil { + b.Fatalf("Failed to create group: %v", err) + } + for j := 0; j < numberOfPeersPerGroup; j++ { + peerIndex := (i*numberOfPeersPerGroup + j) % numberOfPeers + err = store.AddPeerToGroup(context.Background(), accountID, peers[peerIndex].ID, groupID) + if err != nil { + b.Fatalf("Failed to add peer to group: %v", err) + } + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peers[i%numberOfPeers].ID) + if err != nil { + b.Fatal(err) + } + } +} + +func intToIPv4(n uint32) net.IP { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, n) + return ip +} diff --git a/management/server/store/store.go b/management/server/store/store.go index b3254c4c9..912939bc2 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -101,8 +101,10 @@ type Store interface { GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types.Group, error) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) - SaveGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error - SaveGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error + CreateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error + UpdateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error + CreateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error + UpdateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error DeleteGroup(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) error DeleteGroups(ctx context.Context, strength LockingStrength, accountID string, groupIDs []string) error @@ -120,9 +122,12 @@ type Store interface { DeletePostureChecks(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) error GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountId string, hostname string) ([]string, error) - AddPeerToAllGroup(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error - AddPeerToGroup(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string, groupID string) error + AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error + AddPeerToGroup(ctx context.Context, accountID, peerId string, groupID string) error + RemovePeerFromGroup(ctx context.Context, peerID string, groupID string) error + RemovePeerFromAllGroups(ctx context.Context, peerID string) error GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) + GetPeerGroupIDs(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]string, error) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error AddPeerToAccount(ctx context.Context, lockStrength LockingStrength, peer *nbpeer.Peer) error @@ -196,6 +201,7 @@ type Store interface { DeleteNetworkResource(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) error GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) + GetAccountGroupPeers(ctx context.Context, lockStrength LockingStrength, accountID string) (map[string]map[string]struct{}, error) } const ( @@ -353,6 +359,15 @@ func getMigrationsPostAuto(ctx context.Context) []migrationFunc { func(db *gorm.DB) error { return migration.CreateIndexIfNotExists[nbpeer.Peer](ctx, db, "idx_account_dnslabel", "account_id", "dns_label") }, + func(db *gorm.DB) error { + return migration.MigrateJsonToTable[types.Group](ctx, db, "peers", func(accountID, id, value string) any { + return &types.GroupPeer{ + AccountID: accountID, + GroupID: id, + PeerID: value, + } + }) + }, } } diff --git a/management/server/types/account.go b/management/server/types/account.go index f0887be07..a3a7ce305 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -73,7 +73,7 @@ type Account struct { Users map[string]*User `gorm:"-"` UsersG []User `json:"-" gorm:"foreignKey:AccountID;references:id"` Groups map[string]*Group `gorm:"-"` - GroupsG []Group `json:"-" gorm:"foreignKey:AccountID;references:id"` + GroupsG []*Group `json:"-" gorm:"foreignKey:AccountID;references:id"` Policies []*Policy `gorm:"foreignKey:AccountID;references:id"` Routes map[route.ID]*route.Route `gorm:"-"` RoutesG []route.Route `json:"-" gorm:"foreignKey:AccountID;references:id"` diff --git a/management/server/types/group.go b/management/server/types/group.go index 1b321387c..00fdf7a69 100644 --- a/management/server/types/group.go +++ b/management/server/types/group.go @@ -26,7 +26,8 @@ type Group struct { Issued string // Peers list of the group - Peers []string `gorm:"serializer:json"` + Peers []string `gorm:"-"` // Peers and GroupPeers list will be ignored when writing to the DB. Use AddPeerToGroup and RemovePeerFromGroup methods to modify group membership + GroupPeers []GroupPeer `gorm:"foreignKey:GroupID;references:id;constraint:OnDelete:CASCADE;"` // Resources contains a list of resources in that group Resources []Resource `gorm:"serializer:json"` @@ -34,6 +35,32 @@ type Group struct { IntegrationReference integration_reference.IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"` } +type GroupPeer struct { + AccountID string `gorm:"index"` + GroupID string `gorm:"primaryKey"` + PeerID string `gorm:"primaryKey"` +} + +func (g *Group) LoadGroupPeers() { + g.Peers = make([]string, len(g.GroupPeers)) + for i, peer := range g.GroupPeers { + g.Peers[i] = peer.PeerID + } + g.GroupPeers = []GroupPeer{} +} + +func (g *Group) StoreGroupPeers() { + g.GroupPeers = make([]GroupPeer, len(g.Peers)) + for i, peer := range g.Peers { + g.GroupPeers[i] = GroupPeer{ + AccountID: g.AccountID, + GroupID: g.ID, + PeerID: peer, + } + } + g.Peers = []string{} +} + // EventMeta returns activity event meta related to the group func (g *Group) EventMeta() map[string]any { return map[string]any{"name": g.Name} @@ -46,13 +73,16 @@ func (g *Group) EventMetaResource(resource *types.NetworkResource) map[string]an func (g *Group) Copy() *Group { group := &Group{ ID: g.ID, + AccountID: g.AccountID, Name: g.Name, Issued: g.Issued, Peers: make([]string, len(g.Peers)), + GroupPeers: make([]GroupPeer, len(g.GroupPeers)), Resources: make([]Resource, len(g.Resources)), IntegrationReference: g.IntegrationReference, } copy(group.Peers, g.Peers) + copy(group.GroupPeers, g.GroupPeers) copy(group.Resources, g.Resources) return group } diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go index 69b381ae5..3d421342d 100644 --- a/management/server/types/setupkey.go +++ b/management/server/types/setupkey.go @@ -35,7 +35,7 @@ type SetupKey struct { // AccountID is a reference to Account that this object belongs AccountID string `json:"-" gorm:"index"` Key string - KeySecret string + KeySecret string `gorm:"index"` Name string Type SetupKeyType CreatedAt time.Time diff --git a/management/server/user.go b/management/server/user.go index 7d8382978..a0f4c4a6c 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -677,13 +677,18 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact if update.AutoGroups != nil && settings.GroupsPropagationEnabled { removedGroups := util.Difference(oldUser.AutoGroups, update.AutoGroups) - updatedGroups, err := updateUserPeersInGroups(groupsMap, userPeers, update.AutoGroups, removedGroups) - if err != nil { - return false, nil, nil, nil, fmt.Errorf("error modifying user peers in groups: %w", err) - } - - if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, updatedGroups); err != nil { - return false, nil, nil, nil, fmt.Errorf("error saving groups: %w", err) + addedGroups := util.Difference(update.AutoGroups, oldUser.AutoGroups) + for _, peer := range userPeers { + for _, groupID := range removedGroups { + if err := transaction.RemovePeerFromGroup(ctx, peer.ID, groupID); err != nil { + return false, nil, nil, nil, fmt.Errorf("failed to remove peer %s from group %s: %w", peer.ID, groupID, err) + } + } + for _, groupID := range addedGroups { + if err := transaction.AddPeerToGroup(ctx, accountID, peer.ID, groupID); err != nil { + return false, nil, nil, nil, fmt.Errorf("failed to add peer %s to group %s: %w", peer.ID, groupID, err) + } + } } } @@ -1137,93 +1142,6 @@ func (am *DefaultAccountManager) GetOwnerInfo(ctx context.Context, accountID str return userInfo, nil } -// updateUserPeersInGroups updates the user's peers in the specified groups by adding or removing them. -func updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbpeer.Peer, groupsToAdd, groupsToRemove []string) (groupsToUpdate []*types.Group, err error) { - if len(groupsToAdd) == 0 && len(groupsToRemove) == 0 { - return - } - - userPeerIDMap := make(map[string]struct{}, len(peers)) - for _, peer := range peers { - userPeerIDMap[peer.ID] = struct{}{} - } - - for _, gid := range groupsToAdd { - group, ok := accountGroups[gid] - if !ok { - return nil, errors.New("group not found") - } - if changed := addUserPeersToGroup(userPeerIDMap, group); changed { - groupsToUpdate = append(groupsToUpdate, group) - } - } - - for _, gid := range groupsToRemove { - group, ok := accountGroups[gid] - if !ok { - return nil, errors.New("group not found") - } - if changed := removeUserPeersFromGroup(userPeerIDMap, group); changed { - groupsToUpdate = append(groupsToUpdate, group) - } - } - - return groupsToUpdate, nil -} - -// addUserPeersToGroup adds the user's peers to the group. -func addUserPeersToGroup(userPeerIDs map[string]struct{}, group *types.Group) bool { - groupPeers := make(map[string]struct{}, len(group.Peers)) - for _, pid := range group.Peers { - groupPeers[pid] = struct{}{} - } - - changed := false - for pid := range userPeerIDs { - if _, exists := groupPeers[pid]; !exists { - groupPeers[pid] = struct{}{} - changed = true - } - } - - group.Peers = make([]string, 0, len(groupPeers)) - for pid := range groupPeers { - group.Peers = append(group.Peers, pid) - } - - if changed { - group.Peers = make([]string, 0, len(groupPeers)) - for pid := range groupPeers { - group.Peers = append(group.Peers, pid) - } - } - return changed -} - -// removeUserPeersFromGroup removes user's peers from the group. -func removeUserPeersFromGroup(userPeerIDs map[string]struct{}, group *types.Group) bool { - // skip removing peers from group All - if group.Name == "All" { - return false - } - - updatedPeers := make([]string, 0, len(group.Peers)) - changed := false - - for _, pid := range group.Peers { - if _, owned := userPeerIDs[pid]; owned { - changed = true - continue - } - updatedPeers = append(updatedPeers, pid) - } - - if changed { - group.Peers = updatedPeers - } - return changed -} - func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserData, bool) { for _, user := range userData { if user.ID == userID { diff --git a/management/server/user_test.go b/management/server/user_test.go index 53baf8f7e..8ab6584cf 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -1335,11 +1335,11 @@ func TestUserAccountPeersUpdate(t *testing.T) { // account groups propagation is enabled manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) - err := manager.SaveGroup(context.Background(), account.Id, userID, &types.Group{ + err := manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{ ID: "groupA", Name: "GroupA", Peers: []string{peer1.ID, peer2.ID, peer3.ID}, - }, true) + }) require.NoError(t, err) policy := &types.Policy{ From b5ed94808c18e10fdb3b41ad6e33b57966eb2ea6 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:17:36 +0200 Subject: [PATCH 190/213] [management, client] Add logout feature (#4268) --- client/cmd/logout.go | 57 +++++ client/cmd/profile.go | 12 +- client/cmd/root.go | 1 + .../internal/profilemanager/profilemanager.go | 3 +- client/proto/daemon.pb.go | 198 ++++++++++---- client/proto/daemon.proto | 12 +- client/proto/daemon_grpc.pb.go | 38 +++ client/server/server.go | 242 ++++++++++++++---- client/ui/client_ui.go | 1 + client/ui/event_handler.go | 17 ++ client/ui/profile.go | 108 +++++++- management/client/client.go | 1 + management/client/grpc.go | 26 ++ management/client/mock.go | 8 + management/proto/management.pb.go | 30 ++- management/proto/management.proto | 3 + management/proto/management_grpc.pb.go | 38 +++ management/server/grpcserver.go | 40 +++ 18 files changed, 710 insertions(+), 125 deletions(-) create mode 100644 client/cmd/logout.go diff --git a/client/cmd/logout.go b/client/cmd/logout.go new file mode 100644 index 000000000..071be5ca9 --- /dev/null +++ b/client/cmd/logout.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "context" + "fmt" + "os/user" + "time" + + "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/client/proto" +) + +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "logout from the Netbird Management Service and delete peer", + RunE: func(cmd *cobra.Command, args []string) error { + SetFlagsFromEnvVars(rootCmd) + + cmd.SetOut(cmd.OutOrStdout()) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) + defer cancel() + + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return fmt.Errorf("connect to daemon: %v", err) + } + defer conn.Close() + + daemonClient := proto.NewDaemonServiceClient(conn) + + req := &proto.LogoutRequest{} + + if profileName != "" { + req.ProfileName = &profileName + + currUser, err := user.Current() + if err != nil { + return fmt.Errorf("get current user: %v", err) + } + username := currUser.Username + req.Username = &username + } + + if _, err := daemonClient.Logout(ctx, req); err != nil { + return fmt.Errorf("logout: %v", err) + } + + cmd.Println("Logged out successfully") + return nil + }, +} + +func init() { + logoutCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) +} diff --git a/client/cmd/profile.go b/client/cmd/profile.go index f32e9c844..d420dcbd9 100644 --- a/client/cmd/profile.go +++ b/client/cmd/profile.go @@ -3,9 +3,8 @@ package cmd import ( "context" "fmt" - "time" - "os/user" + "time" "github.com/spf13/cobra" @@ -22,10 +21,11 @@ var profileCmd = &cobra.Command{ } var profileListCmd = &cobra.Command{ - Use: "list", - Short: "list all profiles", - Long: `List all available profiles in the Netbird client.`, - RunE: listProfilesFunc, + Use: "list", + Short: "list all profiles", + Long: `List all available profiles in the Netbird client.`, + Aliases: []string{"ls"}, + RunE: listProfilesFunc, } var profileAddCmd = &cobra.Command{ diff --git a/client/cmd/root.go b/client/cmd/root.go index e3ce79964..6a8ae27f4 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -133,6 +133,7 @@ func init() { rootCmd.AddCommand(downCmd) rootCmd.AddCommand(statusCmd) rootCmd.AddCommand(loginCmd) + rootCmd.AddCommand(logoutCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(sshCmd) rootCmd.AddCommand(networksCMD) diff --git a/client/internal/profilemanager/profilemanager.go b/client/internal/profilemanager/profilemanager.go index 4598af33e..fe0afae2b 100644 --- a/client/internal/profilemanager/profilemanager.go +++ b/client/internal/profilemanager/profilemanager.go @@ -13,7 +13,8 @@ import ( ) const ( - defaultProfileName = "default" + DefaultProfileName = "default" + defaultProfileName = DefaultProfileName // Keep for backward compatibility activeProfileStateFilename = "active_profile.txt" ) diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index f405ffd65..7d5ddc8a9 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -4342,6 +4342,94 @@ func (x *GetActiveProfileResponse) GetUsername() string { return "" } +type LogoutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` + Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogoutRequest) Reset() { + *x = LogoutRequest{} + mi := &file_daemon_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogoutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogoutRequest) ProtoMessage() {} + +func (x *LogoutRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. +func (*LogoutRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{65} +} + +func (x *LogoutRequest) GetProfileName() string { + if x != nil && x.ProfileName != nil { + return *x.ProfileName + } + return "" +} + +func (x *LogoutRequest) GetUsername() string { + if x != nil && x.Username != nil { + return *x.Username + } + return "" +} + +type LogoutResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogoutResponse) Reset() { + *x = LogoutResponse{} + mi := &file_daemon_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogoutResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogoutResponse) ProtoMessage() {} + +func (x *LogoutResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. +func (*LogoutResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{66} +} + type PortInfo_Range struct { state protoimpl.MessageState `protogen:"open.v1"` Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` @@ -4352,7 +4440,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4364,7 +4452,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4778,7 +4866,13 @@ const file_daemon_proto_rawDesc = "" + "\x17GetActiveProfileRequest\"X\n" + "\x18GetActiveProfileResponse\x12 \n" + "\vprofileName\x18\x01 \x01(\tR\vprofileName\x12\x1a\n" + - "\busername\x18\x02 \x01(\tR\busername*b\n" + + "\busername\x18\x02 \x01(\tR\busername\"t\n" + + "\rLogoutRequest\x12%\n" + + "\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" + + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + + "\f_profileNameB\v\n" + + "\t_username\"\x10\n" + + "\x0eLogoutResponse*b\n" + "\bLogLevel\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05PANIC\x10\x01\x12\t\n" + @@ -4787,7 +4881,7 @@ const file_daemon_proto_rawDesc = "" + "\x04WARN\x10\x04\x12\b\n" + "\x04INFO\x10\x05\x12\t\n" + "\x05DEBUG\x10\x06\x12\t\n" + - "\x05TRACE\x10\a2\x84\x0f\n" + + "\x05TRACE\x10\a2\xbf\x0f\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -4817,7 +4911,8 @@ const file_daemon_proto_rawDesc = "" + "AddProfile\x12\x19.daemon.AddProfileRequest\x1a\x1a.daemon.AddProfileResponse\"\x00\x12N\n" + "\rRemoveProfile\x12\x1c.daemon.RemoveProfileRequest\x1a\x1d.daemon.RemoveProfileResponse\"\x00\x12K\n" + "\fListProfiles\x12\x1b.daemon.ListProfilesRequest\x1a\x1c.daemon.ListProfilesResponse\"\x00\x12W\n" + - "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00B\bZ\x06/protob\x06proto3" + "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00\x129\n" + + "\x06Logout\x12\x15.daemon.LogoutRequest\x1a\x16.daemon.LogoutResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -4832,7 +4927,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 68) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 70) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity @@ -4902,18 +4997,20 @@ var file_daemon_proto_goTypes = []any{ (*Profile)(nil), // 65: daemon.Profile (*GetActiveProfileRequest)(nil), // 66: daemon.GetActiveProfileRequest (*GetActiveProfileResponse)(nil), // 67: daemon.GetActiveProfileResponse - nil, // 68: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 69: daemon.PortInfo.Range - nil, // 70: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 71: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 72: google.protobuf.Timestamp + (*LogoutRequest)(nil), // 68: daemon.LogoutRequest + (*LogoutResponse)(nil), // 69: daemon.LogoutResponse + nil, // 70: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 71: daemon.PortInfo.Range + nil, // 72: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 73: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 74: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 71, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 73, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 22, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 72, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 72, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 71, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 74, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 74, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 73, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 19, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 18, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState 17, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState @@ -4922,8 +5019,8 @@ var file_daemon_proto_depIdxs = []int32{ 21, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState 52, // 11: daemon.FullStatus.events:type_name -> daemon.SystemEvent 28, // 12: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 68, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 69, // 14: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 70, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 71, // 14: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 29, // 15: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 29, // 16: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 30, // 17: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule @@ -4934,10 +5031,10 @@ var file_daemon_proto_depIdxs = []int32{ 49, // 22: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 1, // 23: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 2, // 24: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 72, // 25: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 70, // 26: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 74, // 25: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 72, // 26: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 52, // 27: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 71, // 28: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 73, // 28: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 65, // 29: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile 27, // 30: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList 4, // 31: daemon.DaemonService.Login:input_type -> daemon.LoginRequest @@ -4966,34 +5063,36 @@ var file_daemon_proto_depIdxs = []int32{ 61, // 54: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest 63, // 55: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest 66, // 56: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 5, // 57: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 7, // 58: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 9, // 59: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 11, // 60: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 13, // 61: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 15, // 62: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 24, // 63: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 26, // 64: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 26, // 65: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 31, // 66: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 33, // 67: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 35, // 68: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 37, // 69: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 40, // 70: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 42, // 71: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 44, // 72: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 46, // 73: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse - 50, // 74: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 52, // 75: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 54, // 76: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 56, // 77: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 58, // 78: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 60, // 79: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 62, // 80: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 64, // 81: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 67, // 82: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 57, // [57:83] is the sub-list for method output_type - 31, // [31:57] is the sub-list for method input_type + 68, // 57: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 5, // 58: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 7, // 59: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 9, // 60: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 11, // 61: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 13, // 62: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 15, // 63: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 24, // 64: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 26, // 65: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 26, // 66: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 31, // 67: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 33, // 68: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 35, // 69: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 37, // 70: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 40, // 71: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 42, // 72: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 44, // 73: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 46, // 74: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 50, // 75: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 52, // 76: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 54, // 77: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 56, // 78: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 58, // 79: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 60, // 80: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 62, // 81: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 64, // 82: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 67, // 83: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 69, // 84: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 58, // [58:85] is the sub-list for method output_type + 31, // [31:58] is the sub-list for method input_type 31, // [31:31] is the sub-list for extension type_name 31, // [31:31] is the sub-list for extension extendee 0, // [0:31] is the sub-list for field type_name @@ -5014,13 +5113,14 @@ func file_daemon_proto_init() { file_daemon_proto_msgTypes[46].OneofWrappers = []any{} file_daemon_proto_msgTypes[52].OneofWrappers = []any{} file_daemon_proto_msgTypes[54].OneofWrappers = []any{} + file_daemon_proto_msgTypes[65].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 3, - NumMessages: 68, + NumMessages: 70, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index c25503df9..2e1e0254c 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -79,6 +79,9 @@ service DaemonService { rpc ListProfiles(ListProfilesRequest) returns (ListProfilesResponse) {} rpc GetActiveProfile(GetActiveProfileRequest) returns (GetActiveProfileResponse) {} + + // Logout disconnects from the network and deletes the peer from the management server + rpc Logout(LogoutRequest) returns (LogoutResponse) {} } @@ -614,4 +617,11 @@ message GetActiveProfileRequest {} message GetActiveProfileResponse { string profileName = 1; string username = 2; -} \ No newline at end of file +} + +message LogoutRequest { + optional string profileName = 1; + optional string username = 2; +} + +message LogoutResponse {} \ No newline at end of file diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 669083168..edb56bd8a 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -61,6 +61,8 @@ type DaemonServiceClient interface { RemoveProfile(ctx context.Context, in *RemoveProfileRequest, opts ...grpc.CallOption) (*RemoveProfileResponse, error) ListProfiles(ctx context.Context, in *ListProfilesRequest, opts ...grpc.CallOption) (*ListProfilesResponse, error) GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error) + // Logout disconnects from the network and deletes the peer from the management server + Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) } type daemonServiceClient struct { @@ -328,6 +330,15 @@ func (c *daemonServiceClient) GetActiveProfile(ctx context.Context, in *GetActiv return out, nil } +func (c *daemonServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) { + out := new(LogoutResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/Logout", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -375,6 +386,8 @@ type DaemonServiceServer interface { RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error) ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) + // Logout disconnects from the network and deletes the peer from the management server + Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -460,6 +473,9 @@ func (UnimplementedDaemonServiceServer) ListProfiles(context.Context, *ListProfi func (UnimplementedDaemonServiceServer) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetActiveProfile not implemented") } +func (UnimplementedDaemonServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -944,6 +960,24 @@ func _DaemonService_GetActiveProfile_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _DaemonService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LogoutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).Logout(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/Logout", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).Logout(ctx, req.(*LogoutRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1051,6 +1085,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetActiveProfile", Handler: _DaemonService_GetActiveProfile_Handler, }, + { + MethodName: "Logout", + Handler: _DaemonService_Logout_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/client/server/server.go b/client/server/server.go index 3cb173881..7eb59c91a 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "errors" "fmt" "os" "os/exec" @@ -13,6 +14,7 @@ import ( "github.com/cenkalti/backoff/v4" "golang.org/x/exp/maps" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/protobuf/types/known/durationpb" log "github.com/sirupsen/logrus" @@ -24,6 +26,7 @@ import ( "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" + mgm "github.com/netbirdio/netbird/management/client" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/client/internal" @@ -47,6 +50,8 @@ const ( errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled" ) +var ErrServiceNotUp = errors.New("service is not up") + // Server for service control. type Server struct { rootCtx context.Context @@ -131,13 +136,7 @@ func (s *Server) Start() error { return fmt.Errorf("failed to get active profile state: %w", err) } - cfgPath, err := activeProf.FilePath() - if err != nil { - log.Errorf("failed to get active profile file path: %v", err) - return fmt.Errorf("failed to get active profile file path: %w", err) - } - - config, err := profilemanager.GetConfig(cfgPath) + config, err := s.getConfig(activeProf) if err != nil { log.Errorf("failed to get active profile config: %v", err) @@ -484,13 +483,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro } s.mutex.Unlock() - cfgPath, err := activeProf.FilePath() - if err != nil { - log.Errorf("failed to get active profile file path: %v", err) - return nil, fmt.Errorf("failed to get active profile file path: %w", err) - } - - config, err := profilemanager.GetConfig(cfgPath) + config, err := s.getConfig(activeProf) if err != nil { log.Errorf("failed to get active profile config: %v", err) return nil, fmt.Errorf("failed to get active profile config: %w", err) @@ -701,13 +694,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR log.Infof("active profile: %s for %s", activeProf.Name, activeProf.Username) - cfgPath, err := activeProf.FilePath() - if err != nil { - log.Errorf("failed to get active profile file path: %v", err) - return nil, fmt.Errorf("failed to get active profile file path: %w", err) - } - - config, err := profilemanager.GetConfig(cfgPath) + config, err := s.getConfig(activeProf) if err != nil { log.Errorf("failed to get active profile config: %v", err) return nil, fmt.Errorf("failed to get active profile config: %w", err) @@ -789,13 +776,7 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi log.Errorf("failed to get active profile state: %v", err) return nil, fmt.Errorf("failed to get active profile state: %w", err) } - cfgPath, err := activeProf.FilePath() - if err != nil { - log.Errorf("failed to get active profile file path: %v", err) - return nil, fmt.Errorf("failed to get active profile file path: %w", err) - } - - config, err := profilemanager.GetConfig(cfgPath) + config, err := s.getConfig(activeProf) if err != nil { log.Errorf("failed to get default profile config: %v", err) return nil, fmt.Errorf("failed to get default profile config: %w", err) @@ -811,26 +792,201 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes s.mutex.Lock() defer s.mutex.Unlock() - s.oauthAuthFlow = oauthAuthFlow{} - - if s.actCancel == nil { - return nil, fmt.Errorf("service is not up") - } - s.actCancel() - - err := s.connectClient.Stop() - if err != nil { + if err := s.cleanupConnection(); err != nil { log.Errorf("failed to shut down properly: %v", err) return nil, err } - s.isSessionActive.Store(false) state := internal.CtxGetState(s.rootCtx) state.Set(internal.StatusIdle) + return &proto.DownResponse{}, nil +} + +func (s *Server) cleanupConnection() error { + s.oauthAuthFlow = oauthAuthFlow{} + + if s.actCancel == nil { + return ErrServiceNotUp + } + s.actCancel() + + if s.connectClient == nil { + return nil + } + + if err := s.connectClient.Stop(); err != nil { + return err + } + + s.connectClient = nil + s.isSessionActive.Store(false) + log.Infof("service is down") - return &proto.DownResponse{}, nil + return nil +} + +func (s *Server) Logout(ctx context.Context, msg *proto.LogoutRequest) (*proto.LogoutResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if msg.ProfileName != nil && *msg.ProfileName != "" { + return s.handleProfileLogout(ctx, msg) + } + + return s.handleActiveProfileLogout(ctx) +} + +func (s *Server) handleProfileLogout(ctx context.Context, msg *proto.LogoutRequest) (*proto.LogoutResponse, error) { + if err := s.validateProfileOperation(*msg.ProfileName, true); err != nil { + return nil, err + } + + if msg.Username == nil || *msg.Username == "" { + return nil, gstatus.Errorf(codes.InvalidArgument, "username must be provided when profile name is specified") + } + username := *msg.Username + + if err := s.logoutFromProfile(ctx, *msg.ProfileName, username); err != nil { + log.Errorf("failed to logout from profile %s: %v", *msg.ProfileName, err) + return nil, gstatus.Errorf(codes.Internal, "logout: %v", err) + } + + activeProf, _ := s.profileManager.GetActiveProfileState() + if activeProf != nil && activeProf.Name == *msg.ProfileName { + if err := s.cleanupConnection(); err != nil && !errors.Is(err, ErrServiceNotUp) { + log.Errorf("failed to cleanup connection: %v", err) + } + state := internal.CtxGetState(s.rootCtx) + state.Set(internal.StatusNeedsLogin) + } + + return &proto.LogoutResponse{}, nil +} + +func (s *Server) handleActiveProfileLogout(ctx context.Context) (*proto.LogoutResponse, error) { + if s.config == nil { + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "failed to get active profile state: %v", err) + } + + config, err := s.getConfig(activeProf) + if err != nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "not logged in") + } + s.config = config + } + + if err := s.sendLogoutRequest(ctx); err != nil { + log.Errorf("failed to send logout request: %v", err) + return nil, err + } + + if err := s.cleanupConnection(); err != nil && !errors.Is(err, ErrServiceNotUp) { + log.Errorf("failed to cleanup connection: %v", err) + return nil, err + } + + state := internal.CtxGetState(s.rootCtx) + state.Set(internal.StatusNeedsLogin) + + return &proto.LogoutResponse{}, nil +} + +// getConfig loads the config from the active profile +func (s *Server) getConfig(activeProf *profilemanager.ActiveProfileState) (*profilemanager.Config, error) { + cfgPath, err := activeProf.FilePath() + if err != nil { + return nil, fmt.Errorf("failed to get active profile file path: %w", err) + } + + config, err := profilemanager.GetConfig(cfgPath) + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + return config, nil +} + +func (s *Server) canRemoveProfile(profileName string) error { + if profileName == profilemanager.DefaultProfileName { + return fmt.Errorf("remove profile with reserved name: %s", profilemanager.DefaultProfileName) + } + + activeProf, err := s.profileManager.GetActiveProfileState() + if err == nil && activeProf.Name == profileName { + return fmt.Errorf("remove active profile: %s", profileName) + } + + return nil +} + +func (s *Server) validateProfileOperation(profileName string, allowActiveProfile bool) error { + if s.checkProfilesDisabled() { + return gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + + if profileName == "" { + return gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") + } + + if !allowActiveProfile { + if err := s.canRemoveProfile(profileName); err != nil { + return gstatus.Errorf(codes.InvalidArgument, "%v", err) + } + } + + return nil +} + +// logoutFromProfile logs out from a specific profile by loading its config and sending logout request +func (s *Server) logoutFromProfile(ctx context.Context, profileName, username string) error { + activeProf, err := s.profileManager.GetActiveProfileState() + if err == nil && activeProf.Name == profileName && s.connectClient != nil { + return s.sendLogoutRequest(ctx) + } + + profileState := &profilemanager.ActiveProfileState{ + Name: profileName, + Username: username, + } + profilePath, err := profileState.FilePath() + if err != nil { + return fmt.Errorf("get profile path: %w", err) + } + + config, err := profilemanager.GetConfig(profilePath) + if err != nil { + return fmt.Errorf("profile '%s' not found", profileName) + } + + return s.sendLogoutRequestWithConfig(ctx, config) +} + +func (s *Server) sendLogoutRequest(ctx context.Context) error { + return s.sendLogoutRequestWithConfig(ctx, s.config) +} + +func (s *Server) sendLogoutRequestWithConfig(ctx context.Context, config *profilemanager.Config) error { + key, err := wgtypes.ParseKey(config.PrivateKey) + if err != nil { + return fmt.Errorf("parse private key: %w", err) + } + + mgmTlsEnabled := config.ManagementURL.Scheme == "https" + mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, key, mgmTlsEnabled) + if err != nil { + return fmt.Errorf("connect to management server: %w", err) + } + defer func() { + if err := mgmClient.Close(); err != nil { + log.Errorf("close management client: %v", err) + } + }() + + return mgmClient.Logout() } // Status returns the daemon status @@ -1107,12 +1263,12 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ s.mutex.Lock() defer s.mutex.Unlock() - if s.checkProfilesDisabled() { - return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + if err := s.validateProfileOperation(msg.ProfileName, false); err != nil { + return nil, err } - if msg.ProfileName == "" { - return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") + if err := s.logoutFromProfile(ctx, msg.ProfileName, msg.Username); err != nil { + log.Warnf("failed to logout from profile %s before removal: %v", msg.ProfileName, err) } if err := s.profileManager.RemoveProfile(msg.ProfileName, msg.Username); err != nil { diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index c74412c8b..88cb11eab 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -831,6 +831,7 @@ func (s *serviceClient) onTrayReady() { s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable Lazy Connections", lazyConnMenuDescr, false) s.mBlockInbound = s.mSettings.AddSubMenuItemCheckbox("Block Inbound Connections", blockInboundMenuDescr, false) s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", notificationsMenuDescr, false) + s.mSettings.AddSeparator() s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", advancedSettingsMenuDescr) s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr) s.loadSettings() diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go index c0bc74a2c..e9b7f4f30 100644 --- a/client/ui/event_handler.go +++ b/client/ui/event_handler.go @@ -13,6 +13,7 @@ import ( "fyne.io/systray" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/version" ) @@ -231,3 +232,19 @@ func (h *eventHandler) runSelfCommand(ctx context.Context, command, arg string) log.Printf("command '%s %s' completed successfully", command, arg) } + +func (h *eventHandler) logout(ctx context.Context) error { + client, err := h.client.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf("failed to get service client: %w", err) + } + + _, err = client.Logout(ctx, &proto.LogoutRequest{}) + if err != nil { + return fmt.Errorf("logout failed: %w", err) + } + + h.client.getSrvConfig() + + return nil +} diff --git a/client/ui/profile.go b/client/ui/profile.go index 779f60aa4..b0502c1fb 100644 --- a/client/ui/profile.go +++ b/client/ui/profile.go @@ -40,12 +40,13 @@ func (s *serviceClient) showProfilesUI() { list := widget.NewList( func() int { return len(profiles) }, func() fyne.CanvasObject { - // Each item: Selected indicator, Name, spacer, Select & Remove buttons + // Each item: Selected indicator, Name, spacer, Select, Logout & Remove buttons return container.NewHBox( widget.NewLabel(""), // indicator widget.NewLabel(""), // profile name layout.NewSpacer(), widget.NewButton("Select", nil), + widget.NewButton("Logout", nil), widget.NewButton("Remove", nil), ) }, @@ -55,7 +56,8 @@ func (s *serviceClient) showProfilesUI() { indicator := row.Objects[0].(*widget.Label) nameLabel := row.Objects[1].(*widget.Label) selectBtn := row.Objects[3].(*widget.Button) - removeBtn := row.Objects[4].(*widget.Button) + logoutBtn := row.Objects[4].(*widget.Button) + removeBtn := row.Objects[5].(*widget.Button) profile := profiles[i] // Show a checkmark if selected @@ -105,7 +107,7 @@ func (s *serviceClient) showProfilesUI() { return } - status, err := conn.Status(context.Background(), &proto.StatusRequest{}) + status, err := conn.Status(s.ctx, &proto.StatusRequest{}) if err != nil { log.Errorf("failed to get status after switching profile: %v", err) return @@ -125,6 +127,12 @@ func (s *serviceClient) showProfilesUI() { ) } + logoutBtn.Show() + logoutBtn.SetText("Logout") + logoutBtn.OnTapped = func() { + s.handleProfileLogout(profile.Name, refresh) + } + // Remove profile removeBtn.SetText("Remove") removeBtn.OnTapped = func() { @@ -135,7 +143,7 @@ func (s *serviceClient) showProfilesUI() { if !confirm { return } - // remove + err = s.removeProfile(profile.Name) if err != nil { log.Errorf("failed to remove profile: %v", err) @@ -230,7 +238,7 @@ func (s *serviceClient) addProfile(profileName string) error { return fmt.Errorf("get current user: %w", err) } - _, err = conn.AddProfile(context.Background(), &proto.AddProfileRequest{ + _, err = conn.AddProfile(s.ctx, &proto.AddProfileRequest{ ProfileName: profileName, Username: currUser.Username, }) @@ -253,7 +261,7 @@ func (s *serviceClient) switchProfile(profileName string) error { return fmt.Errorf("get current user: %w", err) } - if _, err := conn.SwitchProfile(context.Background(), &proto.SwitchProfileRequest{ + if _, err := conn.SwitchProfile(s.ctx, &proto.SwitchProfileRequest{ ProfileName: &profileName, Username: &currUser.Username, }); err != nil { @@ -279,7 +287,7 @@ func (s *serviceClient) removeProfile(profileName string) error { return fmt.Errorf("get current user: %w", err) } - _, err = conn.RemoveProfile(context.Background(), &proto.RemoveProfileRequest{ + _, err = conn.RemoveProfile(s.ctx, &proto.RemoveProfileRequest{ ProfileName: profileName, Username: currUser.Username, }) @@ -305,7 +313,7 @@ func (s *serviceClient) getProfiles() ([]Profile, error) { if err != nil { return nil, fmt.Errorf("get current user: %w", err) } - profilesResp, err := conn.ListProfiles(context.Background(), &proto.ListProfilesRequest{ + profilesResp, err := conn.ListProfiles(s.ctx, &proto.ListProfilesRequest{ Username: currUser.Username, }) if err != nil { @@ -324,6 +332,52 @@ func (s *serviceClient) getProfiles() ([]Profile, error) { return profiles, nil } +func (s *serviceClient) handleProfileLogout(profileName string, refreshCallback func()) { + dialog.ShowConfirm( + "Logout", + fmt.Sprintf("Are you sure you want to logout from '%s'?", profileName), + func(confirm bool) { + if !confirm { + return + } + + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("failed to get service client: %v", err) + dialog.ShowError(fmt.Errorf("failed to connect to service"), s.wProfiles) + return + } + + currUser, err := user.Current() + if err != nil { + log.Errorf("failed to get current user: %v", err) + dialog.ShowError(fmt.Errorf("failed to get current user"), s.wProfiles) + return + } + + username := currUser.Username + _, err = conn.Logout(s.ctx, &proto.LogoutRequest{ + ProfileName: &profileName, + Username: &username, + }) + if err != nil { + log.Errorf("logout failed: %v", err) + dialog.ShowError(fmt.Errorf("logout failed"), s.wProfiles) + return + } + + dialog.ShowInformation( + "Logged Out", + fmt.Sprintf("Successfully logged out from '%s'", profileName), + s.wProfiles, + ) + + refreshCallback() + }, + s.wProfiles, + ) +} + type subItem struct { *systray.MenuItem ctx context.Context @@ -339,6 +393,7 @@ type profileMenu struct { emailMenuItem *systray.MenuItem profileSubItems []*subItem manageProfilesSubItem *subItem + logoutSubItem *subItem profilesState []Profile downClickCallback func() error upClickCallback func() error @@ -533,12 +588,11 @@ func (p *profileMenu) refresh() { for { select { case <-ctx.Done(): - return // context cancelled + return case _, ok := <-manageItem.ClickedCh: if !ok { - return // channel closed + return } - // Handle manage profiles click p.eventHandler.runSelfCommand(p.ctx, "profiles", "true") p.refresh() p.loadSettingsCallback() @@ -546,6 +600,30 @@ func (p *profileMenu) refresh() { } }() + // Add Logout menu item + ctx2, cancel2 := context.WithCancel(context.Background()) + logoutItem := p.profileMenuItem.AddSubMenuItem("Logout", "") + p.logoutSubItem = &subItem{logoutItem, ctx2, cancel2} + + go func() { + for { + select { + case <-ctx2.Done(): + return + case _, ok := <-logoutItem.ClickedCh: + if !ok { + return + } + if err := p.eventHandler.logout(p.ctx); err != nil { + log.Errorf("logout failed: %v", err) + p.app.SendNotification(fyne.NewNotification("Error", "Failed to logout")) + } else { + p.app.SendNotification(fyne.NewNotification("Success", "Logged out successfully")) + } + } + } + }() + if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username { p.profileMenuItem.SetTitle(activeProf.ProfileName) } else { @@ -556,7 +634,6 @@ func (p *profileMenu) refresh() { } func (p *profileMenu) clear(profiles []Profile) { - // Clear existing profile items for _, item := range p.profileSubItems { item.Remove() item.cancel() @@ -565,11 +642,16 @@ func (p *profileMenu) clear(profiles []Profile) { p.profilesState = profiles if p.manageProfilesSubItem != nil { - // Remove the manage profiles item if it exists p.manageProfilesSubItem.Remove() p.manageProfilesSubItem.cancel() p.manageProfilesSubItem = nil } + + if p.logoutSubItem != nil { + p.logoutSubItem.Remove() + p.logoutSubItem.cancel() + p.logoutSubItem = nil + } } func (p *profileMenu) updateMenu() { diff --git a/management/client/client.go b/management/client/client.go index 950f6137e..3a50a155b 100644 --- a/management/client/client.go +++ b/management/client/client.go @@ -22,4 +22,5 @@ type Client interface { GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error) IsHealthy() bool SyncMeta(sysInfo *system.Info) error + Logout() error } diff --git a/management/client/grpc.go b/management/client/grpc.go index ef26574bd..f181d8b46 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -497,6 +497,32 @@ func (c *GrpcClient) notifyConnected() { c.connStateCallback.MarkManagementConnected() } +func (c *GrpcClient) Logout() error { + serverKey, err := c.GetServerPublicKey() + if err != nil { + return fmt.Errorf("get server public key: %w", err) + } + + mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*5) + defer cancel() + + message := &proto.Empty{} + encryptedMSG, err := encryption.EncryptMessage(*serverKey, c.key, message) + if err != nil { + return fmt.Errorf("encrypt logout message: %w", err) + } + + _, err = c.realClient.Logout(mgmCtx, &proto.EncryptedMessage{ + WgPubKey: c.key.PublicKey().String(), + Body: encryptedMSG, + }) + if err != nil { + return fmt.Errorf("logout: %w", err) + } + + return nil +} + func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { if info == nil { return nil diff --git a/management/client/mock.go b/management/client/mock.go index 9e1786f82..8e1a13705 100644 --- a/management/client/mock.go +++ b/management/client/mock.go @@ -19,6 +19,7 @@ type MockClient struct { GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) SyncMetaFunc func(sysInfo *system.Info) error + LogoutFunc func() error } func (m *MockClient) IsHealthy() bool { @@ -85,3 +86,10 @@ func (m *MockClient) SyncMeta(sysInfo *system.Info) error { } return m.SyncMetaFunc(sysInfo) } + +func (m *MockClient) Logout() error { + if m.LogoutFunc == nil { + return nil + } + return m.LogoutFunc() +} diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 8503f2e94..848610c78 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -3825,7 +3825,7 @@ var file_management_proto_rawDesc = []byte{ 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, - 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0xcd, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, @@ -3858,8 +3858,12 @@ var file_management_proto_rawDesc = []byte{ 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, + 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3986,15 +3990,17 @@ var file_management_proto_depIdxs = []int32{ 5, // 57: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage 5, // 58: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage 5, // 59: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 5, // 60: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 61: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 16, // 62: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 17, // 63: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 64: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 65: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 17, // 66: management.ManagementService.SyncMeta:output_type -> management.Empty - 60, // [60:67] is the sub-list for method output_type - 53, // [53:60] is the sub-list for method input_type + 5, // 60: management.ManagementService.Logout:input_type -> management.EncryptedMessage + 5, // 61: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 62: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 16, // 63: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 17, // 64: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 65: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 66: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 17, // 67: management.ManagementService.SyncMeta:output_type -> management.Empty + 17, // 68: management.ManagementService.Logout:output_type -> management.Empty + 61, // [61:69] is the sub-list for method output_type + 53, // [53:61] is the sub-list for method input_type 53, // [53:53] is the sub-list for extension type_name 53, // [53:53] is the sub-list for extension extendee 0, // [0:53] is the sub-list for field type_name diff --git a/management/proto/management.proto b/management/proto/management.proto index 8e137df93..d5441d352 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -45,6 +45,9 @@ service ManagementService { // sync meta will evaluate the checks and update the peer meta with the result. // EncryptedMessage of the request has a body of Empty. rpc SyncMeta(EncryptedMessage) returns (Empty) {} + + // Logout logs out the peer and removes it from the management server + rpc Logout(EncryptedMessage) returns (Empty) {} } message EncryptedMessage { diff --git a/management/proto/management_grpc.pb.go b/management/proto/management_grpc.pb.go index badf242f5..5b189334d 100644 --- a/management/proto/management_grpc.pb.go +++ b/management/proto/management_grpc.pb.go @@ -48,6 +48,8 @@ type ManagementServiceClient interface { // sync meta will evaluate the checks and update the peer meta with the result. // EncryptedMessage of the request has a body of Empty. SyncMeta(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) + // Logout logs out the peer and removes it from the management server + Logout(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) } type managementServiceClient struct { @@ -144,6 +146,15 @@ func (c *managementServiceClient) SyncMeta(ctx context.Context, in *EncryptedMes return out, nil } +func (c *managementServiceClient) Logout(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/management.ManagementService/Logout", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ManagementServiceServer is the server API for ManagementService service. // All implementations must embed UnimplementedManagementServiceServer // for forward compatibility @@ -178,6 +189,8 @@ type ManagementServiceServer interface { // sync meta will evaluate the checks and update the peer meta with the result. // EncryptedMessage of the request has a body of Empty. SyncMeta(context.Context, *EncryptedMessage) (*Empty, error) + // Logout logs out the peer and removes it from the management server + Logout(context.Context, *EncryptedMessage) (*Empty, error) mustEmbedUnimplementedManagementServiceServer() } @@ -206,6 +219,9 @@ func (UnimplementedManagementServiceServer) GetPKCEAuthorizationFlow(context.Con func (UnimplementedManagementServiceServer) SyncMeta(context.Context, *EncryptedMessage) (*Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SyncMeta not implemented") } +func (UnimplementedManagementServiceServer) Logout(context.Context, *EncryptedMessage) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") +} func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {} // UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service. @@ -348,6 +364,24 @@ func _ManagementService_SyncMeta_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _ManagementService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).Logout(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/Logout", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).Logout(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + // ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -379,6 +413,10 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SyncMeta", Handler: _ManagementService_SyncMeta_Handler, }, + { + MethodName: "Logout", + Handler: _ManagementService_Logout_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 2f1bc3673..b121cc993 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -19,7 +19,9 @@ import ( "google.golang.org/grpc/status" integrationsConfig "github.com/netbirdio/management-integrations/integrations/config" + "github.com/netbirdio/netbird/management/server/integrations/integrated_validator" + "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/management/proto" @@ -909,6 +911,44 @@ func (s *GRPCServer) SyncMeta(ctx context.Context, req *proto.EncryptedMessage) return &proto.Empty{}, nil } +func (s *GRPCServer) Logout(ctx context.Context, req *proto.EncryptedMessage) (*proto.Empty, error) { + log.WithContext(ctx).Debugf("Logout request from peer [%s]", req.WgPubKey) + + empty := &proto.Empty{} + peerKey, err := s.parseRequest(ctx, req, empty) + if err != nil { + return nil, err + } + + peer, err := s.accountManager.GetStore().GetPeerByPeerPubKey(ctx, store.LockingStrengthShare, peerKey.String()) + if err != nil { + log.WithContext(ctx).Debugf("peer %s is not registered for logout", peerKey.String()) + // TODO: consider idempotency + return nil, mapError(ctx, err) + } + + // nolint:staticcheck + ctx = context.WithValue(ctx, nbContext.PeerIDKey, peer.ID) + // nolint:staticcheck + ctx = context.WithValue(ctx, nbContext.AccountIDKey, peer.AccountID) + + userID := peer.UserID + if userID == "" { + userID = activity.SystemInitiator + } + + if err = s.accountManager.DeletePeer(ctx, peer.AccountID, peer.ID, userID); err != nil { + log.WithContext(ctx).Errorf("failed to logout peer %s: %v", peerKey.String(), err) + return nil, mapError(ctx, err) + } + + s.accountManager.BufferUpdateAccountPeers(ctx, peer.AccountID) + + log.WithContext(ctx).Infof("peer %s logged out successfully", peerKey.String()) + + return &proto.Empty{}, nil +} + // toProtocolChecks converts posture checks to protocol checks. func toProtocolChecks(ctx context.Context, postureChecks []*posture.Checks) []*proto.Checks { protoChecks := make([]*proto.Checks, 0, len(postureChecks)) From 58eb3c8cc23d8c89f0f5cba3c430876bcd37c725 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:20:43 +0200 Subject: [PATCH 191/213] [client] Increase ip rule priorities to avoid conflicts (#4273) --- client/internal/routemanager/systemops/systemops_linux.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index f50ea572c..c0cef94ba 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -77,8 +77,8 @@ type ruleParams struct { func getSetupRules() []ruleParams { return []ruleParams{ - {100, 0, syscall.RT_TABLE_MAIN, netlink.FAMILY_V4, false, 0, "rule with suppress prefixlen v4"}, - {100, 0, syscall.RT_TABLE_MAIN, netlink.FAMILY_V6, false, 0, "rule with suppress prefixlen v6"}, + {105, 0, syscall.RT_TABLE_MAIN, netlink.FAMILY_V4, false, 0, "rule with suppress prefixlen v4"}, + {105, 0, syscall.RT_TABLE_MAIN, netlink.FAMILY_V6, false, 0, "rule with suppress prefixlen v6"}, {110, nbnet.ControlPlaneMark, NetbirdVPNTableID, netlink.FAMILY_V4, true, -1, "rule v4 netbird"}, {110, nbnet.ControlPlaneMark, NetbirdVPNTableID, netlink.FAMILY_V6, true, -1, "rule v6 netbird"}, } From beb66208a0fa354c551968d8bb248293a7944fea Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:45:49 +0200 Subject: [PATCH 192/213] [management, client] Add API to change the network range (#4177) --- client/iface/wgproxy/bind/proxy.go | 2 +- client/iface/wgproxy/ebpf/wrapper.go | 2 +- client/internal/engine.go | 13 +- management/cmd/management.go | 10 +- management/server/account.go | 172 +++++++++++++++++- management/server/account/manager.go | 1 + management/server/account_test.go | 68 +++++++ management/server/activity/codes.go | 7 + management/server/http/api/openapi.yml | 10 + management/server/http/api/types.gen.go | 16 +- .../handlers/accounts/accounts_handler.go | 110 +++++++++++ .../http/handlers/peers/peers_handler.go | 14 ++ .../http/handlers/peers/peers_handler_test.go | 81 +++++++++ management/server/mock_server/account_mock.go | 8 + management/server/types/config.go | 2 +- management/server/types/network.go | 5 +- management/server/types/network_test.go | 102 +++++++++++ management/server/types/settings.go | 5 + util/common.go | 3 +- util/duration.go | 2 +- 20 files changed, 606 insertions(+), 27 deletions(-) diff --git a/client/iface/wgproxy/bind/proxy.go b/client/iface/wgproxy/bind/proxy.go index 179ac0b75..f68e84810 100644 --- a/client/iface/wgproxy/bind/proxy.go +++ b/client/iface/wgproxy/bind/proxy.go @@ -171,7 +171,7 @@ func fakeAddress(peerAddress *net.UDPAddr) (*netip.AddrPort, error) { fakeIP, err := netip.ParseAddr(fmt.Sprintf("127.1.%s.%s", octets[2], octets[3])) if err != nil { - return nil, fmt.Errorf("failed to parse new IP: %w", err) + return nil, fmt.Errorf("parse new IP: %w", err) } netipAddr := netip.AddrPortFrom(fakeIP, uint16(peerAddress.Port)) diff --git a/client/iface/wgproxy/ebpf/wrapper.go b/client/iface/wgproxy/ebpf/wrapper.go index dbf9128a8..b25dc4198 100644 --- a/client/iface/wgproxy/ebpf/wrapper.go +++ b/client/iface/wgproxy/ebpf/wrapper.go @@ -95,7 +95,7 @@ func (e *ProxyWrapper) CloseConn() error { e.closeListener.SetCloseListener(nil) if err := e.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { - return fmt.Errorf("failed to close remote conn: %w", err) + return fmt.Errorf("close remote conn: %w", err) } return nil } diff --git a/client/internal/engine.go b/client/internal/engine.go index 2339866fb..28d1bff53 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -861,15 +861,10 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { return errors.New("wireguard interface is not initialized") } + // Cannot update the IP address without restarting the engine because + // the firewall, route manager, and other components cache the old address if e.wgInterface.Address().String() != conf.Address { - oldAddr := e.wgInterface.Address().String() - log.Debugf("updating peer address from %s to %s", oldAddr, conf.Address) - err := e.wgInterface.UpdateAddr(conf.Address) - if err != nil { - return err - } - e.config.WgAddr = conf.Address - log.Infof("updated peer address from %s to %s", oldAddr, conf.Address) + log.Infof("peer IP address has changed from %s to %s", e.wgInterface.Address().String(), conf.Address) } if conf.GetSshConfig() != nil { @@ -880,7 +875,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { } state := e.statusRecorder.GetLocalPeerState() - state.IP = e.config.WgAddr + state.IP = e.wgInterface.Address().String() state.PubKey = e.config.WgPrivateKey.PublicKey().String() state.KernelInterface = device.WireGuardModuleIsLoaded() state.FQDN = conf.GetFqdn() diff --git a/management/cmd/management.go b/management/cmd/management.go index 24c260e9c..e6fe5c81c 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -142,7 +142,7 @@ var ( err := handleRebrand(cmd) if err != nil { - return fmt.Errorf("failed to migrate files %v", err) + return fmt.Errorf("migrate files %v", err) } if _, err = os.Stat(config.Datadir); os.IsNotExist(err) { @@ -184,7 +184,7 @@ var ( } eventStore, key, err := integrations.InitEventStore(ctx, config.Datadir, config.DataStoreEncryptionKey, integrationMetrics) if err != nil { - return fmt.Errorf("failed to initialize database: %s", err) + return fmt.Errorf("initialize database: %s", err) } if config.DataStoreEncryptionKey != key { @@ -192,7 +192,7 @@ var ( config.DataStoreEncryptionKey = key err := updateMgmtConfig(ctx, types.MgmtConfigPath, config) if err != nil { - return fmt.Errorf("failed to write out store encryption key: %s", err) + return fmt.Errorf("write out store encryption key: %s", err) } } @@ -205,7 +205,7 @@ var ( integratedPeerValidator, err := integrations.NewIntegratedValidator(ctx, eventStore) if err != nil { - return fmt.Errorf("failed to initialize integrated peer validator: %v", err) + return fmt.Errorf("initialize integrated peer validator: %v", err) } permissionsManager := integrations.InitPermissionsManager(store) @@ -217,7 +217,7 @@ var ( accountManager, err := server.BuildManager(ctx, store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain, dnsDomain, eventStore, geo, userDeleteFromIDPEnabled, integratedPeerValidator, appMetrics, proxyController, settingsManager, permissionsManager, config.DisableDefaultPolicy) if err != nil { - return fmt.Errorf("failed to build default manager: %v", err) + return fmt.Errorf("build default manager: %v", err) } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsManager) diff --git a/management/server/account.go b/management/server/account.go index d392cd0b9..6cb2a40e0 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -6,6 +6,7 @@ import ( "fmt" "math/rand" "net" + "net/netip" "os" "reflect" "regexp" @@ -324,6 +325,13 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco return err } + if oldSettings.NetworkRange != newSettings.NetworkRange { + if err = am.reallocateAccountPeerIPs(ctx, transaction, accountID, newSettings.NetworkRange); err != nil { + return err + } + updateAccountPeers = true + } + if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled || oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled || oldSettings.DNSDomain != newSettings.DNSDomain { @@ -362,7 +370,18 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco return nil, err } if oldSettings.DNSDomain != newSettings.DNSDomain { - am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil) + eventMeta := map[string]any{ + "old_dns_domain": oldSettings.DNSDomain, + "new_dns_domain": newSettings.DNSDomain, + } + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, eventMeta) + } + if oldSettings.NetworkRange != newSettings.NetworkRange { + eventMeta := map[string]any{ + "old_network_range": oldSettings.NetworkRange.String(), + "new_network_range": newSettings.NetworkRange.String(), + } + am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountNetworkRangeUpdated, eventMeta) } if updateAccountPeers || extraSettingsChanged || groupChangesAffectPeers { @@ -2018,3 +2037,154 @@ func propagateUserGroupMemberships(ctx context.Context, transaction store.Store, return len(updatedGroups) > 0, peersAffected, nil } + +// reallocateAccountPeerIPs re-allocates all peer IPs when the network range changes +func (am *DefaultAccountManager) reallocateAccountPeerIPs(ctx context.Context, transaction store.Store, accountID string, newNetworkRange netip.Prefix) error { + if !newNetworkRange.IsValid() { + return nil + } + + newIPNet := net.IPNet{ + IP: newNetworkRange.Masked().Addr().AsSlice(), + Mask: net.CIDRMask(newNetworkRange.Bits(), newNetworkRange.Addr().BitLen()), + } + + account, err := transaction.GetAccount(ctx, accountID) + if err != nil { + return err + } + + account.Network.Net = newIPNet + + peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + if err != nil { + return err + } + + var takenIPs []net.IP + + for _, peer := range peers { + newIP, err := types.AllocatePeerIP(newIPNet, takenIPs) + if err != nil { + return status.Errorf(status.Internal, "allocate IP for peer %s: %v", peer.ID, err) + } + + log.WithContext(ctx).Infof("reallocating peer %s IP from %s to %s due to network range change", + peer.ID, peer.IP.String(), newIP.String()) + + peer.IP = newIP + takenIPs = append(takenIPs, newIP) + } + + if err = transaction.SaveAccount(ctx, account); err != nil { + return err + } + + for _, peer := range peers { + if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { + return status.Errorf(status.Internal, "save updated peer %s: %v", peer.ID, err) + } + } + + log.WithContext(ctx).Infof("successfully re-allocated IPs for %d peers in account %s to network range %s", + len(peers), accountID, newNetworkRange.String()) + + return nil +} + +func (am *DefaultAccountManager) validateIPForUpdate(account *types.Account, peers []*nbpeer.Peer, peerID string, newIP netip.Addr) error { + if !account.Network.Net.Contains(newIP.AsSlice()) { + return status.Errorf(status.InvalidArgument, "IP %s is not within the account network range %s", newIP.String(), account.Network.Net.String()) + } + + for _, peer := range peers { + if peer.ID != peerID && peer.IP.Equal(newIP.AsSlice()) { + return status.Errorf(status.InvalidArgument, "IP %s is already assigned to peer %s", newIP.String(), peer.ID) + } + } + return nil +} + +func (am *DefaultAccountManager) UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error { + unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) + defer unlock() + + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update) + if err != nil { + return fmt.Errorf("validate user permissions: %w", err) + } + if !allowed { + return status.NewPermissionDeniedError() + } + + updateNetworkMap, err := am.updatePeerIPInTransaction(ctx, accountID, userID, peerID, newIP) + if err != nil { + return fmt.Errorf("update peer IP transaction: %w", err) + } + + if updateNetworkMap { + am.BufferUpdateAccountPeers(ctx, accountID) + } + return nil +} + +func (am *DefaultAccountManager) updatePeerIPInTransaction(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) (bool, error) { + var updateNetworkMap bool + err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + account, err := transaction.GetAccount(ctx, accountID) + if err != nil { + return fmt.Errorf("get account: %w", err) + } + + existingPeer, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID) + if err != nil { + return fmt.Errorf("get peer: %w", err) + } + + if existingPeer.IP.Equal(newIP.AsSlice()) { + return nil + } + + peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + if err != nil { + return fmt.Errorf("get account peers: %w", err) + } + + if err := am.validateIPForUpdate(account, peers, peerID, newIP); err != nil { + return err + } + + if err := am.savePeerIPUpdate(ctx, transaction, accountID, userID, existingPeer, newIP); err != nil { + return err + } + + updateNetworkMap = true + return nil + }) + return updateNetworkMap, err +} + +func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transaction store.Store, accountID, userID string, peer *nbpeer.Peer, newIP netip.Addr) error { + log.WithContext(ctx).Infof("updating peer %s IP from %s to %s", peer.ID, peer.IP, newIP) + + settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return fmt.Errorf("get account settings: %w", err) + } + dnsDomain := am.GetDNSDomain(settings) + + eventMeta := peer.EventMeta(dnsDomain) + oldIP := peer.IP.String() + + peer.IP = newIP.AsSlice() + err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer) + if err != nil { + return fmt.Errorf("save peer: %w", err) + } + + eventMeta["old_ip"] = oldIP + eventMeta["ip"] = newIP.String() + am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerIPUpdated, eventMeta) + + return nil +} diff --git a/management/server/account/manager.go b/management/server/account/manager.go index 0cd1c6637..17e269e35 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -51,6 +51,7 @@ type Manager interface { MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error DeletePeer(ctx context.Context, accountID, peerID, userID string) error UpdatePeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) + UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error) GetPeerNetwork(ctx context.Context, peerID string) (*types.Network, error) AddPeer(ctx context.Context, setupKey, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) diff --git a/management/server/account_test.go b/management/server/account_test.go index 1dd74104b..77014855f 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "net/netip" "os" "reflect" "strconv" @@ -3522,3 +3523,70 @@ func TestDefaultAccountManager_UpdateAccountOnboarding(t *testing.T) { require.NoError(t, err) }) } + +func TestDefaultAccountManager_UpdatePeerIP(t *testing.T) { + manager, err := createManager(t) + require.NoError(t, err, "unable to create account manager") + + accountID, err := manager.GetAccountIDByUserID(context.Background(), userID, "") + require.NoError(t, err, "unable to create an account") + + key1, err := wgtypes.GenerateKey() + require.NoError(t, err, "unable to generate WireGuard key") + key2, err := wgtypes.GenerateKey() + require.NoError(t, err, "unable to generate WireGuard key") + + peer1, _, _, err := manager.AddPeer(context.Background(), "", userID, &nbpeer.Peer{ + Key: key1.PublicKey().String(), + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-1"}, + }) + require.NoError(t, err, "unable to add peer1") + + peer2, _, _, err := manager.AddPeer(context.Background(), "", userID, &nbpeer.Peer{ + Key: key2.PublicKey().String(), + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"}, + }) + require.NoError(t, err, "unable to add peer2") + + t.Run("update peer IP successfully", func(t *testing.T) { + account, err := manager.Store.GetAccount(context.Background(), accountID) + require.NoError(t, err, "unable to get account") + + newIP, err := types.AllocatePeerIP(account.Network.Net, []net.IP{peer1.IP, peer2.IP}) + require.NoError(t, err, "unable to allocate new IP") + + newAddr := netip.MustParseAddr(newIP.String()) + err = manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, newAddr) + require.NoError(t, err, "unable to update peer IP") + + updatedPeer, err := manager.GetPeer(context.Background(), accountID, peer1.ID, userID) + require.NoError(t, err, "unable to get updated peer") + assert.Equal(t, newIP.String(), updatedPeer.IP.String(), "peer IP should be updated") + }) + + t.Run("update peer IP with same IP should be no-op", func(t *testing.T) { + currentAddr := netip.MustParseAddr(peer1.IP.String()) + err := manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, currentAddr) + require.NoError(t, err, "updating with same IP should not error") + }) + + t.Run("update peer IP with collision should fail", func(t *testing.T) { + peer2Addr := netip.MustParseAddr(peer2.IP.String()) + err := manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, peer2Addr) + require.Error(t, err, "should fail when IP is already assigned") + assert.Contains(t, err.Error(), "already assigned", "error should mention IP collision") + }) + + t.Run("update peer IP outside network range should fail", func(t *testing.T) { + invalidAddr := netip.MustParseAddr("192.168.1.100") + err := manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, invalidAddr) + require.Error(t, err, "should fail when IP is outside network range") + assert.Contains(t, err.Error(), "not within the account network range", "error should mention network range") + }) + + t.Run("update peer IP with invalid peer ID should fail", func(t *testing.T) { + newAddr := netip.MustParseAddr("100.64.0.101") + err := manager.UpdatePeerIP(context.Background(), accountID, userID, "invalid-peer-id", newAddr) + require.Error(t, err, "should fail with invalid peer ID") + }) +} diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 23ddd1dd5..6f9619597 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -175,6 +175,9 @@ const ( AccountLazyConnectionEnabled Activity = 85 AccountLazyConnectionDisabled Activity = 86 + AccountNetworkRangeUpdated Activity = 87 + PeerIPUpdated Activity = 88 + AccountDeleted Activity = 99999 ) @@ -277,6 +280,10 @@ var activityMap = map[Activity]Code{ AccountLazyConnectionEnabled: {"Account lazy connection enabled", "account.setting.lazy.connection.enable"}, AccountLazyConnectionDisabled: {"Account lazy connection disabled", "account.setting.lazy.connection.disable"}, + + AccountNetworkRangeUpdated: {"Account network range updated", "account.network.range.update"}, + + PeerIPUpdated: {"Peer IP updated", "peer.ip.update"}, } // StringCode returns a string code of the activity diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index f8c2b9854..877c68df0 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -133,6 +133,11 @@ components: description: Allows to define a custom dns domain for the account type: string example: my-organization.org + network_range: + description: Allows to define a custom network range for the account in CIDR format + type: string + format: cidr + example: 100.64.0.0/16 extra: $ref: '#/components/schemas/AccountExtraSettings' lazy_connection_enabled: @@ -342,6 +347,11 @@ components: description: (Cloud only) Indicates whether peer needs approval type: boolean example: true + ip: + description: Peer's IP address + type: string + format: ipv4 + example: 100.64.0.15 required: - name - ssh_enabled diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index a9f17aab4..71aa9c830 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -303,6 +303,9 @@ type AccountSettings struct { // LazyConnectionEnabled Enables or disables experimental lazy connection LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"` + // NetworkRange Allows to define a custom network range for the account in CIDR format + NetworkRange *string `json:"network_range,omitempty"` + // PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds). PeerInactivityExpiration int `json:"peer_inactivity_expiration"` @@ -1196,11 +1199,14 @@ type PeerNetworkRangeCheckAction string // PeerRequest defines model for PeerRequest. type PeerRequest struct { // ApprovalRequired (Cloud only) Indicates whether peer needs approval - ApprovalRequired *bool `json:"approval_required,omitempty"` - InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"` - LoginExpirationEnabled bool `json:"login_expiration_enabled"` - Name string `json:"name"` - SshEnabled bool `json:"ssh_enabled"` + ApprovalRequired *bool `json:"approval_required,omitempty"` + InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"` + + // Ip Peer's IP address + Ip *string `json:"ip,omitempty"` + LoginExpirationEnabled bool `json:"login_expiration_enabled"` + Name string `json:"name"` + SshEnabled bool `json:"ssh_enabled"` } // PersonalAccessToken defines model for PersonalAccessToken. diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index ab59434d1..1a22b8b9f 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -1,8 +1,10 @@ package accounts import ( + "context" "encoding/json" "net/http" + "net/netip" "time" "github.com/gorilla/mux" @@ -16,6 +18,17 @@ import ( "github.com/netbirdio/netbird/management/server/types" ) +const ( + // PeerBufferPercentage is the percentage of peers to add as buffer for network range calculations + PeerBufferPercentage = 0.5 + // MinRequiredAddresses is the minimum number of addresses required in a network range + MinRequiredAddresses = 10 + // MinNetworkBits is the minimum prefix length for IPv4 network ranges (e.g., /29 gives 8 addresses, /28 gives 16) + MinNetworkBitsIPv4 = 28 + // MinNetworkBitsIPv6 is the minimum prefix length for IPv6 network ranges + MinNetworkBitsIPv6 = 120 +) + // handler is a handler that handles the server.Account HTTP endpoints type handler struct { accountManager account.Manager @@ -37,6 +50,86 @@ func newHandler(accountManager account.Manager, settingsManager settings.Manager } } +func validateIPAddress(addr netip.Addr) error { + if addr.IsLoopback() { + return status.Errorf(status.InvalidArgument, "loopback address range not allowed") + } + + if addr.IsMulticast() { + return status.Errorf(status.InvalidArgument, "multicast address range not allowed") + } + + if addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast() { + return status.Errorf(status.InvalidArgument, "link-local address range not allowed") + } + + return nil +} + +func validateMinimumSize(prefix netip.Prefix) error { + addr := prefix.Addr() + if addr.Is4() && prefix.Bits() > MinNetworkBitsIPv4 { + return status.Errorf(status.InvalidArgument, "network range too small: minimum size is /%d for IPv4", MinNetworkBitsIPv4) + } + if addr.Is6() && prefix.Bits() > MinNetworkBitsIPv6 { + return status.Errorf(status.InvalidArgument, "network range too small: minimum size is /%d for IPv6", MinNetworkBitsIPv6) + } + return nil +} + +func (h *handler) validateNetworkRange(ctx context.Context, accountID, userID string, networkRange netip.Prefix) error { + if !networkRange.IsValid() { + return nil + } + + if err := validateIPAddress(networkRange.Addr()); err != nil { + return err + } + + if err := validateMinimumSize(networkRange); err != nil { + return err + } + + return h.validateCapacity(ctx, accountID, userID, networkRange) +} + +func (h *handler) validateCapacity(ctx context.Context, accountID, userID string, prefix netip.Prefix) error { + peers, err := h.accountManager.GetPeers(ctx, accountID, userID, "", "") + if err != nil { + return status.Errorf(status.Internal, "get peer count: %v", err) + } + + maxHosts := calculateMaxHosts(prefix) + requiredAddresses := calculateRequiredAddresses(len(peers)) + + if maxHosts < requiredAddresses { + return status.Errorf(status.InvalidArgument, + "network range too small: need at least %d addresses for %d peers + buffer, but range provides %d", + requiredAddresses, len(peers), maxHosts) + } + + return nil +} + +func calculateMaxHosts(prefix netip.Prefix) int64 { + availableAddresses := prefix.Addr().BitLen() - prefix.Bits() + maxHosts := int64(1) << availableAddresses + + if prefix.Addr().Is4() { + maxHosts -= 2 // network and broadcast addresses + } + + return maxHosts +} + +func calculateRequiredAddresses(peerCount int) int64 { + requiredAddresses := int64(peerCount) + int64(float64(peerCount)*PeerBufferPercentage) + if requiredAddresses < MinRequiredAddresses { + requiredAddresses = MinRequiredAddresses + } + return requiredAddresses +} + // getAllAccounts is HTTP GET handler that returns a list of accounts. Effectively returns just a single account. func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) @@ -131,6 +224,18 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { if req.Settings.LazyConnectionEnabled != nil { settings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled } + if req.Settings.NetworkRange != nil && *req.Settings.NetworkRange != "" { + prefix, err := netip.ParsePrefix(*req.Settings.NetworkRange) + if err != nil { + util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid CIDR format: %v", err), w) + return + } + if err := h.validateNetworkRange(r.Context(), accountID, userID, prefix); err != nil { + util.WriteError(r.Context(), err, w) + return + } + settings.NetworkRange = prefix + } var onboarding *types.AccountOnboarding if req.Onboarding != nil { @@ -208,6 +313,11 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A DnsDomain: &settings.DNSDomain, } + if settings.NetworkRange.IsValid() { + networkRangeStr := settings.NetworkRange.String() + apiSettings.NetworkRange = &networkRangeStr + } + apiOnboarding := api.AccountOnboarding{ OnboardingFlowPending: onboarding.OnboardingFlowPending, SignupFormPending: onboarding.SignupFormPending, diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 8c20ed65f..644693e19 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/netip" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" @@ -111,6 +112,19 @@ func (h *Handler) updatePeer(ctx context.Context, accountID, userID, peerID stri } } + if req.Ip != nil { + addr, err := netip.ParseAddr(*req.Ip) + if err != nil { + util.WriteError(ctx, status.Errorf(status.InvalidArgument, "invalid IP address %s: %v", *req.Ip, err), w) + return + } + + if err = h.accountManager.UpdatePeerIP(ctx, accountID, userID, peerID, addr); err != nil { + util.WriteError(ctx, err, w) + return + } + } + peer, err := h.accountManager.UpdatePeer(ctx, accountID, userID, update) if err != nil { util.WriteError(ctx, err, w) diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go index a1fc13dd3..d49ec6b46 100644 --- a/management/server/http/handlers/peers/peers_handler_test.go +++ b/management/server/http/handlers/peers/peers_handler_test.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/netip" "testing" "time" @@ -21,6 +22,7 @@ import ( "github.com/netbirdio/netbird/management/server/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/server/mock_server" ) @@ -112,6 +114,15 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler { p.Name = update.Name return p, nil }, + UpdatePeerIPFunc: func(_ context.Context, accountID, userID, peerID string, newIP netip.Addr) error { + for _, peer := range peers { + if peer.ID == peerID { + peer.IP = net.IP(newIP.AsSlice()) + return nil + } + } + return fmt.Errorf("peer not found") + }, GetPeerFunc: func(_ context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) { var p *nbpeer.Peer for _, peer := range peers { @@ -450,3 +461,73 @@ func TestGetAccessiblePeers(t *testing.T) { }) } } + +func TestPeersHandlerUpdatePeerIP(t *testing.T) { + testPeer := &nbpeer.Peer{ + ID: testPeerID, + Key: "key", + IP: net.ParseIP("100.64.0.1"), + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()}, + Name: "test-host@netbird.io", + LoginExpirationEnabled: false, + UserID: regularUser, + Meta: nbpeer.PeerSystemMeta{ + Hostname: "test-host@netbird.io", + Core: "22.04", + }, + } + + p := initTestMetaData(testPeer) + + tt := []struct { + name string + peerID string + requestBody string + callerUserID string + expectedStatus int + expectedIP string + }{ + { + name: "update peer IP successfully", + peerID: testPeerID, + requestBody: `{"ip": "100.64.0.100"}`, + callerUserID: adminUser, + expectedStatus: http.StatusOK, + expectedIP: "100.64.0.100", + }, + { + name: "update peer IP with invalid IP", + peerID: testPeerID, + requestBody: `{"ip": "invalid-ip"}`, + callerUserID: adminUser, + expectedStatus: http.StatusUnprocessableEntity, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/peers/%s", tc.peerID), bytes.NewBuffer([]byte(tc.requestBody))) + req.Header.Set("Content-Type", "application/json") + req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{ + UserId: tc.callerUserID, + Domain: "hotmail.com", + AccountId: "test_id", + }) + + rr := httptest.NewRecorder() + router := mux.NewRouter() + router.HandleFunc("/peers/{peerId}", p.HandlePeer).Methods("PUT") + + router.ServeHTTP(rr, req) + + assert.Equal(t, tc.expectedStatus, rr.Code) + + if tc.expectedStatus == http.StatusOK && tc.expectedIP != "" { + var updatedPeer api.Peer + err := json.Unmarshal(rr.Body.Bytes(), &updatedPeer) + require.NoError(t, err) + assert.Equal(t, tc.expectedIP, updatedPeer.Ip) + } + }) + } +} diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 8c8fd19c9..d72ca8c2b 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -60,6 +60,7 @@ type MockAccountManager struct { GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) + UpdatePeerIPFunc func(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error CreateRouteFunc func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) GetRouteFunc func(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error) SaveRouteFunc func(ctx context.Context, accountID string, userID string, route *route.Route) error @@ -483,6 +484,13 @@ func (am *MockAccountManager) UpdatePeer(ctx context.Context, accountID, userID return nil, status.Errorf(codes.Unimplemented, "method UpdatePeer is not implemented") } +func (am *MockAccountManager) UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error { + if am.UpdatePeerIPFunc != nil { + return am.UpdatePeerIPFunc(ctx, accountID, userID, peerID, newIP) + } + return status.Errorf(codes.Unimplemented, "method UpdatePeerIP is not implemented") +} + // CreateRoute mock implementation of CreateRoute from server.AccountManager interface func (am *MockAccountManager) CreateRoute(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupID []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) { if am.CreateRouteFunc != nil { diff --git a/management/server/types/config.go b/management/server/types/config.go index 5c64d6c6d..7df7f3874 100644 --- a/management/server/types/config.go +++ b/management/server/types/config.go @@ -53,7 +53,7 @@ type Config struct { StoreConfig StoreConfig ReverseProxy ReverseProxy - + // disable default all-to-all policy DisableDefaultPolicy bool } diff --git a/management/server/types/network.go b/management/server/types/network.go index eb8415264..5d725c2a4 100644 --- a/management/server/types/network.go +++ b/management/server/types/network.go @@ -163,7 +163,10 @@ func (n *Network) Copy() *Network { // E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3 func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) { baseIP := ipToUint32(ipNet.IP.Mask(ipNet.Mask)) - totalIPs := uint32(1 << SubnetSize) + + ones, bits := ipNet.Mask.Size() + hostBits := bits - ones + totalIPs := uint32(1 << hostBits) taken := make(map[uint32]struct{}, len(takenIps)+1) taken[baseIP] = struct{}{} // reserve network IP diff --git a/management/server/types/network_test.go b/management/server/types/network_test.go index d0b0894d4..4c1459ce5 100644 --- a/management/server/types/network_test.go +++ b/management/server/types/network_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewNetwork(t *testing.T) { @@ -38,6 +39,107 @@ func TestAllocatePeerIP(t *testing.T) { } } +func TestAllocatePeerIPSmallSubnet(t *testing.T) { + // Test /27 network (10.0.0.0/27) - should only have 30 usable IPs (10.0.0.1 to 10.0.0.30) + ipNet := net.IPNet{IP: net.ParseIP("10.0.0.0"), Mask: net.IPMask{255, 255, 255, 224}} + var ips []net.IP + + // Allocate all available IPs in the /27 network + for i := 0; i < 30; i++ { + ip, err := AllocatePeerIP(ipNet, ips) + if err != nil { + t.Fatal(err) + } + + // Verify IP is within the correct range + if !ipNet.Contains(ip) { + t.Errorf("allocated IP %s is not within network %s", ip.String(), ipNet.String()) + } + + ips = append(ips, ip) + } + + assert.Len(t, ips, 30) + + // Verify all IPs are unique + uniq := make(map[string]struct{}) + for _, ip := range ips { + if _, ok := uniq[ip.String()]; !ok { + uniq[ip.String()] = struct{}{} + } else { + t.Errorf("found duplicate IP %s", ip.String()) + } + } + + // Try to allocate one more IP - should fail as network is full + _, err := AllocatePeerIP(ipNet, ips) + if err == nil { + t.Error("expected error when network is full, but got none") + } +} + +func TestAllocatePeerIPVariousCIDRs(t *testing.T) { + testCases := []struct { + name string + cidr string + expectedUsable int + }{ + {"/30 network", "192.168.1.0/30", 2}, // 4 total - 2 reserved = 2 usable + {"/29 network", "192.168.1.0/29", 6}, // 8 total - 2 reserved = 6 usable + {"/28 network", "192.168.1.0/28", 14}, // 16 total - 2 reserved = 14 usable + {"/27 network", "192.168.1.0/27", 30}, // 32 total - 2 reserved = 30 usable + {"/26 network", "192.168.1.0/26", 62}, // 64 total - 2 reserved = 62 usable + {"/25 network", "192.168.1.0/25", 126}, // 128 total - 2 reserved = 126 usable + {"/16 network", "10.0.0.0/16", 65534}, // 65536 total - 2 reserved = 65534 usable + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, ipNet, err := net.ParseCIDR(tc.cidr) + require.NoError(t, err) + + var ips []net.IP + + // For larger networks, test only a subset to avoid long test runs + testCount := tc.expectedUsable + if testCount > 1000 { + testCount = 1000 + } + + // Allocate IPs and verify they're within the correct range + for i := 0; i < testCount; i++ { + ip, err := AllocatePeerIP(*ipNet, ips) + require.NoError(t, err, "failed to allocate IP %d", i) + + // Verify IP is within the correct range + assert.True(t, ipNet.Contains(ip), "allocated IP %s is not within network %s", ip.String(), ipNet.String()) + + // Verify IP is not network or broadcast address + networkIP := ipNet.IP.Mask(ipNet.Mask) + ones, bits := ipNet.Mask.Size() + hostBits := bits - ones + broadcastInt := uint32(ipToUint32(networkIP)) + (1 << hostBits) - 1 + broadcastIP := uint32ToIP(broadcastInt) + + assert.False(t, ip.Equal(networkIP), "allocated network address %s", ip.String()) + assert.False(t, ip.Equal(broadcastIP), "allocated broadcast address %s", ip.String()) + + ips = append(ips, ip) + } + + assert.Len(t, ips, testCount) + + // Verify all IPs are unique + uniq := make(map[string]struct{}) + for _, ip := range ips { + ipStr := ip.String() + assert.NotContains(t, uniq, ipStr, "found duplicate IP %s", ipStr) + uniq[ipStr] = struct{}{} + } + }) + } +} + func TestGenerateIPs(t *testing.T) { ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}} ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}}) diff --git a/management/server/types/settings.go b/management/server/types/settings.go index 37c728bf8..436eb337c 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -1,6 +1,7 @@ package types import ( + "net/netip" "time" ) @@ -42,6 +43,9 @@ type Settings struct { // DNSDomain is the custom domain for that account DNSDomain string + // NetworkRange is the custom network range for that account + NetworkRange netip.Prefix `gorm:"serializer:json"` + // Extra is a dictionary of Account settings Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` @@ -66,6 +70,7 @@ func (s *Settings) Copy() *Settings { RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled, LazyConnectionEnabled: s.LazyConnectionEnabled, DNSDomain: s.DNSDomain, + NetworkRange: s.NetworkRange, } if s.Extra != nil { settings.Extra = s.Extra.Copy() diff --git a/util/common.go b/util/common.go index cd19d9747..27adb9d13 100644 --- a/util/common.go +++ b/util/common.go @@ -23,7 +23,6 @@ func FileExists(path string) bool { return err == nil } - /// Bool helpers // True returns a *bool whose underlying value is true. @@ -56,4 +55,4 @@ func ReturnBoolWithDefaultTrue(b *bool) bool { return true } -} \ No newline at end of file +} diff --git a/util/duration.go b/util/duration.go index 4757bf17e..b657a582d 100644 --- a/util/duration.go +++ b/util/duration.go @@ -6,7 +6,7 @@ import ( "time" ) -//Duration is used strictly for JSON requests/responses due to duration marshalling issues +// Duration is used strictly for JSON requests/responses due to duration marshalling issues type Duration struct { time.Duration } From d1e0b7f4fb45339197f567602f91bf4a943dcaf0 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 5 Aug 2025 01:11:44 +0200 Subject: [PATCH 193/213] [management] get peer groups without lock (#4280) --- management/server/ephemeral_test.go | 2 +- management/server/peer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go index f71d48c58..d07b9a422 100644 --- a/management/server/ephemeral_test.go +++ b/management/server/ephemeral_test.go @@ -43,10 +43,10 @@ func (a *MockAccountManager) DeletePeer(_ context.Context, accountID, peerID, us a.mu.Lock() defer a.mu.Unlock() a.deletePeerCalls++ + delete(a.store.account.Peers, peerID) if a.wg != nil { a.wg.Done() } - delete(a.store.account.Peers, peerID) return nil } diff --git a/management/server/peer.go b/management/server/peer.go index f954369d8..ddf54bd59 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1525,7 +1525,7 @@ func (am *DefaultAccountManager) getInactivePeers(ctx context.Context, accountID // GetPeerGroups returns groups that the peer is part of. func (am *DefaultAccountManager) GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error) { - return am.Store.GetPeerGroups(ctx, store.LockingStrengthShare, accountID, peerID) + return am.Store.GetPeerGroups(ctx, store.LockingStrengthNone, accountID, peerID) } // getPeerGroupIDs returns the IDs of the groups that the peer is part of. From 348d981b2c2453d9ace14a8a2f355224ba105f2e Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:31:19 +0200 Subject: [PATCH 194/213] [management] expire invalid peer (#4275) --- go.mod | 2 +- go.sum | 4 +-- management/server/account.go | 28 +++++++++++++++---- management/server/integrated_validator.go | 2 +- .../integrated_validator/interface.go | 2 +- management/server/user.go | 3 +- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 4d9191d04..70ae9abc2 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c + github.com/netbirdio/management-integrations/integrations v0.0.0-20250805075620-df61803a4461 github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index 84dfe2403..8e7ab2bdf 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c h1:OtX903X0FKEE+fcsp/P2701md7X/xbi/W/ojWIJNKSk= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250805075620-df61803a4461 h1:86MeBP7UxexphbOWpO5dxf5SLKlpGPYTDGR4m5WJ/Ew= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250805075620-df61803a4461/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/server/account.go b/management/server/account.go index 6cb2a40e0..395634769 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -251,8 +251,8 @@ func BuildManager( }() } - am.integratedPeerValidator.SetPeerInvalidationListener(func(accountID string) { - am.onPeersInvalidated(ctx, accountID) + am.integratedPeerValidator.SetPeerInvalidationListener(func(accountID string, peerIDs []string) { + am.onPeersInvalidated(ctx, accountID, peerIDs) }) return am, nil @@ -1719,9 +1719,27 @@ func (am *DefaultAccountManager) GetDNSDomain(settings *types.Settings) string { return settings.DNSDomain } -func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) { - log.WithContext(ctx).Debugf("validated peers has been invalidated for account %s", accountID) - am.BufferUpdateAccountPeers(ctx, accountID) +func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string, peerIDs []string) { + peers := []*nbpeer.Peer{} + log.WithContext(ctx).Debugf("invalidating peers %v for account %s", peerIDs, accountID) + for _, peerID := range peerIDs { + peer, err := am.GetPeer(ctx, accountID, peerID, activity.SystemInitiator) + if err != nil { + log.WithContext(ctx).Errorf("failed to get invalidated peer %s for account %s: %v", peerID, accountID, err) + continue + } + peers = append(peers, peer) + } + if len(peers) > 0 { + err := am.expireAndUpdatePeers(ctx, accountID, peers) + if err != nil { + log.WithContext(ctx).Errorf("failed to expire and update invalidated peers for account %s: %v", accountID, err) + return + } + } else { + log.WithContext(ctx).Debugf("running invalidation with no invalid peers") + } + log.WithContext(ctx).Debugf("invalidated peers have been expired for account %s", accountID) } func (am *DefaultAccountManager) FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) { diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index b89739be9..1e92e0c50 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -151,7 +151,7 @@ func (MockIntegratedValidator) PeerDeleted(_ context.Context, _, _ string, extra return nil } -func (MockIntegratedValidator) SetPeerInvalidationListener(func(accountID string)) { +func (MockIntegratedValidator) SetPeerInvalidationListener(func(accountID string, peerIDs []string)) { // just a dummy } diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go index 4d4a8cdf6..945cd7131 100644 --- a/management/server/integrations/integrated_validator/interface.go +++ b/management/server/integrations/integrated_validator/interface.go @@ -16,7 +16,7 @@ type IntegratedValidator interface { IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) GetValidatedPeers(ctx context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error - SetPeerInvalidationListener(fn func(accountID string)) + SetPeerInvalidationListener(fn func(accountID string, peerIDs []string)) Stop(ctx context.Context) ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow } diff --git a/management/server/user.go b/management/server/user.go index a0f4c4a6c..58d2d0c41 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -938,6 +938,7 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a // expireAndUpdatePeers expires all peers of the given user and updates them in the account func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accountID string, peers []*nbpeer.Peer) error { + log.WithContext(ctx).Debugf("Expiring %d peers for account %s", len(peers), accountID) settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) if err != nil { return err @@ -968,7 +969,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou if len(peerIDs) != 0 { // this will trigger peer disconnect from the management service am.peersUpdateManager.CloseChannels(ctx, peerIDs) - am.UpdateAccountPeers(ctx, accountID) + am.BufferUpdateAccountPeers(ctx, accountID) } return nil } From 9ccc13e6ea53241ad2beaa537d7951066530d65a Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:33:43 +0300 Subject: [PATCH 195/213] [client]: Add config flag to service to override default profile path (#4276) [client]: Add config flag to service to override default profile path (#4276) --- client/cmd/root.go | 2 +- client/cmd/service_controller.go | 2 +- client/cmd/service_installer.go | 4 ++++ client/cmd/testutil_test.go | 2 +- client/cmd/up.go | 12 +++++++++++- client/internal/debug/debug.go | 4 ++-- client/internal/engine.go | 2 +- client/internal/profilemanager/service.go | 10 +++++++++- client/server/server.go | 6 +++--- client/server/server_test.go | 6 +++--- 10 files changed, 36 insertions(+), 14 deletions(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 6a8ae27f4..86c76e6ab 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -127,7 +127,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.") rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device") rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output") - rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "(DEPRECATED) Netbird config file location") + rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Overrides the default profile file location") rootCmd.AddCommand(upCmd) rootCmd.AddCommand(downCmd) diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 6dc6bca9b..14a41e607 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), profilesDisabled) + serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index be8a897dc..ac22000bd 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -41,6 +41,10 @@ func buildServiceArguments() []string { args = append(args, "--management-url", managementURL) } + if configPath != "" { + args = append(args, "--config", configPath) + } + for _, logFile := range logFiles { args = append(args, "--log-file", logFile) } diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index 5dbc8cd7f..a6be84641 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -134,7 +134,7 @@ func startClientDaemon( s := grpc.NewServer() server := client.New(ctx, - "", false) + "", "", false) if err := server.Start(); err != nil { t.Fatal(err) } diff --git a/client/cmd/up.go b/client/cmd/up.go index a0c26a207..35ce77539 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -79,7 +79,7 @@ func init() { upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) - upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location") + upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location. ") } @@ -146,6 +146,11 @@ func upFunc(cmd *cobra.Command, args []string) error { } func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error { + // override the default profile filepath if provided + if configPath != "" { + _ = profilemanager.NewServiceManager(configPath) + } + err := handleRebrand(cmd) if err != nil { return err @@ -197,6 +202,11 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr } func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, profileSwitched bool) error { + // Check if deprecated config flag is set and show warning + if cmd.Flag("config").Changed && configPath != "" { + cmd.PrintErrf("Warning: Config flag is deprecated on up command, it should be set as a service argument with $NB_CONFIG environment or with \"-config\" flag; netbird service reconfigure --service-env=\"NB_CONFIG=\" or netbird service run --config=\n") + } + customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed) if err != nil { return fmt.Errorf("parse custom DNS address: %v", err) diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 71ebf431d..c25c29d03 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -558,7 +558,7 @@ func (g *BundleGenerator) addNetworkMap() error { } func (g *BundleGenerator) addStateFile() error { - sm := profilemanager.ServiceManager{} + sm := profilemanager.NewServiceManager("") path := sm.GetStatePath() if path == "" { return nil @@ -597,7 +597,7 @@ func (g *BundleGenerator) addStateFile() error { } func (g *BundleGenerator) addCorruptedStateFiles() error { - sm := profilemanager.ServiceManager{} + sm := profilemanager.NewServiceManager("") pattern := sm.GetStatePath() if pattern == "" { return nil diff --git a/client/internal/engine.go b/client/internal/engine.go index 28d1bff53..f8d3c33bc 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -238,7 +238,7 @@ func NewEngine( connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), } - sm := profilemanager.ServiceManager{} + sm := profilemanager.NewServiceManager("") path := sm.GetStatePath() if runtime.GOOS == "ios" { diff --git a/client/internal/profilemanager/service.go b/client/internal/profilemanager/service.go index 520eef2e9..faccf5f68 100644 --- a/client/internal/profilemanager/service.go +++ b/client/internal/profilemanager/service.go @@ -75,7 +75,15 @@ func (a *ActiveProfileState) FilePath() (string, error) { return filepath.Join(configDir, a.Name+".json"), nil } -type ServiceManager struct{} +type ServiceManager struct { +} + +func NewServiceManager(defaultConfigPath string) *ServiceManager { + if defaultConfigPath != "" { + DefaultConfigPath = defaultConfigPath + } + return &ServiceManager{} +} func (s *ServiceManager) CopyDefaultProfileIfNotExists() (bool, error) { diff --git a/client/server/server.go b/client/server/server.go index 7eb59c91a..b7c27ba5c 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -74,7 +74,7 @@ type Server struct { persistNetworkMap bool isSessionActive atomic.Bool - profileManager profilemanager.ServiceManager + profileManager *profilemanager.ServiceManager profilesDisabled bool } @@ -86,13 +86,13 @@ type oauthAuthFlow struct { } // New server instance constructor. -func New(ctx context.Context, logFile string, profilesDisabled bool) *Server { +func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool) *Server { return &Server{ rootCtx: ctx, logFile: logFile, persistNetworkMap: true, statusRecorder: peer.NewRecorder(""), - profileManager: profilemanager.ServiceManager{}, + profileManager: profilemanager.NewServiceManager(configFile), profilesDisabled: profilesDisabled, } } diff --git a/client/server/server_test.go b/client/server/server_test.go index afd38b4a4..c0a3621ec 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -94,7 +94,7 @@ func TestConnectWithRetryRuns(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "debug", false) + s := New(ctx, "debug", "", false) s.config = config @@ -151,7 +151,7 @@ func TestServer_Up(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console", false) + s := New(ctx, "console", "", false) err = s.Start() require.NoError(t, err) @@ -227,7 +227,7 @@ func TestServer_SubcribeEvents(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console", false) + s := New(ctx, "console", "", false) err = s.Start() require.NoError(t, err) From 92ce5afe80f2f2919e047efb9d5d53a1c402f047 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Tue, 5 Aug 2025 11:37:21 +0200 Subject: [PATCH 196/213] =?UTF-8?q?Dual=20license:=20apply=20AGPL=E2=80=91?= =?UTF-8?q?3.0=20to=20management/,=20signal/,=20and=20relay=20directories?= =?UTF-8?q?=20(BSD=E2=80=913=20remains=20for=20the=20rest)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 3 + LICENSES/AGPL-3.0.txt | 661 ++++++++++++++++++++++++++++++++++++++ LICENSES/BSD-3-Clause.txt | 13 + LICENSES/REUSE.toml | 6 + README.md | 6 +- management/LICENSE | 661 ++++++++++++++++++++++++++++++++++++++ relay/LICENSE | 661 ++++++++++++++++++++++++++++++++++++++ signal/LICENSE | 661 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 2671 insertions(+), 1 deletion(-) create mode 100644 LICENSES/AGPL-3.0.txt create mode 100644 LICENSES/BSD-3-Clause.txt create mode 100644 LICENSES/REUSE.toml create mode 100644 management/LICENSE create mode 100644 relay/LICENSE create mode 100644 signal/LICENSE diff --git a/LICENSE b/LICENSE index 7cba76dfd..594691464 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,6 @@ +This BSD‑3‑Clause license applies to all parts of the repository except for the directories management/, signal/ and relay/. +Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory. + BSD 3-Clause License Copyright (c) 2022 NetBird GmbH & AUTHORS diff --git a/LICENSES/AGPL-3.0.txt b/LICENSES/AGPL-3.0.txt new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/LICENSES/AGPL-3.0.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 000000000..7cba76dfd --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2022 NetBird GmbH & AUTHORS + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/REUSE.toml b/LICENSES/REUSE.toml new file mode 100644 index 000000000..68f32724c --- /dev/null +++ b/LICENSES/REUSE.toml @@ -0,0 +1,6 @@ +[project] +default_license = "BSD-3-Clause" + +[[files]] +paths = ["management/", "signal/", "relay/"] +license = "AGPL-3.0-only" diff --git a/README.md b/README.md index d5469c28b..ea7655869 100644 --- a/README.md +++ b/README.md @@ -134,5 +134,9 @@ In November 2022, NetBird joined the [StartUpSecure program](https://www.forschu We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn), and [Rosenpass](https://rosenpass.eu). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g., by giving a star or a contribution). ### Legal - _WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld. +This repository is licensed under BSD-3-Clause license that applies to all parts of the repository except for the directories management/, signal/ and relay/. +Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory. + +_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld. + diff --git a/management/LICENSE b/management/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/management/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/relay/LICENSE b/relay/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/relay/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/signal/LICENSE b/signal/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/signal/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From 3d3c4c5844066996e78eae06925185d4c49504c1 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:55:50 +0200 Subject: [PATCH 197/213] [client] Add full sync response to debug bundle (#4287) --- client/cmd/debug.go | 28 ++--- client/internal/connect.go | 32 +++--- client/internal/debug/debug.go | 120 ++++++++++++++++---- client/internal/engine.go | 49 ++++---- client/proto/daemon.pb.go | 202 ++++++++++++++++----------------- client/proto/daemon.proto | 8 +- client/proto/daemon_grpc.pb.go | 32 +++--- client/server/debug.go | 22 ++-- client/server/server.go | 20 ++-- client/ui/debug.go | 6 +- 10 files changed, 298 insertions(+), 221 deletions(-) diff --git a/client/cmd/debug.go b/client/cmd/debug.go index a79fd40d0..556b3bb25 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -77,11 +77,11 @@ var forCmd = &cobra.Command{ var persistenceCmd = &cobra.Command{ Use: "persistence [on|off]", - Short: "Set network map memory persistence", - Long: `Configure whether the latest network map should persist in memory. When enabled, the last known network map will be kept in memory.`, + Short: "Set sync response memory persistence", + Long: `Configure whether the latest sync response should persist in memory. When enabled, the last known sync response will be kept in memory.`, Example: " netbird debug persistence on", Args: cobra.ExactArgs(1), - RunE: setNetworkMapPersistence, + RunE: setSyncResponsePersistence, } func debugBundle(cmd *cobra.Command, _ []string) error { @@ -206,11 +206,11 @@ func runForDuration(cmd *cobra.Command, args []string) error { time.Sleep(1 * time.Second) - // Enable network map persistence before bringing the service up - if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{ + // Enable sync response persistence before bringing the service up + if _, err := client.SetSyncResponsePersistence(cmd.Context(), &proto.SetSyncResponsePersistenceRequest{ Enabled: true, }); err != nil { - return fmt.Errorf("failed to enable network map persistence: %v", status.Convert(err).Message()) + return fmt.Errorf("failed to enable sync response persistence: %v", status.Convert(err).Message()) } if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { @@ -273,7 +273,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { return nil } -func setNetworkMapPersistence(cmd *cobra.Command, args []string) error { +func setSyncResponsePersistence(cmd *cobra.Command, args []string) error { conn, err := getClient(cmd) if err != nil { return err @@ -290,14 +290,14 @@ func setNetworkMapPersistence(cmd *cobra.Command, args []string) error { } client := proto.NewDaemonServiceClient(conn) - _, err = client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{ + _, err = client.SetSyncResponsePersistence(cmd.Context(), &proto.SetSyncResponsePersistenceRequest{ Enabled: persistence == "on", }) if err != nil { - return fmt.Errorf("failed to set network map persistence: %v", status.Convert(err).Message()) + return fmt.Errorf("failed to set sync response persistence: %v", status.Convert(err).Message()) } - cmd.Printf("Network map persistence set to: %s\n", persistence) + cmd.Printf("Sync response persistence set to: %s\n", persistence) return nil } @@ -357,13 +357,13 @@ func formatDuration(d time.Duration) string { } func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) { - var networkMap *mgmProto.NetworkMap + var syncResponse *mgmProto.SyncResponse var err error if connectClient != nil { - networkMap, err = connectClient.GetLatestNetworkMap() + syncResponse, err = connectClient.GetLatestSyncResponse() if err != nil { - log.Warnf("Failed to get latest network map: %v", err) + log.Warnf("Failed to get latest sync response: %v", err) } } @@ -371,7 +371,7 @@ func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, c debug.GeneratorDependencies{ InternalConfig: config, StatusRecorder: recorder, - NetworkMap: networkMap, + SyncResponse: syncResponse, LogFile: logFilePath, }, debug.BundleConfig{ diff --git a/client/internal/connect.go b/client/internal/connect.go index cd4dd3cb7..e1f6f2bbe 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -43,7 +43,7 @@ type ConnectClient struct { engine *Engine engineMutex sync.Mutex - persistNetworkMap bool + persistSyncResponse bool } func NewConnectClient( @@ -270,7 +270,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan c.engineMutex.Lock() c.engine = NewEngine(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, checks) - c.engine.SetNetworkMapPersistence(c.persistNetworkMap) + c.engine.SetSyncResponsePersistence(c.persistSyncResponse) c.engineMutex.Unlock() if err := c.engine.Start(); err != nil { @@ -349,23 +349,23 @@ func (c *ConnectClient) Engine() *Engine { return e } -// GetLatestNetworkMap returns the latest network map from the engine. -func (c *ConnectClient) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) { +// GetLatestSyncResponse returns the latest sync response from the engine. +func (c *ConnectClient) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) { engine := c.Engine() if engine == nil { return nil, errors.New("engine is not initialized") } - networkMap, err := engine.GetLatestNetworkMap() + syncResponse, err := engine.GetLatestSyncResponse() if err != nil { - return nil, fmt.Errorf("get latest network map: %w", err) + return nil, fmt.Errorf("get latest sync response: %w", err) } - if networkMap == nil { - return nil, errors.New("network map is not available") + if syncResponse == nil { + return nil, errors.New("sync response is not available") } - return networkMap, nil + return syncResponse, nil } // Status returns the current client status @@ -398,18 +398,18 @@ func (c *ConnectClient) Stop() error { return nil } -// SetNetworkMapPersistence enables or disables network map persistence. -// When enabled, the last received network map will be stored and can be retrieved -// through the Engine's getLatestNetworkMap method. When disabled, any stored -// network map will be cleared. -func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) { +// SetSyncResponsePersistence enables or disables sync response persistence. +// When enabled, the last received sync response will be stored and can be retrieved +// through the Engine's GetLatestSyncResponse method. When disabled, any stored +// sync response will be cleared. +func (c *ConnectClient) SetSyncResponsePersistence(enabled bool) { c.engineMutex.Lock() - c.persistNetworkMap = enabled + c.persistSyncResponse = enabled c.engineMutex.Unlock() engine := c.Engine() if engine != nil { - engine.SetNetworkMapPersistence(enabled) + engine.SetSyncResponsePersistence(enabled) } } diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index c25c29d03..70786583e 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -46,7 +46,7 @@ iptables.txt: Anonymized iptables rules with packet counters, if --system-info f nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided. resolved_domains.txt: Anonymized resolved domain IP addresses from the status recorder. config.txt: Anonymized configuration information of the NetBird client. -network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules. +network_map.json: Anonymized sync response containing peer configurations, routes, DNS settings, and firewall rules. state.json: Anonymized client state dump containing netbird states. mutex.prof: Mutex profiling information. goroutine.prof: Goroutine profiling information. @@ -73,7 +73,7 @@ Domains All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle. Reoccuring domain names are replaced with the same anonymized domain. -Network Map +Sync Response The network_map.json file contains the following anonymized information: - Peer configurations (addresses, FQDNs, DNS settings) - Remote and offline peer information (allowed IPs, FQDNs) @@ -81,7 +81,7 @@ The network_map.json file contains the following anonymized information: - DNS configuration (nameservers, domains, custom zones) - Firewall rules (peer IPs, source/destination ranges) -SSH keys in the network map are replaced with a placeholder value. All IP addresses and domains in the network map follow the same anonymization rules as described above. +SSH keys in the sync response are replaced with a placeholder value. All IP addresses and domains in the sync response follow the same anonymization rules as described above. State File The state.json file contains anonymized internal state information of the NetBird client, including: @@ -201,7 +201,7 @@ type BundleGenerator struct { // deps internalConfig *profilemanager.Config statusRecorder *peer.Status - networkMap *mgmProto.NetworkMap + syncResponse *mgmProto.SyncResponse logFile string anonymize bool @@ -222,7 +222,7 @@ type BundleConfig struct { type GeneratorDependencies struct { InternalConfig *profilemanager.Config StatusRecorder *peer.Status - NetworkMap *mgmProto.NetworkMap + SyncResponse *mgmProto.SyncResponse LogFile string } @@ -238,7 +238,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen internalConfig: deps.InternalConfig, statusRecorder: deps.StatusRecorder, - networkMap: deps.NetworkMap, + syncResponse: deps.SyncResponse, logFile: deps.LogFile, anonymize: cfg.Anonymize, @@ -311,8 +311,8 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("failed to add profiles to debug bundle: %v", err) } - if err := g.addNetworkMap(); err != nil { - return fmt.Errorf("add network map: %w", err) + if err := g.addSyncResponse(); err != nil { + return fmt.Errorf("add sync response: %w", err) } if err := g.addStateFile(); err != nil { @@ -526,15 +526,15 @@ func (g *BundleGenerator) addResolvedDomains() error { return nil } -func (g *BundleGenerator) addNetworkMap() error { - if g.networkMap == nil { - log.Debugf("skipping empty network map in debug bundle") +func (g *BundleGenerator) addSyncResponse() error { + if g.syncResponse == nil { + log.Debugf("skipping empty sync response in debug bundle") return nil } if g.anonymize { - if err := anonymizeNetworkMap(g.networkMap, g.anonymizer); err != nil { - return fmt.Errorf("anonymize network map: %w", err) + if err := anonymizeSyncResponse(g.syncResponse, g.anonymizer); err != nil { + return fmt.Errorf("anonymize sync response: %w", err) } } @@ -545,13 +545,13 @@ func (g *BundleGenerator) addNetworkMap() error { AllowPartial: true, } - jsonBytes, err := options.Marshal(g.networkMap) + jsonBytes, err := options.Marshal(g.syncResponse) if err != nil { return fmt.Errorf("generate json: %w", err) } if err := g.addFileToZip(bytes.NewReader(jsonBytes), "network_map.json"); err != nil { - return fmt.Errorf("add network map to zip: %w", err) + return fmt.Errorf("add sync response to zip: %w", err) } return nil @@ -921,6 +921,88 @@ func anonymizeNetworkMap(networkMap *mgmProto.NetworkMap, anonymizer *anonymize. return nil } +func anonymizeNetbirdConfig(config *mgmProto.NetbirdConfig, anonymizer *anonymize.Anonymizer) { + for _, stun := range config.Stuns { + if stun.Uri != "" { + stun.Uri = anonymizer.AnonymizeURI(stun.Uri) + } + } + + for _, turn := range config.Turns { + if turn.HostConfig != nil && turn.HostConfig.Uri != "" { + turn.HostConfig.Uri = anonymizer.AnonymizeURI(turn.HostConfig.Uri) + } + if turn.User != "" { + turn.User = "turn-user-placeholder" + } + if turn.Password != "" { + turn.Password = "turn-password-placeholder" + } + } + + if config.Signal != nil && config.Signal.Uri != "" { + config.Signal.Uri = anonymizer.AnonymizeURI(config.Signal.Uri) + } + + if config.Relay != nil { + for i, url := range config.Relay.Urls { + config.Relay.Urls[i] = anonymizer.AnonymizeURI(url) + } + if config.Relay.TokenPayload != "" { + config.Relay.TokenPayload = "relay-token-payload-placeholder" + } + if config.Relay.TokenSignature != "" { + config.Relay.TokenSignature = "relay-token-signature-placeholder" + } + } + + if config.Flow != nil { + if config.Flow.Url != "" { + config.Flow.Url = anonymizer.AnonymizeURI(config.Flow.Url) + } + if config.Flow.TokenPayload != "" { + config.Flow.TokenPayload = "flow-token-payload-placeholder" + } + if config.Flow.TokenSignature != "" { + config.Flow.TokenSignature = "flow-token-signature-placeholder" + } + } +} + +func anonymizeSyncResponse(syncResponse *mgmProto.SyncResponse, anonymizer *anonymize.Anonymizer) error { + if syncResponse.NetbirdConfig != nil { + anonymizeNetbirdConfig(syncResponse.NetbirdConfig, anonymizer) + } + + if syncResponse.PeerConfig != nil { + anonymizePeerConfig(syncResponse.PeerConfig, anonymizer) + } + + for _, p := range syncResponse.RemotePeers { + anonymizeRemotePeer(p, anonymizer) + } + + if syncResponse.NetworkMap != nil { + if err := anonymizeNetworkMap(syncResponse.NetworkMap, anonymizer); err != nil { + return err + } + } + + for _, check := range syncResponse.Checks { + for i, file := range check.Files { + check.Files[i] = anonymizer.AnonymizeString(file) + } + } + + return nil +} + +func anonymizeSSHConfig(sshConfig *mgmProto.SSHConfig) { + if sshConfig != nil && len(sshConfig.SshPubKey) > 0 { + sshConfig.SshPubKey = []byte("ssh-placeholder-key") + } +} + func anonymizePeerConfig(config *mgmProto.PeerConfig, anonymizer *anonymize.Anonymizer) { if config == nil { return @@ -930,9 +1012,7 @@ func anonymizePeerConfig(config *mgmProto.PeerConfig, anonymizer *anonymize.Anon config.Address = anonymizer.AnonymizeIP(addr).String() } - if config.SshConfig != nil && len(config.SshConfig.SshPubKey) > 0 { - config.SshConfig.SshPubKey = []byte("ssh-placeholder-key") - } + anonymizeSSHConfig(config.SshConfig) config.Dns = anonymizer.AnonymizeString(config.Dns) config.Fqdn = anonymizer.AnonymizeDomain(config.Fqdn) @@ -954,9 +1034,7 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize. peer.Fqdn = anonymizer.AnonymizeDomain(peer.Fqdn) - if peer.SshConfig != nil && len(peer.SshConfig.SshPubKey) > 0 { - peer.SshConfig.SshPubKey = []byte("ssh-placeholder-key") - } + anonymizeSSHConfig(peer.SshConfig) } func anonymizeRoute(route *mgmProto.Route, anonymizer *anonymize.Anonymizer) { diff --git a/client/internal/engine.go b/client/internal/engine.go index f8d3c33bc..52a8eee16 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -189,11 +189,11 @@ type Engine struct { stateManager *statemanager.Manager srWatcher *guard.SRWatcher - // Network map persistence - persistNetworkMap bool - latestNetworkMap *mgmProto.NetworkMap - connSemaphore *semaphoregroup.SemaphoreGroup - flowManager nftypes.FlowManager + // Sync response persistence + persistSyncResponse bool + latestSyncResponse *mgmProto.SyncResponse + connSemaphore *semaphoregroup.SemaphoreGroup + flowManager nftypes.FlowManager } // Peer is an instance of the Connection Peer @@ -697,10 +697,10 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return nil } - // Store network map if persistence is enabled - if e.persistNetworkMap { - e.latestNetworkMap = nm - log.Debugf("network map persisted with serial %d", nm.GetSerial()) + // Store sync response if persistence is enabled + if e.persistSyncResponse { + e.latestSyncResponse = update + log.Debugf("sync response persisted with serial %d", nm.GetSerial()) } // only apply new changes and ignore old ones @@ -1765,44 +1765,43 @@ func (e *Engine) stopDNSServer() { e.statusRecorder.UpdateDNSStates(nsGroupStates) } -// SetNetworkMapPersistence enables or disables network map persistence -func (e *Engine) SetNetworkMapPersistence(enabled bool) { +// SetSyncResponsePersistence enables or disables sync response persistence +func (e *Engine) SetSyncResponsePersistence(enabled bool) { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() - if enabled == e.persistNetworkMap { + if enabled == e.persistSyncResponse { return } - e.persistNetworkMap = enabled - log.Debugf("Network map persistence is set to %t", enabled) + e.persistSyncResponse = enabled + log.Debugf("Sync response persistence is set to %t", enabled) if !enabled { - e.latestNetworkMap = nil + e.latestSyncResponse = nil } } -// GetLatestNetworkMap returns the stored network map if persistence is enabled -func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) { +// GetLatestSyncResponse returns the stored sync response if persistence is enabled +func (e *Engine) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() - if !e.persistNetworkMap { - return nil, errors.New("network map persistence is disabled") + if !e.persistSyncResponse { + return nil, errors.New("sync response persistence is disabled") } - if e.latestNetworkMap == nil { + if e.latestSyncResponse == nil { //nolint:nilnil return nil, nil } - log.Debugf("Retrieving latest network map with size %d bytes", proto.Size(e.latestNetworkMap)) - nm, ok := proto.Clone(e.latestNetworkMap).(*mgmProto.NetworkMap) + log.Debugf("Retrieving latest sync response with size %d bytes", proto.Size(e.latestSyncResponse)) + sr, ok := proto.Clone(e.latestSyncResponse).(*mgmProto.SyncResponse) if !ok { - - return nil, fmt.Errorf("failed to clone network map") + return nil, fmt.Errorf("failed to clone sync response") } - return nm, nil + return sr, nil } // GetWgAddr returns the wireguard address diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 7d5ddc8a9..691976971 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -2959,27 +2959,27 @@ func (x *DeleteStateResponse) GetDeletedStates() int32 { return 0 } -type SetNetworkMapPersistenceRequest struct { +type SetSyncResponsePersistenceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SetNetworkMapPersistenceRequest) Reset() { - *x = SetNetworkMapPersistenceRequest{} +func (x *SetSyncResponsePersistenceRequest) Reset() { + *x = SetSyncResponsePersistenceRequest{} mi := &file_daemon_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SetNetworkMapPersistenceRequest) String() string { +func (x *SetSyncResponsePersistenceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SetNetworkMapPersistenceRequest) ProtoMessage() {} +func (*SetSyncResponsePersistenceRequest) ProtoMessage() {} -func (x *SetNetworkMapPersistenceRequest) ProtoReflect() protoreflect.Message { +func (x *SetSyncResponsePersistenceRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2991,38 +2991,38 @@ func (x *SetNetworkMapPersistenceRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SetNetworkMapPersistenceRequest.ProtoReflect.Descriptor instead. -func (*SetNetworkMapPersistenceRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use SetSyncResponsePersistenceRequest.ProtoReflect.Descriptor instead. +func (*SetSyncResponsePersistenceRequest) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{42} } -func (x *SetNetworkMapPersistenceRequest) GetEnabled() bool { +func (x *SetSyncResponsePersistenceRequest) GetEnabled() bool { if x != nil { return x.Enabled } return false } -type SetNetworkMapPersistenceResponse struct { +type SetSyncResponsePersistenceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SetNetworkMapPersistenceResponse) Reset() { - *x = SetNetworkMapPersistenceResponse{} +func (x *SetSyncResponsePersistenceResponse) Reset() { + *x = SetSyncResponsePersistenceResponse{} mi := &file_daemon_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SetNetworkMapPersistenceResponse) String() string { +func (x *SetSyncResponsePersistenceResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SetNetworkMapPersistenceResponse) ProtoMessage() {} +func (*SetSyncResponsePersistenceResponse) ProtoMessage() {} -func (x *SetNetworkMapPersistenceResponse) ProtoReflect() protoreflect.Message { +func (x *SetSyncResponsePersistenceResponse) ProtoReflect() protoreflect.Message { mi := &file_daemon_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3034,8 +3034,8 @@ func (x *SetNetworkMapPersistenceResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SetNetworkMapPersistenceResponse.ProtoReflect.Descriptor instead. -func (*SetNetworkMapPersistenceResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use SetSyncResponsePersistenceResponse.ProtoReflect.Descriptor instead. +func (*SetSyncResponsePersistenceResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{43} } @@ -4727,10 +4727,10 @@ const file_daemon_proto_rawDesc = "" + "state_name\x18\x01 \x01(\tR\tstateName\x12\x10\n" + "\x03all\x18\x02 \x01(\bR\x03all\"<\n" + "\x13DeleteStateResponse\x12%\n" + - "\x0edeleted_states\x18\x01 \x01(\x05R\rdeletedStates\";\n" + - "\x1fSetNetworkMapPersistenceRequest\x12\x18\n" + - "\aenabled\x18\x01 \x01(\bR\aenabled\"\"\n" + - " SetNetworkMapPersistenceResponse\"v\n" + + "\x0edeleted_states\x18\x01 \x01(\x05R\rdeletedStates\"=\n" + + "!SetSyncResponsePersistenceRequest\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\"$\n" + + "\"SetSyncResponsePersistenceResponse\"v\n" + "\bTCPFlags\x12\x10\n" + "\x03syn\x18\x01 \x01(\bR\x03syn\x12\x10\n" + "\x03ack\x18\x02 \x01(\bR\x03ack\x12\x10\n" + @@ -4881,7 +4881,7 @@ const file_daemon_proto_rawDesc = "" + "\x04WARN\x10\x04\x12\b\n" + "\x04INFO\x10\x05\x12\t\n" + "\x05DEBUG\x10\x06\x12\t\n" + - "\x05TRACE\x10\a2\xbf\x0f\n" + + "\x05TRACE\x10\a2\xc5\x0f\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -4900,8 +4900,8 @@ const file_daemon_proto_rawDesc = "" + "ListStates\x12\x19.daemon.ListStatesRequest\x1a\x1a.daemon.ListStatesResponse\"\x00\x12E\n" + "\n" + "CleanState\x12\x19.daemon.CleanStateRequest\x1a\x1a.daemon.CleanStateResponse\"\x00\x12H\n" + - "\vDeleteState\x12\x1a.daemon.DeleteStateRequest\x1a\x1b.daemon.DeleteStateResponse\"\x00\x12o\n" + - "\x18SetNetworkMapPersistence\x12'.daemon.SetNetworkMapPersistenceRequest\x1a(.daemon.SetNetworkMapPersistenceResponse\"\x00\x12H\n" + + "\vDeleteState\x12\x1a.daemon.DeleteStateRequest\x1a\x1b.daemon.DeleteStateResponse\"\x00\x12u\n" + + "\x1aSetSyncResponsePersistence\x12).daemon.SetSyncResponsePersistenceRequest\x1a*.daemon.SetSyncResponsePersistenceResponse\"\x00\x12H\n" + "\vTracePacket\x12\x1a.daemon.TracePacketRequest\x1a\x1b.daemon.TracePacketResponse\"\x00\x12D\n" + "\x0fSubscribeEvents\x12\x18.daemon.SubscribeRequest\x1a\x13.daemon.SystemEvent\"\x000\x01\x12B\n" + "\tGetEvents\x12\x18.daemon.GetEventsRequest\x1a\x19.daemon.GetEventsResponse\"\x00\x12N\n" + @@ -4929,81 +4929,81 @@ func file_daemon_proto_rawDescGZIP() []byte { var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 70) var file_daemon_proto_goTypes = []any{ - (LogLevel)(0), // 0: daemon.LogLevel - (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity - (SystemEvent_Category)(0), // 2: daemon.SystemEvent.Category - (*EmptyRequest)(nil), // 3: daemon.EmptyRequest - (*LoginRequest)(nil), // 4: daemon.LoginRequest - (*LoginResponse)(nil), // 5: daemon.LoginResponse - (*WaitSSOLoginRequest)(nil), // 6: daemon.WaitSSOLoginRequest - (*WaitSSOLoginResponse)(nil), // 7: daemon.WaitSSOLoginResponse - (*UpRequest)(nil), // 8: daemon.UpRequest - (*UpResponse)(nil), // 9: daemon.UpResponse - (*StatusRequest)(nil), // 10: daemon.StatusRequest - (*StatusResponse)(nil), // 11: daemon.StatusResponse - (*DownRequest)(nil), // 12: daemon.DownRequest - (*DownResponse)(nil), // 13: daemon.DownResponse - (*GetConfigRequest)(nil), // 14: daemon.GetConfigRequest - (*GetConfigResponse)(nil), // 15: daemon.GetConfigResponse - (*PeerState)(nil), // 16: daemon.PeerState - (*LocalPeerState)(nil), // 17: daemon.LocalPeerState - (*SignalState)(nil), // 18: daemon.SignalState - (*ManagementState)(nil), // 19: daemon.ManagementState - (*RelayState)(nil), // 20: daemon.RelayState - (*NSGroupState)(nil), // 21: daemon.NSGroupState - (*FullStatus)(nil), // 22: daemon.FullStatus - (*ListNetworksRequest)(nil), // 23: daemon.ListNetworksRequest - (*ListNetworksResponse)(nil), // 24: daemon.ListNetworksResponse - (*SelectNetworksRequest)(nil), // 25: daemon.SelectNetworksRequest - (*SelectNetworksResponse)(nil), // 26: daemon.SelectNetworksResponse - (*IPList)(nil), // 27: daemon.IPList - (*Network)(nil), // 28: daemon.Network - (*PortInfo)(nil), // 29: daemon.PortInfo - (*ForwardingRule)(nil), // 30: daemon.ForwardingRule - (*ForwardingRulesResponse)(nil), // 31: daemon.ForwardingRulesResponse - (*DebugBundleRequest)(nil), // 32: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 33: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 34: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 35: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 36: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 37: daemon.SetLogLevelResponse - (*State)(nil), // 38: daemon.State - (*ListStatesRequest)(nil), // 39: daemon.ListStatesRequest - (*ListStatesResponse)(nil), // 40: daemon.ListStatesResponse - (*CleanStateRequest)(nil), // 41: daemon.CleanStateRequest - (*CleanStateResponse)(nil), // 42: daemon.CleanStateResponse - (*DeleteStateRequest)(nil), // 43: daemon.DeleteStateRequest - (*DeleteStateResponse)(nil), // 44: daemon.DeleteStateResponse - (*SetNetworkMapPersistenceRequest)(nil), // 45: daemon.SetNetworkMapPersistenceRequest - (*SetNetworkMapPersistenceResponse)(nil), // 46: daemon.SetNetworkMapPersistenceResponse - (*TCPFlags)(nil), // 47: daemon.TCPFlags - (*TracePacketRequest)(nil), // 48: daemon.TracePacketRequest - (*TraceStage)(nil), // 49: daemon.TraceStage - (*TracePacketResponse)(nil), // 50: daemon.TracePacketResponse - (*SubscribeRequest)(nil), // 51: daemon.SubscribeRequest - (*SystemEvent)(nil), // 52: daemon.SystemEvent - (*GetEventsRequest)(nil), // 53: daemon.GetEventsRequest - (*GetEventsResponse)(nil), // 54: daemon.GetEventsResponse - (*SwitchProfileRequest)(nil), // 55: daemon.SwitchProfileRequest - (*SwitchProfileResponse)(nil), // 56: daemon.SwitchProfileResponse - (*SetConfigRequest)(nil), // 57: daemon.SetConfigRequest - (*SetConfigResponse)(nil), // 58: daemon.SetConfigResponse - (*AddProfileRequest)(nil), // 59: daemon.AddProfileRequest - (*AddProfileResponse)(nil), // 60: daemon.AddProfileResponse - (*RemoveProfileRequest)(nil), // 61: daemon.RemoveProfileRequest - (*RemoveProfileResponse)(nil), // 62: daemon.RemoveProfileResponse - (*ListProfilesRequest)(nil), // 63: daemon.ListProfilesRequest - (*ListProfilesResponse)(nil), // 64: daemon.ListProfilesResponse - (*Profile)(nil), // 65: daemon.Profile - (*GetActiveProfileRequest)(nil), // 66: daemon.GetActiveProfileRequest - (*GetActiveProfileResponse)(nil), // 67: daemon.GetActiveProfileResponse - (*LogoutRequest)(nil), // 68: daemon.LogoutRequest - (*LogoutResponse)(nil), // 69: daemon.LogoutResponse - nil, // 70: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 71: daemon.PortInfo.Range - nil, // 72: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 73: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 74: google.protobuf.Timestamp + (LogLevel)(0), // 0: daemon.LogLevel + (SystemEvent_Severity)(0), // 1: daemon.SystemEvent.Severity + (SystemEvent_Category)(0), // 2: daemon.SystemEvent.Category + (*EmptyRequest)(nil), // 3: daemon.EmptyRequest + (*LoginRequest)(nil), // 4: daemon.LoginRequest + (*LoginResponse)(nil), // 5: daemon.LoginResponse + (*WaitSSOLoginRequest)(nil), // 6: daemon.WaitSSOLoginRequest + (*WaitSSOLoginResponse)(nil), // 7: daemon.WaitSSOLoginResponse + (*UpRequest)(nil), // 8: daemon.UpRequest + (*UpResponse)(nil), // 9: daemon.UpResponse + (*StatusRequest)(nil), // 10: daemon.StatusRequest + (*StatusResponse)(nil), // 11: daemon.StatusResponse + (*DownRequest)(nil), // 12: daemon.DownRequest + (*DownResponse)(nil), // 13: daemon.DownResponse + (*GetConfigRequest)(nil), // 14: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 15: daemon.GetConfigResponse + (*PeerState)(nil), // 16: daemon.PeerState + (*LocalPeerState)(nil), // 17: daemon.LocalPeerState + (*SignalState)(nil), // 18: daemon.SignalState + (*ManagementState)(nil), // 19: daemon.ManagementState + (*RelayState)(nil), // 20: daemon.RelayState + (*NSGroupState)(nil), // 21: daemon.NSGroupState + (*FullStatus)(nil), // 22: daemon.FullStatus + (*ListNetworksRequest)(nil), // 23: daemon.ListNetworksRequest + (*ListNetworksResponse)(nil), // 24: daemon.ListNetworksResponse + (*SelectNetworksRequest)(nil), // 25: daemon.SelectNetworksRequest + (*SelectNetworksResponse)(nil), // 26: daemon.SelectNetworksResponse + (*IPList)(nil), // 27: daemon.IPList + (*Network)(nil), // 28: daemon.Network + (*PortInfo)(nil), // 29: daemon.PortInfo + (*ForwardingRule)(nil), // 30: daemon.ForwardingRule + (*ForwardingRulesResponse)(nil), // 31: daemon.ForwardingRulesResponse + (*DebugBundleRequest)(nil), // 32: daemon.DebugBundleRequest + (*DebugBundleResponse)(nil), // 33: daemon.DebugBundleResponse + (*GetLogLevelRequest)(nil), // 34: daemon.GetLogLevelRequest + (*GetLogLevelResponse)(nil), // 35: daemon.GetLogLevelResponse + (*SetLogLevelRequest)(nil), // 36: daemon.SetLogLevelRequest + (*SetLogLevelResponse)(nil), // 37: daemon.SetLogLevelResponse + (*State)(nil), // 38: daemon.State + (*ListStatesRequest)(nil), // 39: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 40: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 41: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 42: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 43: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 44: daemon.DeleteStateResponse + (*SetSyncResponsePersistenceRequest)(nil), // 45: daemon.SetSyncResponsePersistenceRequest + (*SetSyncResponsePersistenceResponse)(nil), // 46: daemon.SetSyncResponsePersistenceResponse + (*TCPFlags)(nil), // 47: daemon.TCPFlags + (*TracePacketRequest)(nil), // 48: daemon.TracePacketRequest + (*TraceStage)(nil), // 49: daemon.TraceStage + (*TracePacketResponse)(nil), // 50: daemon.TracePacketResponse + (*SubscribeRequest)(nil), // 51: daemon.SubscribeRequest + (*SystemEvent)(nil), // 52: daemon.SystemEvent + (*GetEventsRequest)(nil), // 53: daemon.GetEventsRequest + (*GetEventsResponse)(nil), // 54: daemon.GetEventsResponse + (*SwitchProfileRequest)(nil), // 55: daemon.SwitchProfileRequest + (*SwitchProfileResponse)(nil), // 56: daemon.SwitchProfileResponse + (*SetConfigRequest)(nil), // 57: daemon.SetConfigRequest + (*SetConfigResponse)(nil), // 58: daemon.SetConfigResponse + (*AddProfileRequest)(nil), // 59: daemon.AddProfileRequest + (*AddProfileResponse)(nil), // 60: daemon.AddProfileResponse + (*RemoveProfileRequest)(nil), // 61: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 62: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 63: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 64: daemon.ListProfilesResponse + (*Profile)(nil), // 65: daemon.Profile + (*GetActiveProfileRequest)(nil), // 66: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 67: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 68: daemon.LogoutRequest + (*LogoutResponse)(nil), // 69: daemon.LogoutResponse + nil, // 70: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 71: daemon.PortInfo.Range + nil, // 72: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 73: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 74: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ 73, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration @@ -5053,7 +5053,7 @@ var file_daemon_proto_depIdxs = []int32{ 39, // 44: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest 41, // 45: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest 43, // 46: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 45, // 47: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest + 45, // 47: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest 48, // 48: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest 51, // 49: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest 53, // 50: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest @@ -5080,7 +5080,7 @@ var file_daemon_proto_depIdxs = []int32{ 40, // 71: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse 42, // 72: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse 44, // 73: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 46, // 74: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 46, // 74: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse 50, // 75: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse 52, // 76: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent 54, // 77: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 2e1e0254c..76db56459 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -59,8 +59,8 @@ service DaemonService { // Delete specific state or all states rpc DeleteState(DeleteStateRequest) returns (DeleteStateResponse) {} - // SetNetworkMapPersistence enables or disables network map persistence - rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {} + // SetSyncResponsePersistence enables or disables sync response persistence + rpc SetSyncResponsePersistence(SetSyncResponsePersistenceRequest) returns (SetSyncResponsePersistenceResponse) {} rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {} @@ -452,11 +452,11 @@ message DeleteStateResponse { } -message SetNetworkMapPersistenceRequest { +message SetSyncResponsePersistenceRequest { bool enabled = 1; } -message SetNetworkMapPersistenceResponse {} +message SetSyncResponsePersistenceResponse {} message TCPFlags { bool syn = 1; diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index edb56bd8a..6dfdfa9c3 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -50,8 +50,8 @@ type DaemonServiceClient interface { CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error) // Delete specific state or all states DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) - // SetNetworkMapPersistence enables or disables network map persistence - SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) + // SetSyncResponsePersistence enables or disables sync response persistence + SetSyncResponsePersistence(ctx context.Context, in *SetSyncResponsePersistenceRequest, opts ...grpc.CallOption) (*SetSyncResponsePersistenceResponse, error) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) @@ -217,9 +217,9 @@ func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRe return out, nil } -func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) { - out := new(SetNetworkMapPersistenceResponse) - err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetNetworkMapPersistence", in, out, opts...) +func (c *daemonServiceClient) SetSyncResponsePersistence(ctx context.Context, in *SetSyncResponsePersistenceRequest, opts ...grpc.CallOption) (*SetSyncResponsePersistenceResponse, error) { + out := new(SetSyncResponsePersistenceResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetSyncResponsePersistence", in, out, opts...) if err != nil { return nil, err } @@ -375,8 +375,8 @@ type DaemonServiceServer interface { CleanState(context.Context, *CleanStateRequest) (*CleanStateResponse, error) // Delete specific state or all states DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) - // SetNetworkMapPersistence enables or disables network map persistence - SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) + // SetSyncResponsePersistence enables or disables sync response persistence + SetSyncResponsePersistence(context.Context, *SetSyncResponsePersistenceRequest) (*SetSyncResponsePersistenceResponse, error) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) @@ -443,8 +443,8 @@ func (UnimplementedDaemonServiceServer) CleanState(context.Context, *CleanStateR func (UnimplementedDaemonServiceServer) DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteState not implemented") } -func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetNetworkMapPersistence not implemented") +func (UnimplementedDaemonServiceServer) SetSyncResponsePersistence(context.Context, *SetSyncResponsePersistenceRequest) (*SetSyncResponsePersistenceResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetSyncResponsePersistence not implemented") } func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented") @@ -777,20 +777,20 @@ func _DaemonService_DeleteState_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } -func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SetNetworkMapPersistenceRequest) +func _DaemonService_SetSyncResponsePersistence_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetSyncResponsePersistenceRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, in) + return srv.(DaemonServiceServer).SetSyncResponsePersistence(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/daemon.DaemonService/SetNetworkMapPersistence", + FullMethod: "/daemon.DaemonService/SetSyncResponsePersistence", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, req.(*SetNetworkMapPersistenceRequest)) + return srv.(DaemonServiceServer).SetSyncResponsePersistence(ctx, req.(*SetSyncResponsePersistenceRequest)) } return interceptor(ctx, in, info, handler) } @@ -1050,8 +1050,8 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ Handler: _DaemonService_DeleteState_Handler, }, { - MethodName: "SetNetworkMapPersistence", - Handler: _DaemonService_SetNetworkMapPersistence_Handler, + MethodName: "SetSyncResponsePersistence", + Handler: _DaemonService_SetSyncResponsePersistence_Handler, }, { MethodName: "TracePacket", diff --git a/client/server/debug.go b/client/server/debug.go index 412602b00..826e19f33 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -27,15 +27,16 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( s.mutex.Lock() defer s.mutex.Unlock() - networkMap, err := s.getLatestNetworkMap() + syncResponse, err := s.getLatestSyncResponse() if err != nil { - log.Warnf("failed to get latest network map: %v", err) + log.Warnf("failed to get latest sync response: %v", err) } + bundleGenerator := debug.NewBundleGenerator( debug.GeneratorDependencies{ InternalConfig: s.config, StatusRecorder: s.statusRecorder, - NetworkMap: networkMap, + SyncResponse: syncResponse, LogFile: s.logFile, }, debug.BundleConfig{ @@ -192,26 +193,25 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) ( return &proto.SetLogLevelResponse{}, nil } -// SetNetworkMapPersistence sets the network map persistence for the server. -func (s *Server) SetNetworkMapPersistence(_ context.Context, req *proto.SetNetworkMapPersistenceRequest) (*proto.SetNetworkMapPersistenceResponse, error) { +// SetSyncResponsePersistence sets the sync response persistence for the server. +func (s *Server) SetSyncResponsePersistence(_ context.Context, req *proto.SetSyncResponsePersistenceRequest) (*proto.SetSyncResponsePersistenceResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() enabled := req.GetEnabled() - s.persistNetworkMap = enabled + s.persistSyncResponse = enabled if s.connectClient != nil { - s.connectClient.SetNetworkMapPersistence(enabled) + s.connectClient.SetSyncResponsePersistence(enabled) } - return &proto.SetNetworkMapPersistenceResponse{}, nil + return &proto.SetSyncResponsePersistenceResponse{}, nil } -// getLatestNetworkMap returns the latest network map from the engine if network map persistence is enabled -func (s *Server) getLatestNetworkMap() (*mgmProto.NetworkMap, error) { +func (s *Server) getLatestSyncResponse() (*mgmProto.SyncResponse, error) { cClient := s.connectClient if cClient == nil { return nil, errors.New("connect client is not initialized") } - return cClient.GetLatestNetworkMap() + return cClient.GetLatestSyncResponse() } diff --git a/client/server/server.go b/client/server/server.go index b7c27ba5c..57a365c48 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -70,9 +70,9 @@ type Server struct { statusRecorder *peer.Status sessionWatcher *internal.SessionWatcher - lastProbe time.Time - persistNetworkMap bool - isSessionActive atomic.Bool + lastProbe time.Time + persistSyncResponse bool + isSessionActive atomic.Bool profileManager *profilemanager.ServiceManager profilesDisabled bool @@ -88,12 +88,12 @@ type oauthAuthFlow struct { // New server instance constructor. func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool) *Server { return &Server{ - rootCtx: ctx, - logFile: logFile, - persistNetworkMap: true, - statusRecorder: peer.NewRecorder(""), - profileManager: profilemanager.NewServiceManager(configFile), - profilesDisabled: profilesDisabled, + rootCtx: ctx, + logFile: logFile, + persistSyncResponse: true, + statusRecorder: peer.NewRecorder(""), + profileManager: profilemanager.NewServiceManager(configFile), + profilesDisabled: profilesDisabled, } } @@ -233,7 +233,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, config *profilemanage runOperation := func() error { log.Tracef("running client connection") s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder) - s.connectClient.SetNetworkMapPersistence(s.persistNetworkMap) + s.connectClient.SetSyncResponsePersistence(s.persistSyncResponse) err := s.connectClient.Run(runningChan) if err != nil { diff --git a/client/ui/debug.go b/client/ui/debug.go index a7f4868ac..76afc7753 100644 --- a/client/ui/debug.go +++ b/client/ui/debug.go @@ -395,12 +395,12 @@ func (s *serviceClient) configureServiceForDebug( time.Sleep(time.Second) if enablePersistence { - if _, err := conn.SetNetworkMapPersistence(s.ctx, &proto.SetNetworkMapPersistenceRequest{ + if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{ Enabled: true, }); err != nil { - return fmt.Errorf("enable network map persistence: %v", err) + return fmt.Errorf("enable sync response persistence: %v", err) } - log.Info("Network map persistence enabled for debug") + log.Info("Sync response persistence enabled for debug") } if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil { From 1d5e871bdfc8a98d3b9c1f254bda293406626f4e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:22:58 +0200 Subject: [PATCH 198/213] [misc] Move shared components to shared directory (#4286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved the following directories: ``` - management/client → shared/management/client - management/domain → shared/management/domain - management/proto → shared/management/proto - signal/client → shared/signal/client - signal/proto → shared/signal/proto - relay/client → shared/relay/client - relay/auth → shared/relay/auth ``` and adjusted import paths --- client/cmd/debug.go | 2 +- client/cmd/testutil_test.go | 4 ++-- client/cmd/up.go | 2 +- client/firewall/manager/set.go | 2 +- client/firewall/uspfilter/filter_filter_test.go | 2 +- client/firewall/uspfilter/filter_test.go | 2 +- client/internal/acl/manager.go | 4 ++-- client/internal/acl/manager_test.go | 2 +- client/internal/auth/pkce_flow_test.go | 2 +- client/internal/connect.go | 10 +++++----- client/internal/debug/debug.go | 2 +- client/internal/debug/debug_test.go | 2 +- client/internal/debug/format.go | 2 +- client/internal/device_auth.go | 2 +- client/internal/dns/local/local.go | 2 +- client/internal/dns/mock_server.go | 2 +- client/internal/dns/server.go | 2 +- client/internal/dns/server_test.go | 2 +- client/internal/dnsfwd/forwarder_test.go | 2 +- client/internal/dnsfwd/manager.go | 2 +- client/internal/engine.go | 14 +++++++------- client/internal/engine_test.go | 10 +++++----- client/internal/login.go | 4 ++-- client/internal/message_convert.go | 2 +- client/internal/peer/conn.go | 2 +- client/internal/peer/signaler.go | 4 ++-- client/internal/peer/status.go | 4 ++-- client/internal/peer/worker_relay.go | 2 +- client/internal/pkce_auth.go | 4 ++-- client/internal/profilemanager/config.go | 4 ++-- .../routemanager/dnsinterceptor/handler.go | 2 +- client/internal/routemanager/dynamic/route.go | 2 +- .../internal/routemanager/dynamic/route_generic.go | 2 +- client/internal/routemanager/dynamic/route_ios.go | 2 +- client/internal/routemanager/manager.go | 2 +- client/ios/NetBirdSDK/client.go | 2 +- client/server/debug.go | 2 +- client/server/network.go | 2 +- client/server/server.go | 4 ++-- client/server/server_test.go | 4 ++-- client/status/status.go | 2 +- client/system/info.go | 2 +- go.mod | 4 ++-- go.sum | 8 ++++---- management/cmd/management.go | 2 +- management/server/account/manager.go | 2 +- management/server/dns.go | 2 +- management/server/grpcserver.go | 2 +- .../server/http/handlers/routes/routes_handler.go | 2 +- .../http/handlers/routes/routes_handler_test.go | 2 +- .../integrations/integrated_validator/interface.go | 2 +- management/server/management_proto_test.go | 2 +- management/server/management_test.go | 2 +- management/server/mock_server/account_mock.go | 2 +- .../server/mock_server/management_server_mock.go | 2 +- .../server/networks/resources/types/resource.go | 2 +- management/server/peer.go | 4 ++-- management/server/peer_test.go | 4 ++-- management/server/policy.go | 2 +- management/server/route.go | 4 ++-- management/server/route_test.go | 2 +- management/server/token_mgr.go | 6 +++--- management/server/token_mgr_test.go | 2 +- management/server/types/account.go | 2 +- management/server/types/config.go | 2 +- management/server/types/network.go | 2 +- management/server/types/policyrule.go | 2 +- management/server/types/route_firewall_rule.go | 2 +- management/server/updatechannel.go | 2 +- management/server/updatechannel_test.go | 2 +- relay/cmd/root.go | 2 +- relay/test/benchmark_test.go | 6 +++--- relay/testec2/relay.go | 4 ++-- route/route.go | 2 +- {management => shared/management}/client/client.go | 4 ++-- .../management}/client/client_test.go | 2 +- .../management}/client/common/types.go | 0 shared/management/client/go.sum | 3 +++ {management => shared/management}/client/grpc.go | 4 ++-- {management => shared/management}/client/mock.go | 4 ++-- .../management}/client/rest/accounts.go | 0 .../management}/client/rest/accounts_test.go | 2 +- .../management}/client/rest/client.go | 0 .../management}/client/rest/client_test.go | 2 +- .../management}/client/rest/dns.go | 0 .../management}/client/rest/dns_test.go | 2 +- .../management}/client/rest/events.go | 0 .../management}/client/rest/events_test.go | 2 +- .../management}/client/rest/geo.go | 0 .../management}/client/rest/geo_test.go | 2 +- .../management}/client/rest/groups.go | 0 .../management}/client/rest/groups_test.go | 2 +- .../management}/client/rest/impersonation.go | 0 .../management}/client/rest/impersonation_test.go | 2 +- .../management}/client/rest/networks.go | 0 .../management}/client/rest/networks_test.go | 2 +- .../management}/client/rest/options.go | 0 .../management}/client/rest/peers.go | 0 .../management}/client/rest/peers_test.go | 2 +- .../management}/client/rest/policies.go | 0 .../management}/client/rest/policies_test.go | 2 +- .../management}/client/rest/posturechecks.go | 0 .../management}/client/rest/posturechecks_test.go | 2 +- .../management}/client/rest/routes.go | 0 .../management}/client/rest/routes_test.go | 2 +- .../management}/client/rest/setupkeys.go | 0 .../management}/client/rest/setupkeys_test.go | 2 +- .../management}/client/rest/tokens.go | 0 .../management}/client/rest/tokens_test.go | 2 +- .../management}/client/rest/users.go | 0 .../management}/client/rest/users_test.go | 2 +- {management => shared/management}/domain/domain.go | 0 shared/management/domain/go.sum | 1 + {management => shared/management}/domain/list.go | 0 .../management}/domain/list_test.go | 0 .../management}/domain/validate.go | 0 .../management}/domain/validate_test.go | 0 .../management}/proto/generate.sh | 0 shared/management/proto/go.sum | 2 ++ .../management}/proto/management.pb.go | 0 .../management}/proto/management.proto | 0 .../management}/proto/management_grpc.pb.go | 0 {relay => shared/relay}/auth/allow/allow_all.go | 0 {relay => shared/relay}/auth/doc.go | 0 shared/relay/auth/go.sum | 1 + {relay => shared/relay}/auth/hmac/doc.go | 0 {relay => shared/relay}/auth/hmac/store.go | 2 +- {relay => shared/relay}/auth/hmac/token.go | 0 {relay => shared/relay}/auth/hmac/token_test.go | 0 {relay => shared/relay}/auth/hmac/v2/algo.go | 0 {relay => shared/relay}/auth/hmac/v2/generator.go | 0 {relay => shared/relay}/auth/hmac/v2/hmac_test.go | 0 {relay => shared/relay}/auth/hmac/v2/token.go | 0 {relay => shared/relay}/auth/hmac/v2/validator.go | 0 {relay => shared/relay}/auth/hmac/validator.go | 0 {relay => shared/relay}/auth/validator.go | 4 ++-- {relay => shared/relay}/client/addr.go | 0 {relay => shared/relay}/client/client.go | 8 ++++---- {relay => shared/relay}/client/client_test.go | 4 ++-- {relay => shared/relay}/client/conn.go | 0 {relay => shared/relay}/client/dialer/net/err.go | 0 {relay => shared/relay}/client/dialer/quic/conn.go | 2 +- {relay => shared/relay}/client/dialer/quic/quic.go | 0 .../relay}/client/dialer/race_dialer.go | 0 .../relay}/client/dialer/race_dialer_test.go | 0 {relay => shared/relay}/client/dialer/ws/addr.go | 0 {relay => shared/relay}/client/dialer/ws/conn.go | 0 {relay => shared/relay}/client/dialer/ws/ws.go | 0 {relay => shared/relay}/client/doc.go | 0 shared/relay/client/go.sum | 10 ++++++++++ {relay => shared/relay}/client/guard.go | 0 {relay => shared/relay}/client/manager.go | 2 +- {relay => shared/relay}/client/manager_test.go | 2 +- .../relay}/client/peer_subscription.go | 0 .../relay}/client/peer_subscription_test.go | 0 {relay => shared/relay}/client/picker.go | 2 +- {relay => shared/relay}/client/picker_test.go | 0 {signal => shared/signal}/client/client.go | 2 +- .../signal}/client/client_suite_test.go | 0 {signal => shared/signal}/client/client_test.go | 2 +- shared/signal/client/go.sum | 10 ++++++++++ {signal => shared/signal}/client/grpc.go | 4 ++-- {signal => shared/signal}/client/mock.go | 2 +- {signal => shared/signal}/proto/constants.go | 0 {signal => shared/signal}/proto/generate.sh | 0 shared/signal/proto/go.sum | 2 ++ .../signal}/proto/signalexchange.pb.go | 0 .../signal}/proto/signalexchange.proto | 0 .../signal}/proto/signalexchange_grpc.pb.go | 0 signal/cmd/run.go | 2 +- signal/peer/peer.go | 2 +- signal/server/signal.go | 2 +- 172 files changed, 181 insertions(+), 152 deletions(-) rename {management => shared/management}/client/client.go (88%) rename {management => shared/management}/client/client_test.go (99%) rename {management => shared/management}/client/common/types.go (100%) create mode 100644 shared/management/client/go.sum rename {management => shared/management}/client/grpc.go (99%) rename {management => shared/management}/client/mock.go (96%) rename {management => shared/management}/client/rest/accounts.go (100%) rename {management => shared/management}/client/rest/accounts_test.go (98%) rename {management => shared/management}/client/rest/client.go (100%) rename {management => shared/management}/client/rest/client_test.go (92%) rename {management => shared/management}/client/rest/dns.go (100%) rename {management => shared/management}/client/rest/dns_test.go (99%) rename {management => shared/management}/client/rest/events.go (100%) rename {management => shared/management}/client/rest/events_test.go (96%) rename {management => shared/management}/client/rest/geo.go (100%) rename {management => shared/management}/client/rest/geo_test.go (97%) rename {management => shared/management}/client/rest/groups.go (100%) rename {management => shared/management}/client/rest/groups_test.go (99%) rename {management => shared/management}/client/rest/impersonation.go (100%) rename {management => shared/management}/client/rest/impersonation_test.go (97%) rename {management => shared/management}/client/rest/networks.go (100%) rename {management => shared/management}/client/rest/networks_test.go (99%) rename {management => shared/management}/client/rest/options.go (100%) rename {management => shared/management}/client/rest/peers.go (100%) rename {management => shared/management}/client/rest/peers_test.go (99%) rename {management => shared/management}/client/rest/policies.go (100%) rename {management => shared/management}/client/rest/policies_test.go (99%) rename {management => shared/management}/client/rest/posturechecks.go (100%) rename {management => shared/management}/client/rest/posturechecks_test.go (99%) rename {management => shared/management}/client/rest/routes.go (100%) rename {management => shared/management}/client/rest/routes_test.go (99%) rename {management => shared/management}/client/rest/setupkeys.go (100%) rename {management => shared/management}/client/rest/setupkeys_test.go (99%) rename {management => shared/management}/client/rest/tokens.go (100%) rename {management => shared/management}/client/rest/tokens_test.go (98%) rename {management => shared/management}/client/rest/users.go (100%) rename {management => shared/management}/client/rest/users_test.go (99%) rename {management => shared/management}/domain/domain.go (100%) create mode 100644 shared/management/domain/go.sum rename {management => shared/management}/domain/list.go (100%) rename {management => shared/management}/domain/list_test.go (100%) rename {management => shared/management}/domain/validate.go (100%) rename {management => shared/management}/domain/validate_test.go (100%) rename {management => shared/management}/proto/generate.sh (100%) create mode 100644 shared/management/proto/go.sum rename {management => shared/management}/proto/management.pb.go (100%) rename {management => shared/management}/proto/management.proto (100%) rename {management => shared/management}/proto/management_grpc.pb.go (100%) rename {relay => shared/relay}/auth/allow/allow_all.go (100%) rename {relay => shared/relay}/auth/doc.go (100%) create mode 100644 shared/relay/auth/go.sum rename {relay => shared/relay}/auth/hmac/doc.go (100%) rename {relay => shared/relay}/auth/hmac/store.go (92%) rename {relay => shared/relay}/auth/hmac/token.go (100%) rename {relay => shared/relay}/auth/hmac/token_test.go (100%) rename {relay => shared/relay}/auth/hmac/v2/algo.go (100%) rename {relay => shared/relay}/auth/hmac/v2/generator.go (100%) rename {relay => shared/relay}/auth/hmac/v2/hmac_test.go (100%) rename {relay => shared/relay}/auth/hmac/v2/token.go (100%) rename {relay => shared/relay}/auth/hmac/v2/validator.go (100%) rename {relay => shared/relay}/auth/hmac/validator.go (100%) rename {relay => shared/relay}/auth/validator.go (83%) rename {relay => shared/relay}/client/addr.go (100%) rename {relay => shared/relay}/client/client.go (98%) rename {relay => shared/relay}/client/client_test.go (99%) rename {relay => shared/relay}/client/conn.go (100%) rename {relay => shared/relay}/client/dialer/net/err.go (100%) rename {relay => shared/relay}/client/dialer/quic/conn.go (96%) rename {relay => shared/relay}/client/dialer/quic/quic.go (100%) rename {relay => shared/relay}/client/dialer/race_dialer.go (100%) rename {relay => shared/relay}/client/dialer/race_dialer_test.go (100%) rename {relay => shared/relay}/client/dialer/ws/addr.go (100%) rename {relay => shared/relay}/client/dialer/ws/conn.go (100%) rename {relay => shared/relay}/client/dialer/ws/ws.go (100%) rename {relay => shared/relay}/client/doc.go (100%) create mode 100644 shared/relay/client/go.sum rename {relay => shared/relay}/client/guard.go (100%) rename {relay => shared/relay}/client/manager.go (99%) rename {relay => shared/relay}/client/manager_test.go (99%) rename {relay => shared/relay}/client/peer_subscription.go (100%) rename {relay => shared/relay}/client/peer_subscription_test.go (100%) rename {relay => shared/relay}/client/picker.go (97%) rename {relay => shared/relay}/client/picker_test.go (100%) rename {signal => shared/signal}/client/client.go (97%) rename {signal => shared/signal}/client/client_suite_test.go (100%) rename {signal => shared/signal}/client/client_test.go (98%) create mode 100644 shared/signal/client/go.sum rename {signal => shared/signal}/client/grpc.go (99%) rename {signal => shared/signal}/client/mock.go (97%) rename {signal => shared/signal}/proto/constants.go (100%) rename {signal => shared/signal}/proto/generate.sh (100%) create mode 100644 shared/signal/proto/go.sum rename {signal => shared/signal}/proto/signalexchange.pb.go (100%) rename {signal => shared/signal}/proto/signalexchange.proto (100%) rename {signal => shared/signal}/proto/signalexchange_grpc.pb.go (100%) diff --git a/client/cmd/debug.go b/client/cmd/debug.go index 556b3bb25..c59847717 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -17,7 +17,7 @@ import ( "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" nbstatus "github.com/netbirdio/netbird/client/status" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/upload-server/types" ) diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index a6be84641..47804a102 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -26,9 +26,9 @@ import ( clientProto "github.com/netbirdio/netbird/client/proto" client "github.com/netbirdio/netbird/client/server" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" mgmt "github.com/netbirdio/netbird/management/server" - sigProto "github.com/netbirdio/netbird/signal/proto" + sigProto "github.com/netbirdio/netbird/shared/signal/proto" sig "github.com/netbirdio/netbird/signal/server" ) diff --git a/client/cmd/up.go b/client/cmd/up.go index 35ce77539..b62925e5e 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -23,7 +23,7 @@ import ( "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/system" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/util" ) diff --git a/client/firewall/manager/set.go b/client/firewall/manager/set.go index 4c88f6eac..dda93bf47 100644 --- a/client/firewall/manager/set.go +++ b/client/firewall/manager/set.go @@ -9,7 +9,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) type Set struct { diff --git a/client/firewall/uspfilter/filter_filter_test.go b/client/firewall/uspfilter/filter_filter_test.go index b630c9e66..009860f73 100644 --- a/client/firewall/uspfilter/filter_filter_test.go +++ b/client/firewall/uspfilter/filter_filter_test.go @@ -15,7 +15,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/mocks" "github.com/netbirdio/netbird/client/iface/wgaddr" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) func TestPeerACLFiltering(t *testing.T) { diff --git a/client/firewall/uspfilter/filter_test.go b/client/firewall/uspfilter/filter_test.go index 5b5cd5a53..3197be4e8 100644 --- a/client/firewall/uspfilter/filter_test.go +++ b/client/firewall/uspfilter/filter_test.go @@ -20,7 +20,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/netflow" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) var logger = log.NewFromLogrus(logrus.StandardLogger()) diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 32dc7fbb8..5ca950297 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -18,8 +18,8 @@ import ( firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/internal/acl/id" "github.com/netbirdio/netbird/client/ssh" - "github.com/netbirdio/netbird/management/domain" - mgmProto "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/domain" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) var ErrSourceRangesEmpty = errors.New("sources range is empty") diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index b378de8c8..664476ef4 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -12,7 +12,7 @@ import ( "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/acl/mocks" "github.com/netbirdio/netbird/client/internal/netflow" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger() diff --git a/client/internal/auth/pkce_flow_test.go b/client/internal/auth/pkce_flow_test.go index f42cce51f..b2347d12d 100644 --- a/client/internal/auth/pkce_flow_test.go +++ b/client/internal/auth/pkce_flow_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/client/internal" - mgm "github.com/netbirdio/netbird/management/client/common" + mgm "github.com/netbirdio/netbird/shared/management/client/common" ) func TestPromptLogin(t *testing.T) { diff --git a/client/internal/connect.go b/client/internal/connect.go index e1f6f2bbe..d6be285f9 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -26,11 +26,11 @@ import ( cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" - mgm "github.com/netbirdio/netbird/management/client" - mgmProto "github.com/netbirdio/netbird/management/proto" - "github.com/netbirdio/netbird/relay/auth/hmac" - relayClient "github.com/netbirdio/netbird/relay/client" - signal "github.com/netbirdio/netbird/signal/client" + mgm "github.com/netbirdio/netbird/shared/management/client" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" + "github.com/netbirdio/netbird/shared/relay/auth/hmac" + relayClient "github.com/netbirdio/netbird/shared/relay/client" + signal "github.com/netbirdio/netbird/shared/signal/client" "github.com/netbirdio/netbird/util" nbnet "github.com/netbirdio/netbird/util/net" "github.com/netbirdio/netbird/version" diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 70786583e..ec920c5f3 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -27,7 +27,7 @@ import ( "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/profilemanager" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/util" ) diff --git a/client/internal/debug/debug_test.go b/client/internal/debug/debug_test.go index eb91fed66..59837c328 100644 --- a/client/internal/debug/debug_test.go +++ b/client/internal/debug/debug_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/client/anonymize" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) func TestAnonymizeStateFile(t *testing.T) { diff --git a/client/internal/debug/format.go b/client/internal/debug/format.go index 54fc77f93..aae1f221f 100644 --- a/client/internal/debug/format.go +++ b/client/internal/debug/format.go @@ -10,7 +10,7 @@ import ( "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string { diff --git a/client/internal/device_auth.go b/client/internal/device_auth.go index 8e68f7544..6bd29801d 100644 --- a/client/internal/device_auth.go +++ b/client/internal/device_auth.go @@ -10,7 +10,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - mgm "github.com/netbirdio/netbird/management/client" + mgm "github.com/netbirdio/netbird/shared/management/client" ) // DeviceAuthorizationFlow represents Device Authorization Flow information diff --git a/client/internal/dns/local/local.go b/client/internal/dns/local/local.go index c19356a7e..b776fbbe3 100644 --- a/client/internal/dns/local/local.go +++ b/client/internal/dns/local/local.go @@ -12,7 +12,7 @@ import ( "github.com/netbirdio/netbird/client/internal/dns/types" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) type Resolver struct { diff --git a/client/internal/dns/mock_server.go b/client/internal/dns/mock_server.go index 40a2e7384..9e0d06399 100644 --- a/client/internal/dns/mock_server.go +++ b/client/internal/dns/mock_server.go @@ -7,7 +7,7 @@ import ( "github.com/miekg/dns" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) // MockServer is the mock instance of a dns server diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index e5f29d807..cd0757dc1 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -21,7 +21,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) // ReadyListener is a notification mechanism what indicate the server is ready to handle host dns address changes diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 50444a86f..3ac1b6eb1 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -32,7 +32,7 @@ import ( "github.com/netbirdio/netbird/client/internal/stdnet" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/formatter" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger() diff --git a/client/internal/dnsfwd/forwarder_test.go b/client/internal/dnsfwd/forwarder_test.go index d8228c733..c820fbb60 100644 --- a/client/internal/dnsfwd/forwarder_test.go +++ b/client/internal/dnsfwd/forwarder_test.go @@ -16,7 +16,7 @@ import ( firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/internal/dns/test" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index 91abce823..bf2ee839b 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -11,7 +11,7 @@ import ( nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/client/internal/engine.go b/client/internal/engine.go index 52a8eee16..943738c22 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -49,19 +49,19 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" cProto "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" nbssh "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" - mgm "github.com/netbirdio/netbird/management/client" - mgmProto "github.com/netbirdio/netbird/management/proto" - auth "github.com/netbirdio/netbird/relay/auth/hmac" - relayClient "github.com/netbirdio/netbird/relay/client" + mgm "github.com/netbirdio/netbird/shared/management/client" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" + auth "github.com/netbirdio/netbird/shared/relay/auth/hmac" + relayClient "github.com/netbirdio/netbird/shared/relay/client" "github.com/netbirdio/netbird/route" - signal "github.com/netbirdio/netbird/signal/client" - sProto "github.com/netbirdio/netbird/signal/proto" + signal "github.com/netbirdio/netbird/shared/signal/client" + sProto "github.com/netbirdio/netbird/shared/signal/proto" "github.com/netbirdio/netbird/util" ) diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 2ac531662..0406fe6dc 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -43,8 +43,8 @@ import ( "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" - mgmt "github.com/netbirdio/netbird/management/client" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmt "github.com/netbirdio/netbird/shared/management/client" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" @@ -54,10 +54,10 @@ import ( "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/monotime" - relayClient "github.com/netbirdio/netbird/relay/client" + relayClient "github.com/netbirdio/netbird/shared/relay/client" "github.com/netbirdio/netbird/route" - signal "github.com/netbirdio/netbird/signal/client" - "github.com/netbirdio/netbird/signal/proto" + signal "github.com/netbirdio/netbird/shared/signal/client" + "github.com/netbirdio/netbird/shared/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" "github.com/netbirdio/netbird/util" ) diff --git a/client/internal/login.go b/client/internal/login.go index 7c96e4081..d5412a110 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -13,8 +13,8 @@ import ( "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" - mgm "github.com/netbirdio/netbird/management/client" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgm "github.com/netbirdio/netbird/shared/management/client" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) // IsLoginRequired check that the server is support SSO or not diff --git a/client/internal/message_convert.go b/client/internal/message_convert.go index 8ad93bfb9..97da32c06 100644 --- a/client/internal/message_convert.go +++ b/client/internal/message_convert.go @@ -7,7 +7,7 @@ import ( "net/netip" firewallManager "github.com/netbirdio/netbird/client/firewall/manager" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" ) func convertToFirewallProtocol(protocol mgmProto.RuleProtocol) (firewallManager.Protocol, error) { diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index ddd90450d..d5a55bc58 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -24,7 +24,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer/id" "github.com/netbirdio/netbird/client/internal/peer/worker" "github.com/netbirdio/netbird/client/internal/stdnet" - relayClient "github.com/netbirdio/netbird/relay/client" + relayClient "github.com/netbirdio/netbird/shared/relay/client" "github.com/netbirdio/netbird/route" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" ) diff --git a/client/internal/peer/signaler.go b/client/internal/peer/signaler.go index 9022e0299..58df66fcf 100644 --- a/client/internal/peer/signaler.go +++ b/client/internal/peer/signaler.go @@ -4,8 +4,8 @@ import ( "github.com/pion/ice/v3" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - signal "github.com/netbirdio/netbird/signal/client" - sProto "github.com/netbirdio/netbird/signal/proto" + signal "github.com/netbirdio/netbird/shared/signal/client" + sProto "github.com/netbirdio/netbird/shared/signal/proto" ) type Signaler struct { diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index e290ef75f..bd4e63357 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -21,8 +21,8 @@ import ( "github.com/netbirdio/netbird/client/internal/ingressgw" "github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/management/domain" - relayClient "github.com/netbirdio/netbird/relay/client" + "github.com/netbirdio/netbird/shared/management/domain" + relayClient "github.com/netbirdio/netbird/shared/relay/client" "github.com/netbirdio/netbird/route" ) diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go index ef9f24a2b..f584487f5 100644 --- a/client/internal/peer/worker_relay.go +++ b/client/internal/peer/worker_relay.go @@ -9,7 +9,7 @@ import ( log "github.com/sirupsen/logrus" - relayClient "github.com/netbirdio/netbird/relay/client" + relayClient "github.com/netbirdio/netbird/shared/relay/client" ) type RelayConnInfo struct { diff --git a/client/internal/pkce_auth.go b/client/internal/pkce_auth.go index f020d7a91..a713bb342 100644 --- a/client/internal/pkce_auth.go +++ b/client/internal/pkce_auth.go @@ -11,8 +11,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - mgm "github.com/netbirdio/netbird/management/client" - "github.com/netbirdio/netbird/management/client/common" + mgm "github.com/netbirdio/netbird/shared/management/client" + "github.com/netbirdio/netbird/shared/management/client/common" ) // PKCEAuthorizationFlow represents PKCE Authorization Flow information diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index df6b93402..806595574 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -20,8 +20,8 @@ import ( "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" "github.com/netbirdio/netbird/client/ssh" - mgm "github.com/netbirdio/netbird/management/client" - "github.com/netbirdio/netbird/management/domain" + mgm "github.com/netbirdio/netbird/shared/management/client" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/util" ) diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index c7c3aeb0b..ba27df654 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -22,7 +22,7 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager/common" "github.com/netbirdio/netbird/client/internal/routemanager/fakeip" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go index 5d561f0cf..587e05c74 100644 --- a/client/internal/routemanager/dynamic/route.go +++ b/client/internal/routemanager/dynamic/route.go @@ -18,7 +18,7 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager/iface" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/util" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/client/internal/routemanager/dynamic/route_generic.go b/client/internal/routemanager/dynamic/route_generic.go index a618a2392..56fd63fba 100644 --- a/client/internal/routemanager/dynamic/route_generic.go +++ b/client/internal/routemanager/dynamic/route_generic.go @@ -5,7 +5,7 @@ package dynamic import ( "net" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) func (r *Route) getIPsFromResolver(domain domain.Domain) ([]net.IP, error) { diff --git a/client/internal/routemanager/dynamic/route_ios.go b/client/internal/routemanager/dynamic/route_ios.go index 34949b626..8fed1c8f9 100644 --- a/client/internal/routemanager/dynamic/route_ios.go +++ b/client/internal/routemanager/dynamic/route_ios.go @@ -11,7 +11,7 @@ import ( nbdns "github.com/netbirdio/netbird/client/internal/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" ) const dialTimeout = 10 * time.Second diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index e51778811..da5534902 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -36,7 +36,7 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager/vars" "github.com/netbirdio/netbird/client/internal/routeselector" "github.com/netbirdio/netbird/client/internal/statemanager" - relayClient "github.com/netbirdio/netbird/relay/client" + relayClient "github.com/netbirdio/netbird/shared/relay/client" "github.com/netbirdio/netbird/route" nbnet "github.com/netbirdio/netbird/util/net" "github.com/netbirdio/netbird/version" diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go index fe0f6034e..2109d4b15 100644 --- a/client/ios/NetBirdSDK/client.go +++ b/client/ios/NetBirdSDK/client.go @@ -20,7 +20,7 @@ import ( "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/client/server/debug.go b/client/server/debug.go index 826e19f33..056d9df21 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -16,7 +16,7 @@ import ( "github.com/netbirdio/netbird/client/internal/debug" "github.com/netbirdio/netbird/client/proto" - mgmProto "github.com/netbirdio/netbird/management/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/upload-server/types" ) diff --git a/client/server/network.go b/client/server/network.go index 93b7caa46..18b16795d 100644 --- a/client/server/network.go +++ b/client/server/network.go @@ -11,7 +11,7 @@ import ( "golang.org/x/exp/maps" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/client/server/server.go b/client/server/server.go index 57a365c48..fadf2e7fc 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -26,8 +26,8 @@ import ( "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/system" - mgm "github.com/netbirdio/netbird/management/client" - "github.com/netbirdio/netbird/management/domain" + mgm "github.com/netbirdio/netbird/shared/management/client" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" diff --git a/client/server/server_test.go b/client/server/server_test.go index c0a3621ec..a88ca5412 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -24,7 +24,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/profilemanager" daemonProto "github.com/netbirdio/netbird/client/proto" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" @@ -33,7 +33,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" ) diff --git a/client/status/status.go b/client/status/status.go index 722ee7e7c..db5b7dc0b 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -16,7 +16,7 @@ import ( "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/version" ) diff --git a/client/system/info.go b/client/system/info.go index aff10ece3..ea3f6063a 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -8,7 +8,7 @@ import ( "google.golang.org/grpc/metadata" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" ) // DeviceNameCtxKey context key for device name diff --git a/go.mod b/go.mod index 70ae9abc2..c6a795424 100644 --- a/go.mod +++ b/go.mod @@ -63,8 +63,8 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250805075620-df61803a4461 - github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb + github.com/netbirdio/management-integrations/integrations v0.0.0-20250805121557-5f225a973d1f + github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index 8e7ab2bdf..db7918e24 100644 --- a/go.sum +++ b/go.sum @@ -503,12 +503,12 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250805075620-df61803a4461 h1:86MeBP7UxexphbOWpO5dxf5SLKlpGPYTDGR4m5WJ/Ew= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250805075620-df61803a4461/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250805121557-5f225a973d1f h1:YmqNWdRbeVn1lSpkLzIiFHX2cndRuaVYyynx2ibrOtg= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250805121557-5f225a973d1f/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= -github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= -github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= +github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ= +github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ= github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= diff --git a/management/cmd/management.go b/management/cmd/management.go index e6fe5c81c..a695767ad 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -40,7 +40,7 @@ import ( "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/formatter/hook" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/auth" nbContext "github.com/netbirdio/netbird/management/server/context" diff --git a/management/server/account/manager.go b/management/server/account/manager.go index 17e269e35..ee82346f3 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -7,7 +7,7 @@ import ( "time" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/activity" nbcache "github.com/netbirdio/netbird/management/server/cache" nbcontext "github.com/netbirdio/netbird/management/server/context" diff --git a/management/server/dns.go b/management/server/dns.go index a3f32c2a9..019282e65 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -8,7 +8,7 @@ import ( log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index b121cc993..c52e8a3d7 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -24,7 +24,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/auth" diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go index ea731d9d8..eba06fbec 100644 --- a/management/server/http/handlers/routes/routes_handler.go +++ b/management/server/http/handlers/routes/routes_handler.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go index ad1f8912d..bed42514f 100644 --- a/management/server/http/handlers/routes/routes_handler_test.go +++ b/management/server/http/handlers/routes/routes_handler_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/mock_server" diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go index 945cd7131..ce632d567 100644 --- a/management/server/integrations/integrated_validator/interface.go +++ b/management/server/integrations/integrated_validator/interface.go @@ -3,7 +3,7 @@ package integrated_validator import ( "context" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 0d61b3a10..b896c52da 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -22,7 +22,7 @@ import ( "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/formatter/hook" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/permissions" diff --git a/management/server/management_test.go b/management/server/management_test.go index ab6f0095b..1be6b377d 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -20,7 +20,7 @@ import ( "google.golang.org/grpc/keepalive" "github.com/netbirdio/netbird/encryption" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index d72ca8c2b..1ae432412 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -10,7 +10,7 @@ import ( "google.golang.org/grpc/status" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbcontext "github.com/netbirdio/netbird/management/server/context" diff --git a/management/server/mock_server/management_server_mock.go b/management/server/mock_server/management_server_mock.go index d79fbd4e9..45049f1fe 100644 --- a/management/server/mock_server/management_server_mock.go +++ b/management/server/mock_server/management_server_mock.go @@ -6,7 +6,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" ) type ManagementServiceServerMock struct { diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 04c63608d..3033a1fa0 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -8,7 +8,7 @@ import ( "github.com/rs/xid" - nbDomain "github.com/netbirdio/netbird/management/domain" + nbDomain "github.com/netbirdio/netbird/shared/management/domain" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" diff --git a/management/server/peer.go b/management/server/peer.go index ddf54bd59..a8c944dce 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -17,7 +17,7 @@ import ( "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/idp" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" @@ -29,7 +29,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 947e53a60..46df48345 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -38,8 +38,8 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" diff --git a/management/server/policy.go b/management/server/policy.go index 1e9331d43..b5440266f 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -6,7 +6,7 @@ import ( "github.com/rs/xid" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/store" diff --git a/management/server/route.go b/management/server/route.go index 32ff39977..ee5835a8b 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -9,8 +9,8 @@ import ( "github.com/rs/xid" - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" diff --git a/management/server/route_test.go b/management/server/route_test.go index ffd1a284b..ac51c25fd 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" diff --git a/management/server/token_mgr.go b/management/server/token_mgr.go index 2f1243512..6f6e20b48 100644 --- a/management/server/token_mgr.go +++ b/management/server/token_mgr.go @@ -11,11 +11,11 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/types" - auth "github.com/netbirdio/netbird/relay/auth/hmac" - authv2 "github.com/netbirdio/netbird/relay/auth/hmac/v2" + auth "github.com/netbirdio/netbird/shared/relay/auth/hmac" + authv2 "github.com/netbirdio/netbird/shared/relay/auth/hmac/v2" integrationsConfig "github.com/netbirdio/management-integrations/integrations/config" ) diff --git a/management/server/token_mgr_test.go b/management/server/token_mgr_test.go index b2184717d..8bd757565 100644 --- a/management/server/token_mgr_test.go +++ b/management/server/token_mgr_test.go @@ -13,7 +13,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/util" diff --git a/management/server/types/account.go b/management/server/types/account.go index a3a7ce305..18259b220 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -16,7 +16,7 @@ import ( log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" diff --git a/management/server/types/config.go b/management/server/types/config.go index 7df7f3874..bb1dddbb1 100644 --- a/management/server/types/config.go +++ b/management/server/types/config.go @@ -3,7 +3,7 @@ package types import ( "net/netip" - "github.com/netbirdio/netbird/management/client/common" + "github.com/netbirdio/netbird/shared/management/client/common" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/util" ) diff --git a/management/server/types/network.go b/management/server/types/network.go index 5d725c2a4..4b0602cd6 100644 --- a/management/server/types/network.go +++ b/management/server/types/network.go @@ -12,7 +12,7 @@ import ( "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/types/policyrule.go b/management/server/types/policyrule.go index b86732415..2643ae45c 100644 --- a/management/server/types/policyrule.go +++ b/management/server/types/policyrule.go @@ -1,7 +1,7 @@ package types import ( - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" ) // PolicyUpdateOperationType operation type diff --git a/management/server/types/route_firewall_rule.go b/management/server/types/route_firewall_rule.go index c09c64a3d..6eb391cb5 100644 --- a/management/server/types/route_firewall_rule.go +++ b/management/server/types/route_firewall_rule.go @@ -1,7 +1,7 @@ package types import ( - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/route" ) diff --git a/management/server/updatechannel.go b/management/server/updatechannel.go index a85650136..da12f1b70 100644 --- a/management/server/updatechannel.go +++ b/management/server/updatechannel.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/updatechannel_test.go b/management/server/updatechannel_test.go index 69f5b895c..0dc86563d 100644 --- a/management/server/updatechannel_test.go +++ b/management/server/updatechannel_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/proto" ) // var peersUpdater *PeersUpdateManager diff --git a/relay/cmd/root.go b/relay/cmd/root.go index 7b8e5bbeb..c662dfbb7 100644 --- a/relay/cmd/root.go +++ b/relay/cmd/root.go @@ -17,7 +17,7 @@ import ( "github.com/spf13/cobra" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/relay/auth" + "github.com/netbirdio/netbird/shared/relay/auth" "github.com/netbirdio/netbird/relay/server" "github.com/netbirdio/netbird/signal/metrics" "github.com/netbirdio/netbird/util" diff --git a/relay/test/benchmark_test.go b/relay/test/benchmark_test.go index afbb14b84..6b8a6f701 100644 --- a/relay/test/benchmark_test.go +++ b/relay/test/benchmark_test.go @@ -13,9 +13,9 @@ import ( "github.com/pion/logging" "github.com/pion/turn/v3" - "github.com/netbirdio/netbird/relay/auth/allow" - "github.com/netbirdio/netbird/relay/auth/hmac" - "github.com/netbirdio/netbird/relay/client" + "github.com/netbirdio/netbird/shared/relay/auth/allow" + "github.com/netbirdio/netbird/shared/relay/auth/hmac" + "github.com/netbirdio/netbird/shared/relay/client" "github.com/netbirdio/netbird/relay/server" "github.com/netbirdio/netbird/util" ) diff --git a/relay/testec2/relay.go b/relay/testec2/relay.go index 9e22a80ea..aa0fc662a 100644 --- a/relay/testec2/relay.go +++ b/relay/testec2/relay.go @@ -11,8 +11,8 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/relay/auth/hmac" - "github.com/netbirdio/netbird/relay/client" + "github.com/netbirdio/netbird/shared/relay/auth/hmac" + "github.com/netbirdio/netbird/shared/relay/client" ) var ( diff --git a/route/route.go b/route/route.go index 722dacc2d..c46dff8c8 100644 --- a/route/route.go +++ b/route/route.go @@ -6,7 +6,7 @@ import ( "slices" "strings" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/status" ) diff --git a/management/client/client.go b/shared/management/client/client.go similarity index 88% rename from management/client/client.go rename to shared/management/client/client.go index 3a50a155b..3126bcd1f 100644 --- a/management/client/client.go +++ b/shared/management/client/client.go @@ -7,8 +7,8 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/system" - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" ) type Client interface { diff --git a/management/client/client_test.go b/shared/management/client/client_test.go similarity index 99% rename from management/client/client_test.go rename to shared/management/client/client_test.go index 5b2a87492..6431ccd45 100644 --- a/management/client/client_test.go +++ b/shared/management/client/client_test.go @@ -26,7 +26,7 @@ import ( "github.com/netbirdio/management-integrations/integrations" "github.com/netbirdio/netbird/encryption" - mgmtProto "github.com/netbirdio/netbird/management/proto" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" mgmt "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/mock_server" diff --git a/management/client/common/types.go b/shared/management/client/common/types.go similarity index 100% rename from management/client/common/types.go rename to shared/management/client/common/types.go diff --git a/shared/management/client/go.sum b/shared/management/client/go.sum new file mode 100644 index 000000000..4badfd6cb --- /dev/null +++ b/shared/management/client/go.sum @@ -0,0 +1,3 @@ +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= diff --git a/management/client/grpc.go b/shared/management/client/grpc.go similarity index 99% rename from management/client/grpc.go rename to shared/management/client/grpc.go index f181d8b46..7e3004945 100644 --- a/management/client/grpc.go +++ b/shared/management/client/grpc.go @@ -19,8 +19,8 @@ import ( "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" nbgrpc "github.com/netbirdio/netbird/util/grpc" ) diff --git a/management/client/mock.go b/shared/management/client/mock.go similarity index 96% rename from management/client/mock.go rename to shared/management/client/mock.go index 8e1a13705..29006c9c3 100644 --- a/management/client/mock.go +++ b/shared/management/client/mock.go @@ -6,8 +6,8 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/system" - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" ) type MockClient struct { diff --git a/management/client/rest/accounts.go b/shared/management/client/rest/accounts.go similarity index 100% rename from management/client/rest/accounts.go rename to shared/management/client/rest/accounts.go diff --git a/management/client/rest/accounts_test.go b/shared/management/client/rest/accounts_test.go similarity index 98% rename from management/client/rest/accounts_test.go rename to shared/management/client/rest/accounts_test.go index d2ace4ec9..ae45de7ab 100644 --- a/management/client/rest/accounts_test.go +++ b/shared/management/client/rest/accounts_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/client.go b/shared/management/client/rest/client.go similarity index 100% rename from management/client/rest/client.go rename to shared/management/client/rest/client.go diff --git a/management/client/rest/client_test.go b/shared/management/client/rest/client_test.go similarity index 92% rename from management/client/rest/client_test.go rename to shared/management/client/rest/client_test.go index 70e6c73e1..437ffa836 100644 --- a/management/client/rest/client_test.go +++ b/shared/management/client/rest/client_test.go @@ -8,7 +8,7 @@ import ( "net/http/httptest" "testing" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" ) diff --git a/management/client/rest/dns.go b/shared/management/client/rest/dns.go similarity index 100% rename from management/client/rest/dns.go rename to shared/management/client/rest/dns.go diff --git a/management/client/rest/dns_test.go b/shared/management/client/rest/dns_test.go similarity index 99% rename from management/client/rest/dns_test.go rename to shared/management/client/rest/dns_test.go index b2e0a0bee..e467659c1 100644 --- a/management/client/rest/dns_test.go +++ b/shared/management/client/rest/dns_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/events.go b/shared/management/client/rest/events.go similarity index 100% rename from management/client/rest/events.go rename to shared/management/client/rest/events.go diff --git a/management/client/rest/events_test.go b/shared/management/client/rest/events_test.go similarity index 96% rename from management/client/rest/events_test.go rename to shared/management/client/rest/events_test.go index 2589193a2..f3c5627f2 100644 --- a/management/client/rest/events_test.go +++ b/shared/management/client/rest/events_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/geo.go b/shared/management/client/rest/geo.go similarity index 100% rename from management/client/rest/geo.go rename to shared/management/client/rest/geo.go diff --git a/management/client/rest/geo_test.go b/shared/management/client/rest/geo_test.go similarity index 97% rename from management/client/rest/geo_test.go rename to shared/management/client/rest/geo_test.go index d24405094..909744b47 100644 --- a/management/client/rest/geo_test.go +++ b/shared/management/client/rest/geo_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/groups.go b/shared/management/client/rest/groups.go similarity index 100% rename from management/client/rest/groups.go rename to shared/management/client/rest/groups.go diff --git a/management/client/rest/groups_test.go b/shared/management/client/rest/groups_test.go similarity index 99% rename from management/client/rest/groups_test.go rename to shared/management/client/rest/groups_test.go index d6a5410e0..8b8207def 100644 --- a/management/client/rest/groups_test.go +++ b/shared/management/client/rest/groups_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/impersonation.go b/shared/management/client/rest/impersonation.go similarity index 100% rename from management/client/rest/impersonation.go rename to shared/management/client/rest/impersonation.go diff --git a/management/client/rest/impersonation_test.go b/shared/management/client/rest/impersonation_test.go similarity index 97% rename from management/client/rest/impersonation_test.go rename to shared/management/client/rest/impersonation_test.go index 69c0f9728..e5739b8f5 100644 --- a/management/client/rest/impersonation_test.go +++ b/shared/management/client/rest/impersonation_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" ) diff --git a/management/client/rest/networks.go b/shared/management/client/rest/networks.go similarity index 100% rename from management/client/rest/networks.go rename to shared/management/client/rest/networks.go diff --git a/management/client/rest/networks_test.go b/shared/management/client/rest/networks_test.go similarity index 99% rename from management/client/rest/networks_test.go rename to shared/management/client/rest/networks_test.go index 0772d7540..45cc81d8d 100644 --- a/management/client/rest/networks_test.go +++ b/shared/management/client/rest/networks_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/options.go b/shared/management/client/rest/options.go similarity index 100% rename from management/client/rest/options.go rename to shared/management/client/rest/options.go diff --git a/management/client/rest/peers.go b/shared/management/client/rest/peers.go similarity index 100% rename from management/client/rest/peers.go rename to shared/management/client/rest/peers.go diff --git a/management/client/rest/peers_test.go b/shared/management/client/rest/peers_test.go similarity index 99% rename from management/client/rest/peers_test.go rename to shared/management/client/rest/peers_test.go index f31e44e10..5701455de 100644 --- a/management/client/rest/peers_test.go +++ b/shared/management/client/rest/peers_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/policies.go b/shared/management/client/rest/policies.go similarity index 100% rename from management/client/rest/policies.go rename to shared/management/client/rest/policies.go diff --git a/management/client/rest/policies_test.go b/shared/management/client/rest/policies_test.go similarity index 99% rename from management/client/rest/policies_test.go rename to shared/management/client/rest/policies_test.go index 5792048df..6254077e9 100644 --- a/management/client/rest/policies_test.go +++ b/shared/management/client/rest/policies_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/posturechecks.go b/shared/management/client/rest/posturechecks.go similarity index 100% rename from management/client/rest/posturechecks.go rename to shared/management/client/rest/posturechecks.go diff --git a/management/client/rest/posturechecks_test.go b/shared/management/client/rest/posturechecks_test.go similarity index 99% rename from management/client/rest/posturechecks_test.go rename to shared/management/client/rest/posturechecks_test.go index a891d6ac9..c8489ac31 100644 --- a/management/client/rest/posturechecks_test.go +++ b/shared/management/client/rest/posturechecks_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/routes.go b/shared/management/client/rest/routes.go similarity index 100% rename from management/client/rest/routes.go rename to shared/management/client/rest/routes.go diff --git a/management/client/rest/routes_test.go b/shared/management/client/rest/routes_test.go similarity index 99% rename from management/client/rest/routes_test.go rename to shared/management/client/rest/routes_test.go index 1c698a7fb..dc83f6ba4 100644 --- a/management/client/rest/routes_test.go +++ b/shared/management/client/rest/routes_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/setupkeys.go b/shared/management/client/rest/setupkeys.go similarity index 100% rename from management/client/rest/setupkeys.go rename to shared/management/client/rest/setupkeys.go diff --git a/management/client/rest/setupkeys_test.go b/shared/management/client/rest/setupkeys_test.go similarity index 99% rename from management/client/rest/setupkeys_test.go rename to shared/management/client/rest/setupkeys_test.go index 8edce8428..eafdc54a5 100644 --- a/management/client/rest/setupkeys_test.go +++ b/shared/management/client/rest/setupkeys_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/tokens.go b/shared/management/client/rest/tokens.go similarity index 100% rename from management/client/rest/tokens.go rename to shared/management/client/rest/tokens.go diff --git a/management/client/rest/tokens_test.go b/shared/management/client/rest/tokens_test.go similarity index 98% rename from management/client/rest/tokens_test.go rename to shared/management/client/rest/tokens_test.go index eea55d22f..8abbca0a9 100644 --- a/management/client/rest/tokens_test.go +++ b/shared/management/client/rest/tokens_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/client/rest/users.go b/shared/management/client/rest/users.go similarity index 100% rename from management/client/rest/users.go rename to shared/management/client/rest/users.go diff --git a/management/client/rest/users_test.go b/shared/management/client/rest/users_test.go similarity index 99% rename from management/client/rest/users_test.go rename to shared/management/client/rest/users_test.go index 715eb1661..28f955380 100644 --- a/management/client/rest/users_test.go +++ b/shared/management/client/rest/users_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" ) diff --git a/management/domain/domain.go b/shared/management/domain/domain.go similarity index 100% rename from management/domain/domain.go rename to shared/management/domain/domain.go diff --git a/shared/management/domain/go.sum b/shared/management/domain/go.sum new file mode 100644 index 000000000..4ce43119c --- /dev/null +++ b/shared/management/domain/go.sum @@ -0,0 +1 @@ +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= diff --git a/management/domain/list.go b/shared/management/domain/list.go similarity index 100% rename from management/domain/list.go rename to shared/management/domain/list.go diff --git a/management/domain/list_test.go b/shared/management/domain/list_test.go similarity index 100% rename from management/domain/list_test.go rename to shared/management/domain/list_test.go diff --git a/management/domain/validate.go b/shared/management/domain/validate.go similarity index 100% rename from management/domain/validate.go rename to shared/management/domain/validate.go diff --git a/management/domain/validate_test.go b/shared/management/domain/validate_test.go similarity index 100% rename from management/domain/validate_test.go rename to shared/management/domain/validate_test.go diff --git a/management/proto/generate.sh b/shared/management/proto/generate.sh similarity index 100% rename from management/proto/generate.sh rename to shared/management/proto/generate.sh diff --git a/shared/management/proto/go.sum b/shared/management/proto/go.sum new file mode 100644 index 000000000..66d866626 --- /dev/null +++ b/shared/management/proto/go.sum @@ -0,0 +1,2 @@ +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/management/proto/management.pb.go b/shared/management/proto/management.pb.go similarity index 100% rename from management/proto/management.pb.go rename to shared/management/proto/management.pb.go diff --git a/management/proto/management.proto b/shared/management/proto/management.proto similarity index 100% rename from management/proto/management.proto rename to shared/management/proto/management.proto diff --git a/management/proto/management_grpc.pb.go b/shared/management/proto/management_grpc.pb.go similarity index 100% rename from management/proto/management_grpc.pb.go rename to shared/management/proto/management_grpc.pb.go diff --git a/relay/auth/allow/allow_all.go b/shared/relay/auth/allow/allow_all.go similarity index 100% rename from relay/auth/allow/allow_all.go rename to shared/relay/auth/allow/allow_all.go diff --git a/relay/auth/doc.go b/shared/relay/auth/doc.go similarity index 100% rename from relay/auth/doc.go rename to shared/relay/auth/doc.go diff --git a/shared/relay/auth/go.sum b/shared/relay/auth/go.sum new file mode 100644 index 000000000..938ef5547 --- /dev/null +++ b/shared/relay/auth/go.sum @@ -0,0 +1 @@ +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= diff --git a/relay/auth/hmac/doc.go b/shared/relay/auth/hmac/doc.go similarity index 100% rename from relay/auth/hmac/doc.go rename to shared/relay/auth/hmac/doc.go diff --git a/relay/auth/hmac/store.go b/shared/relay/auth/hmac/store.go similarity index 92% rename from relay/auth/hmac/store.go rename to shared/relay/auth/hmac/store.go index 169b8d6b0..f177b5b06 100644 --- a/relay/auth/hmac/store.go +++ b/shared/relay/auth/hmac/store.go @@ -5,7 +5,7 @@ import ( "fmt" "sync" - v2 "github.com/netbirdio/netbird/relay/auth/hmac/v2" + v2 "github.com/netbirdio/netbird/shared/relay/auth/hmac/v2" ) // TokenStore is a simple in-memory store for token diff --git a/relay/auth/hmac/token.go b/shared/relay/auth/hmac/token.go similarity index 100% rename from relay/auth/hmac/token.go rename to shared/relay/auth/hmac/token.go diff --git a/relay/auth/hmac/token_test.go b/shared/relay/auth/hmac/token_test.go similarity index 100% rename from relay/auth/hmac/token_test.go rename to shared/relay/auth/hmac/token_test.go diff --git a/relay/auth/hmac/v2/algo.go b/shared/relay/auth/hmac/v2/algo.go similarity index 100% rename from relay/auth/hmac/v2/algo.go rename to shared/relay/auth/hmac/v2/algo.go diff --git a/relay/auth/hmac/v2/generator.go b/shared/relay/auth/hmac/v2/generator.go similarity index 100% rename from relay/auth/hmac/v2/generator.go rename to shared/relay/auth/hmac/v2/generator.go diff --git a/relay/auth/hmac/v2/hmac_test.go b/shared/relay/auth/hmac/v2/hmac_test.go similarity index 100% rename from relay/auth/hmac/v2/hmac_test.go rename to shared/relay/auth/hmac/v2/hmac_test.go diff --git a/relay/auth/hmac/v2/token.go b/shared/relay/auth/hmac/v2/token.go similarity index 100% rename from relay/auth/hmac/v2/token.go rename to shared/relay/auth/hmac/v2/token.go diff --git a/relay/auth/hmac/v2/validator.go b/shared/relay/auth/hmac/v2/validator.go similarity index 100% rename from relay/auth/hmac/v2/validator.go rename to shared/relay/auth/hmac/v2/validator.go diff --git a/relay/auth/hmac/validator.go b/shared/relay/auth/hmac/validator.go similarity index 100% rename from relay/auth/hmac/validator.go rename to shared/relay/auth/hmac/validator.go diff --git a/relay/auth/validator.go b/shared/relay/auth/validator.go similarity index 83% rename from relay/auth/validator.go rename to shared/relay/auth/validator.go index 56a20fbfe..8e339bb2e 100644 --- a/relay/auth/validator.go +++ b/shared/relay/auth/validator.go @@ -3,8 +3,8 @@ package auth import ( "time" - auth "github.com/netbirdio/netbird/relay/auth/hmac" - authv2 "github.com/netbirdio/netbird/relay/auth/hmac/v2" + auth "github.com/netbirdio/netbird/shared/relay/auth/hmac" + authv2 "github.com/netbirdio/netbird/shared/relay/auth/hmac/v2" ) type TimedHMACValidator struct { diff --git a/relay/client/addr.go b/shared/relay/client/addr.go similarity index 100% rename from relay/client/addr.go rename to shared/relay/client/addr.go diff --git a/relay/client/client.go b/shared/relay/client/client.go similarity index 98% rename from relay/client/client.go rename to shared/relay/client/client.go index e4db278f5..f9f5b70e4 100644 --- a/relay/client/client.go +++ b/shared/relay/client/client.go @@ -9,10 +9,10 @@ import ( log "github.com/sirupsen/logrus" - auth "github.com/netbirdio/netbird/relay/auth/hmac" - "github.com/netbirdio/netbird/relay/client/dialer" - "github.com/netbirdio/netbird/relay/client/dialer/quic" - "github.com/netbirdio/netbird/relay/client/dialer/ws" + auth "github.com/netbirdio/netbird/shared/relay/auth/hmac" + "github.com/netbirdio/netbird/shared/relay/client/dialer" + "github.com/netbirdio/netbird/shared/relay/client/dialer/quic" + "github.com/netbirdio/netbird/shared/relay/client/dialer/ws" "github.com/netbirdio/netbird/relay/healthcheck" "github.com/netbirdio/netbird/relay/messages" ) diff --git a/relay/client/client_test.go b/shared/relay/client/client_test.go similarity index 99% rename from relay/client/client_test.go rename to shared/relay/client/client_test.go index 2ce8d7e34..c7c5fbf2b 100644 --- a/relay/client/client_test.go +++ b/shared/relay/client/client_test.go @@ -10,8 +10,8 @@ import ( log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" - "github.com/netbirdio/netbird/relay/auth/allow" - "github.com/netbirdio/netbird/relay/auth/hmac" + "github.com/netbirdio/netbird/shared/relay/auth/allow" + "github.com/netbirdio/netbird/shared/relay/auth/hmac" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/relay/server" diff --git a/relay/client/conn.go b/shared/relay/client/conn.go similarity index 100% rename from relay/client/conn.go rename to shared/relay/client/conn.go diff --git a/relay/client/dialer/net/err.go b/shared/relay/client/dialer/net/err.go similarity index 100% rename from relay/client/dialer/net/err.go rename to shared/relay/client/dialer/net/err.go diff --git a/relay/client/dialer/quic/conn.go b/shared/relay/client/dialer/quic/conn.go similarity index 96% rename from relay/client/dialer/quic/conn.go rename to shared/relay/client/dialer/quic/conn.go index d64633c8c..9243605b5 100644 --- a/relay/client/dialer/quic/conn.go +++ b/shared/relay/client/dialer/quic/conn.go @@ -10,7 +10,7 @@ import ( "github.com/quic-go/quic-go" log "github.com/sirupsen/logrus" - netErr "github.com/netbirdio/netbird/relay/client/dialer/net" + netErr "github.com/netbirdio/netbird/shared/relay/client/dialer/net" ) const ( diff --git a/relay/client/dialer/quic/quic.go b/shared/relay/client/dialer/quic/quic.go similarity index 100% rename from relay/client/dialer/quic/quic.go rename to shared/relay/client/dialer/quic/quic.go diff --git a/relay/client/dialer/race_dialer.go b/shared/relay/client/dialer/race_dialer.go similarity index 100% rename from relay/client/dialer/race_dialer.go rename to shared/relay/client/dialer/race_dialer.go diff --git a/relay/client/dialer/race_dialer_test.go b/shared/relay/client/dialer/race_dialer_test.go similarity index 100% rename from relay/client/dialer/race_dialer_test.go rename to shared/relay/client/dialer/race_dialer_test.go diff --git a/relay/client/dialer/ws/addr.go b/shared/relay/client/dialer/ws/addr.go similarity index 100% rename from relay/client/dialer/ws/addr.go rename to shared/relay/client/dialer/ws/addr.go diff --git a/relay/client/dialer/ws/conn.go b/shared/relay/client/dialer/ws/conn.go similarity index 100% rename from relay/client/dialer/ws/conn.go rename to shared/relay/client/dialer/ws/conn.go diff --git a/relay/client/dialer/ws/ws.go b/shared/relay/client/dialer/ws/ws.go similarity index 100% rename from relay/client/dialer/ws/ws.go rename to shared/relay/client/dialer/ws/ws.go diff --git a/relay/client/doc.go b/shared/relay/client/doc.go similarity index 100% rename from relay/client/doc.go rename to shared/relay/client/doc.go diff --git a/shared/relay/client/go.sum b/shared/relay/client/go.sum new file mode 100644 index 000000000..dc9715262 --- /dev/null +++ b/shared/relay/client/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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= diff --git a/relay/client/guard.go b/shared/relay/client/guard.go similarity index 100% rename from relay/client/guard.go rename to shared/relay/client/guard.go diff --git a/relay/client/manager.go b/shared/relay/client/manager.go similarity index 99% rename from relay/client/manager.go rename to shared/relay/client/manager.go index f32bb9f26..f3428f255 100644 --- a/relay/client/manager.go +++ b/shared/relay/client/manager.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" - relayAuth "github.com/netbirdio/netbird/relay/auth/hmac" + relayAuth "github.com/netbirdio/netbird/shared/relay/auth/hmac" ) var ( diff --git a/relay/client/manager_test.go b/shared/relay/client/manager_test.go similarity index 99% rename from relay/client/manager_test.go rename to shared/relay/client/manager_test.go index d0075f982..674555ff4 100644 --- a/relay/client/manager_test.go +++ b/shared/relay/client/manager_test.go @@ -8,7 +8,7 @@ import ( log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" - "github.com/netbirdio/netbird/relay/auth/allow" + "github.com/netbirdio/netbird/shared/relay/auth/allow" "github.com/netbirdio/netbird/relay/server" ) diff --git a/relay/client/peer_subscription.go b/shared/relay/client/peer_subscription.go similarity index 100% rename from relay/client/peer_subscription.go rename to shared/relay/client/peer_subscription.go diff --git a/relay/client/peer_subscription_test.go b/shared/relay/client/peer_subscription_test.go similarity index 100% rename from relay/client/peer_subscription_test.go rename to shared/relay/client/peer_subscription_test.go diff --git a/relay/client/picker.go b/shared/relay/client/picker.go similarity index 97% rename from relay/client/picker.go rename to shared/relay/client/picker.go index 9565425a8..1cad466ba 100644 --- a/relay/client/picker.go +++ b/shared/relay/client/picker.go @@ -9,7 +9,7 @@ import ( log "github.com/sirupsen/logrus" - auth "github.com/netbirdio/netbird/relay/auth/hmac" + auth "github.com/netbirdio/netbird/shared/relay/auth/hmac" ) const ( diff --git a/relay/client/picker_test.go b/shared/relay/client/picker_test.go similarity index 100% rename from relay/client/picker_test.go rename to shared/relay/client/picker_test.go diff --git a/signal/client/client.go b/shared/signal/client/client.go similarity index 97% rename from signal/client/client.go rename to shared/signal/client/client.go index eff1ccb87..184666575 100644 --- a/signal/client/client.go +++ b/shared/signal/client/client.go @@ -6,7 +6,7 @@ import ( "io" "strings" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/signal/proto" "github.com/netbirdio/netbird/version" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" diff --git a/signal/client/client_suite_test.go b/shared/signal/client/client_suite_test.go similarity index 100% rename from signal/client/client_suite_test.go rename to shared/signal/client/client_suite_test.go diff --git a/signal/client/client_test.go b/shared/signal/client/client_test.go similarity index 98% rename from signal/client/client_test.go rename to shared/signal/client/client_test.go index f7d4ebc50..1af34e37a 100644 --- a/signal/client/client_test.go +++ b/shared/signal/client/client_test.go @@ -16,7 +16,7 @@ import ( "google.golang.org/grpc/keepalive" "google.golang.org/grpc/metadata" - sigProto "github.com/netbirdio/netbird/signal/proto" + sigProto "github.com/netbirdio/netbird/shared/signal/proto" "github.com/netbirdio/netbird/signal/server" ) diff --git a/shared/signal/client/go.sum b/shared/signal/client/go.sum new file mode 100644 index 000000000..961f68d3d --- /dev/null +++ b/shared/signal/client/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +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= diff --git a/signal/client/grpc.go b/shared/signal/client/grpc.go similarity index 99% rename from signal/client/grpc.go rename to shared/signal/client/grpc.go index 2ff84e460..c7ae1444f 100644 --- a/signal/client/grpc.go +++ b/shared/signal/client/grpc.go @@ -17,8 +17,8 @@ import ( "google.golang.org/grpc/status" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/management/client" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/management/client" + "github.com/netbirdio/netbird/shared/signal/proto" nbgrpc "github.com/netbirdio/netbird/util/grpc" ) diff --git a/signal/client/mock.go b/shared/signal/client/mock.go similarity index 97% rename from signal/client/mock.go rename to shared/signal/client/mock.go index 32236c82c..95381a5b0 100644 --- a/signal/client/mock.go +++ b/shared/signal/client/mock.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/signal/proto" ) type MockClient struct { diff --git a/signal/proto/constants.go b/shared/signal/proto/constants.go similarity index 100% rename from signal/proto/constants.go rename to shared/signal/proto/constants.go diff --git a/signal/proto/generate.sh b/shared/signal/proto/generate.sh similarity index 100% rename from signal/proto/generate.sh rename to shared/signal/proto/generate.sh diff --git a/shared/signal/proto/go.sum b/shared/signal/proto/go.sum new file mode 100644 index 000000000..66d866626 --- /dev/null +++ b/shared/signal/proto/go.sum @@ -0,0 +1,2 @@ +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/signal/proto/signalexchange.pb.go b/shared/signal/proto/signalexchange.pb.go similarity index 100% rename from signal/proto/signalexchange.pb.go rename to shared/signal/proto/signalexchange.pb.go diff --git a/signal/proto/signalexchange.proto b/shared/signal/proto/signalexchange.proto similarity index 100% rename from signal/proto/signalexchange.proto rename to shared/signal/proto/signalexchange.proto diff --git a/signal/proto/signalexchange_grpc.pb.go b/shared/signal/proto/signalexchange_grpc.pb.go similarity index 100% rename from signal/proto/signalexchange_grpc.pb.go rename to shared/signal/proto/signalexchange_grpc.pb.go diff --git a/signal/cmd/run.go b/signal/cmd/run.go index 39bc8331f..2e89b491a 100644 --- a/signal/cmd/run.go +++ b/signal/cmd/run.go @@ -19,7 +19,7 @@ import ( "github.com/netbirdio/netbird/signal/metrics" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/signal/proto" "github.com/netbirdio/netbird/signal/server" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/version" diff --git a/signal/peer/peer.go b/signal/peer/peer.go index 2252e877e..f21c95a41 100644 --- a/signal/peer/peer.go +++ b/signal/peer/peer.go @@ -8,7 +8,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/signal/metrics" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/signal/proto" ) // Peer representation of a connected Peer diff --git a/signal/server/signal.go b/signal/server/signal.go index 9dfba7798..8ae14822b 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -17,7 +17,7 @@ import ( "github.com/netbirdio/netbird/signal/metrics" "github.com/netbirdio/netbird/signal/peer" - "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/shared/signal/proto" ) const ( From abd152ee5a66abbe2b04dbe1bbd9fa14fbdc1977 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:34:41 +0200 Subject: [PATCH 199/213] [misc] Separate shared code dependencies (#4288) * Separate shared code dependencies * Fix import * Test respective shared code * Update openapi ref * Fix test * Fix test path --- .../workflows/check-license-dependencies.yml | 41 +++++++++++++++++++ .github/workflows/golang-test-linux.yml | 12 +++--- .github/workflows/update-docs.yml | 2 +- client/internal/profilemanager/config.go | 1 - formatter/hook/hook.go | 2 +- management/server/account.go | 2 +- management/server/context/keys.go | 10 +++-- management/server/dns.go | 2 +- management/server/dns_test.go | 2 +- management/server/event.go | 2 +- management/server/geolocation/store.go | 2 +- management/server/group.go | 2 +- management/server/group_test.go | 2 +- management/server/groups/manager.go | 2 +- management/server/grpcserver.go | 2 +- .../handlers/accounts/accounts_handler.go | 6 +-- .../accounts/accounts_handler_test.go | 4 +- .../http/handlers/dns/dns_settings_handler.go | 4 +- .../handlers/dns/dns_settings_handler_test.go | 4 +- .../http/handlers/dns/nameservers_handler.go | 6 +-- .../handlers/dns/nameservers_handler_test.go | 4 +- .../http/handlers/events/events_handler.go | 4 +- .../handlers/events/events_handler_test.go | 2 +- .../http/handlers/groups/groups_handler.go | 6 +-- .../handlers/groups/groups_handler_test.go | 6 +-- .../server/http/handlers/networks/handler.go | 6 +-- .../handlers/networks/resources_handler.go | 4 +- .../http/handlers/networks/routers_handler.go | 4 +- .../http/handlers/peers/peers_handler.go | 6 +-- .../http/handlers/peers/peers_handler_test.go | 2 +- .../policies/geolocation_handler_test.go | 2 +- .../handlers/policies/geolocations_handler.go | 6 +-- .../handlers/policies/policies_handler.go | 6 +-- .../policies/policies_handler_test.go | 4 +- .../policies/posture_checks_handler.go | 6 +-- .../policies/posture_checks_handler_test.go | 4 +- .../http/handlers/routes/routes_handler.go | 6 +-- .../handlers/routes/routes_handler_test.go | 4 +- .../handlers/setup_keys/setupkeys_handler.go | 6 +-- .../setup_keys/setupkeys_handler_test.go | 4 +- .../server/http/handlers/users/pat_handler.go | 6 +-- .../http/handlers/users/pat_handler_test.go | 4 +- .../http/handlers/users/users_handler.go | 6 +-- .../http/handlers/users/users_handler_test.go | 4 +- .../server/http/middleware/auth_middleware.go | 4 +- .../peers_handler_benchmark_test.go | 2 +- .../setupkeys_handler_benchmark_test.go | 2 +- .../users_handler_benchmark_test.go | 2 +- .../setupkeys_handler_integration_test.go | 2 +- management/server/nameserver.go | 2 +- management/server/networks/manager.go | 2 +- .../server/networks/resources/manager.go | 2 +- .../server/networks/resources/manager_test.go | 2 +- .../networks/resources/types/resource.go | 2 +- management/server/networks/routers/manager.go | 2 +- .../server/networks/routers/manager_test.go | 2 +- .../server/networks/routers/types/router.go | 2 +- management/server/networks/types/network.go | 2 +- management/server/peer.go | 2 +- management/server/peer_test.go | 2 +- management/server/peers/manager.go | 2 +- management/server/permissions/manager.go | 2 +- management/server/policy.go | 2 +- management/server/posture/checks.go | 4 +- management/server/posture/network.go | 2 +- management/server/posture_checks.go | 2 +- management/server/route.go | 2 +- management/server/settings/manager.go | 2 +- management/server/setupkey.go | 2 +- management/server/store/sql_store.go | 2 +- management/server/store/sql_store_test.go | 2 +- management/server/types/account.go | 2 +- management/server/types/network.go | 2 +- management/server/types/resource.go | 2 +- management/server/user.go | 2 +- management/server/user_test.go | 2 +- relay/server/handshake.go | 6 +-- relay/server/listener/ws/listener.go | 4 +- relay/server/peer.go | 4 +- relay/server/server.go | 2 +- relay/server/store/listener.go | 2 +- relay/server/store/notifier.go | 2 +- relay/server/store/store.go | 2 +- relay/server/store/store_test.go | 2 +- route/route.go | 2 +- shared/context/keys.go | 8 ++++ shared/management/client/client_test.go | 4 +- shared/management/client/rest/accounts.go | 2 +- .../management/client/rest/accounts_test.go | 4 +- shared/management/client/rest/client.go | 2 +- shared/management/client/rest/client_test.go | 2 +- shared/management/client/rest/dns.go | 2 +- shared/management/client/rest/dns_test.go | 4 +- shared/management/client/rest/events.go | 2 +- shared/management/client/rest/events_test.go | 4 +- shared/management/client/rest/geo.go | 2 +- shared/management/client/rest/geo_test.go | 4 +- shared/management/client/rest/groups.go | 2 +- shared/management/client/rest/groups_test.go | 4 +- .../client/rest/impersonation_test.go | 2 +- shared/management/client/rest/networks.go | 2 +- .../management/client/rest/networks_test.go | 4 +- shared/management/client/rest/peers.go | 2 +- shared/management/client/rest/peers_test.go | 4 +- shared/management/client/rest/policies.go | 2 +- .../management/client/rest/policies_test.go | 4 +- .../management/client/rest/posturechecks.go | 2 +- .../client/rest/posturechecks_test.go | 4 +- shared/management/client/rest/routes.go | 2 +- shared/management/client/rest/routes_test.go | 4 +- shared/management/client/rest/setupkeys.go | 2 +- .../management/client/rest/setupkeys_test.go | 4 +- shared/management/client/rest/tokens.go | 2 +- shared/management/client/rest/tokens_test.go | 4 +- shared/management/client/rest/users.go | 2 +- shared/management/client/rest/users_test.go | 4 +- shared/management/domain/go.sum | 1 - .../management}/http/api/cfg.yaml | 0 .../management}/http/api/generate.sh | 0 .../management}/http/api/openapi.yml | 0 .../management}/http/api/types.gen.go | 0 .../management}/http/util/util.go | 2 +- shared/management/operations/operation.go | 4 ++ .../management}/status/error.go | 2 +- shared/relay/client/client.go | 4 +- shared/relay/client/conn.go | 2 +- shared/relay/client/dialer/quic/quic.go | 2 +- shared/relay/client/dialer/ws/ws.go | 4 +- shared/relay/client/peer_subscription.go | 2 +- shared/relay/client/peer_subscription_test.go | 2 +- shared/relay/constants.go | 6 +++ {relay => shared/relay}/healthcheck/doc.go | 0 .../relay}/healthcheck/receiver.go | 0 .../relay}/healthcheck/receiver_test.go | 0 {relay => shared/relay}/healthcheck/sender.go | 0 .../relay}/healthcheck/sender_test.go | 0 .../relay}/messages/address/address.go | 0 {relay => shared/relay}/messages/auth/auth.go | 0 {relay => shared/relay}/messages/doc.go | 0 {relay => shared/relay}/messages/id.go | 0 {relay => shared/relay}/messages/message.go | 0 .../relay}/messages/message_test.go | 0 .../relay}/messages/peer_state.go | 0 .../relay}/messages/peer_state_test.go | 0 {relay => shared/relay}/tls/alpn.go | 0 {relay => shared/relay}/tls/client_dev.go | 0 {relay => shared/relay}/tls/client_prod.go | 0 {relay => shared/relay}/tls/doc.go | 0 {relay => shared/relay}/tls/server_dev.go | 0 {relay => shared/relay}/tls/server_prod.go | 0 150 files changed, 252 insertions(+), 191 deletions(-) create mode 100644 .github/workflows/check-license-dependencies.yml create mode 100644 shared/context/keys.go delete mode 100644 shared/management/domain/go.sum rename {management/server => shared/management}/http/api/cfg.yaml (100%) rename {management/server => shared/management}/http/api/generate.sh (100%) rename {management/server => shared/management}/http/api/openapi.yml (100%) rename {management/server => shared/management}/http/api/types.gen.go (100%) rename {management/server => shared/management}/http/util/util.go (98%) create mode 100644 shared/management/operations/operation.go rename {management/server => shared/management}/status/error.go (99%) create mode 100644 shared/relay/constants.go rename {relay => shared/relay}/healthcheck/doc.go (100%) rename {relay => shared/relay}/healthcheck/receiver.go (100%) rename {relay => shared/relay}/healthcheck/receiver_test.go (100%) rename {relay => shared/relay}/healthcheck/sender.go (100%) rename {relay => shared/relay}/healthcheck/sender_test.go (100%) rename {relay => shared/relay}/messages/address/address.go (100%) rename {relay => shared/relay}/messages/auth/auth.go (100%) rename {relay => shared/relay}/messages/doc.go (100%) rename {relay => shared/relay}/messages/id.go (100%) rename {relay => shared/relay}/messages/message.go (100%) rename {relay => shared/relay}/messages/message_test.go (100%) rename {relay => shared/relay}/messages/peer_state.go (100%) rename {relay => shared/relay}/messages/peer_state_test.go (100%) rename {relay => shared/relay}/tls/alpn.go (100%) rename {relay => shared/relay}/tls/client_dev.go (100%) rename {relay => shared/relay}/tls/client_prod.go (100%) rename {relay => shared/relay}/tls/doc.go (100%) rename {relay => shared/relay}/tls/server_dev.go (100%) rename {relay => shared/relay}/tls/server_prod.go (100%) diff --git a/.github/workflows/check-license-dependencies.yml b/.github/workflows/check-license-dependencies.yml new file mode 100644 index 000000000..d3da427b0 --- /dev/null +++ b/.github/workflows/check-license-dependencies.yml @@ -0,0 +1,41 @@ +name: Check License Dependencies + +on: + push: + branches: [ main ] + pull_request: + +jobs: + check-dependencies: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Check for problematic license dependencies + run: | + echo "Checking for dependencies on management/, signal/, and relay/ packages..." + + # Find all directories except the problematic ones and system dirs + FOUND_ISSUES=0 + find . -maxdepth 1 -type d -not -name "." -not -name "management" -not -name "signal" -not -name "relay" -not -name ".git*" | sort | while read dir; do + echo "=== Checking $dir ===" + # Search for problematic imports, excluding test files + RESULTS=$(grep -r "github.com/netbirdio/netbird/\(management\|signal\|relay\)" "$dir" --include="*.go" | grep -v "_test.go" | grep -v "test_" | grep -v "/test/" || true) + if [ ! -z "$RESULTS" ]; then + echo "❌ Found problematic dependencies:" + echo "$RESULTS" + FOUND_ISSUES=1 + else + echo "✓ No problematic dependencies found" + fi + done + if [ $FOUND_ISSUES -eq 1 ]; then + echo "" + echo "❌ Found dependencies on management/, signal/, or relay/ packages" + echo "These packages will change license and should not be imported by client or shared code" + exit 1 + else + echo "" + echo "✅ All license dependencies are clean" + fi diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 0d7233c3e..0013833c4 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -259,7 +259,7 @@ jobs: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ go test ${{ matrix.raceFlag }} \ -exec 'sudo' \ - -timeout 10m ./relay/... + -timeout 10m ./relay/... ./shared/relay/... test_signal: name: "Signal / Unit" @@ -309,7 +309,7 @@ jobs: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ go test \ -exec 'sudo' \ - -timeout 10m ./signal/... + -timeout 10m ./signal/... ./shared/signal/... test_management: name: "Management / Unit" @@ -369,7 +369,7 @@ jobs: CI=true \ go test -tags=devcert \ -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \ - -timeout 20m ./management/... + -timeout 20m ./management/... ./shared/management/... benchmark: name: "Management / Benchmark" @@ -430,7 +430,7 @@ jobs: CI=true \ go test -tags devcert -run=^$ -bench=. \ -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ - -timeout 20m ./management/... + -timeout 20m ./management/... ./shared/management/... api_benchmark: name: "Management / Benchmark (API)" @@ -521,7 +521,7 @@ jobs: -run=^$ \ -bench=. \ -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \ - -timeout 20m ./management/... + -timeout 20m ./management/... ./shared/management/... api_integration_test: name: "Management / Integration" @@ -571,4 +571,4 @@ jobs: CI=true \ go test -tags=integration \ -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ - -timeout 20m ./management/... + -timeout 20m ./management/... ./shared/management/... \ No newline at end of file diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml index 77096790f..26f3b8f02 100644 --- a/.github/workflows/update-docs.yml +++ b/.github/workflows/update-docs.yml @@ -5,7 +5,7 @@ on: tags: - 'v*' paths: - - 'management/server/http/api/openapi.yml' + - 'shared/management/http/api/openapi.yml' jobs: trigger_docs_api_update: diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 806595574..084a03d88 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -28,7 +28,6 @@ import ( const ( // managementLegacyPortString is the port that was used before by the Management gRPC server. // It is used for backward compatibility now. - // NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import managementLegacyPortString = "33073" // DefaultManagementURL points to the NetBird's cloud management endpoint DefaultManagementURL = "https://api.netbird.io:443" diff --git a/formatter/hook/hook.go b/formatter/hook/hook.go index 290c3377d..c0d8c4eba 100644 --- a/formatter/hook/hook.go +++ b/formatter/hook/hook.go @@ -9,7 +9,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/shared/context" ) type ExecutionContext string diff --git a/management/server/account.go b/management/server/account.go index 395634769..49ea38fe1 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -40,7 +40,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/settings" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" diff --git a/management/server/context/keys.go b/management/server/context/keys.go index c5b5da044..9697997a8 100644 --- a/management/server/context/keys.go +++ b/management/server/context/keys.go @@ -1,8 +1,10 @@ package context +import "github.com/netbirdio/netbird/shared/context" + const ( - RequestIDKey = "requestID" - AccountIDKey = "accountID" - UserIDKey = "userID" - PeerIDKey = "peerID" + RequestIDKey = context.RequestIDKey + AccountIDKey = context.AccountIDKey + UserIDKey = context.UserIDKey + PeerIDKey = context.PeerIDKey ) diff --git a/management/server/dns.go b/management/server/dns.go index 019282e65..489618c17 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -12,7 +12,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 2af07d8e4..d58689544 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -24,7 +24,7 @@ import ( "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) const ( diff --git a/management/server/event.go b/management/server/event.go index d94714e2c..3144c52ea 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -11,7 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 5af8276b5..4b9a6b2d9 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -13,7 +13,7 @@ import ( "gorm.io/gorm" "gorm.io/gorm/logger" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) type GeoNames struct { diff --git a/management/server/group.go b/management/server/group.go index 95bed7d18..2b804b5f6 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -14,7 +14,7 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/group_test.go b/management/server/group_test.go index 51069dc56..c31280156 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -26,7 +26,7 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" peer2 "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/permissions" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" diff --git a/management/server/groups/manager.go b/management/server/groups/manager.go index df4b6c3d6..8647eeb68 100644 --- a/management/server/groups/manager.go +++ b/management/server/groups/manager.go @@ -6,7 +6,7 @@ import ( "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index c52e8a3d7..f5d9c250b 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -32,7 +32,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/settings" - internalStatus "github.com/netbirdio/netbird/management/server/status" + internalStatus "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index 1a22b8b9f..aeda61184 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -11,10 +11,10 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/settings" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index dbf0c22bc..1dad33a6f 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -15,10 +15,10 @@ import ( "github.com/stretchr/testify/assert" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/settings" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/dns/dns_settings_handler.go b/management/server/http/handlers/dns/dns_settings_handler.go index 60822c883..08a0b2afd 100644 --- a/management/server/http/handlers/dns/dns_settings_handler.go +++ b/management/server/http/handlers/dns/dns_settings_handler.go @@ -9,8 +9,8 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/dns/dns_settings_handler_test.go b/management/server/http/handlers/dns/dns_settings_handler_test.go index ca81adf43..42b519c29 100644 --- a/management/server/http/handlers/dns/dns_settings_handler_test.go +++ b/management/server/http/handlers/dns/dns_settings_handler_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" "github.com/gorilla/mux" diff --git a/management/server/http/handlers/dns/nameservers_handler.go b/management/server/http/handlers/dns/nameservers_handler.go index 970be6d8a..bce1c4b78 100644 --- a/management/server/http/handlers/dns/nameservers_handler.go +++ b/management/server/http/handlers/dns/nameservers_handler.go @@ -11,9 +11,9 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" ) // nameserversHandler is the nameserver group handler of the account diff --git a/management/server/http/handlers/dns/nameservers_handler_test.go b/management/server/http/handlers/dns/nameservers_handler_test.go index 45283bc37..d49b6c7e0 100644 --- a/management/server/http/handlers/dns/nameservers_handler_test.go +++ b/management/server/http/handlers/dns/nameservers_handler_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/assert" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/status" "github.com/gorilla/mux" diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go index eee5d8aa7..ae1e64e5c 100644 --- a/management/server/http/handlers/events/events_handler.go +++ b/management/server/http/handlers/events/events_handler.go @@ -10,8 +10,8 @@ import ( "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) // handler HTTP handler diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go index 3a643fe90..a0695fa3f 100644 --- a/management/server/http/handlers/events/events_handler_test.go +++ b/management/server/http/handlers/events/events_handler_test.go @@ -16,7 +16,7 @@ import ( nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/activity" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index bede652f5..e861e873c 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -11,9 +11,9 @@ import ( nbcontext "github.com/netbirdio/netbird/management/server/context" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go index 2caa2f5bf..34694ec8c 100644 --- a/management/server/http/handlers/groups/groups_handler_test.go +++ b/management/server/http/handlers/groups/groups_handler_test.go @@ -19,11 +19,11 @@ import ( "github.com/netbirdio/netbird/management/server" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/mock_server" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go index 1809019a6..d7b598a5d 100644 --- a/management/server/http/handlers/networks/handler.go +++ b/management/server/http/handlers/networks/handler.go @@ -12,14 +12,14 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/networks" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/routers" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/networks/types" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" nbtypes "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/networks/resources_handler.go b/management/server/http/handlers/networks/resources_handler.go index 616083302..59396dceb 100644 --- a/management/server/http/handlers/networks/resources_handler.go +++ b/management/server/http/handlers/networks/resources_handler.go @@ -8,8 +8,8 @@ import ( nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/resources/types" ) diff --git a/management/server/http/handlers/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go index 6b00534fc..2e64c637f 100644 --- a/management/server/http/handlers/networks/routers_handler.go +++ b/management/server/http/handlers/networks/routers_handler.go @@ -7,8 +7,8 @@ import ( "github.com/gorilla/mux" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/networks/routers" "github.com/netbirdio/netbird/management/server/networks/routers/types" ) diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 644693e19..eed07e95d 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -14,10 +14,10 @@ import ( "github.com/netbirdio/netbird/management/server/activity" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go index d49ec6b46..94564113f 100644 --- a/management/server/http/handlers/peers/peers_handler_test.go +++ b/management/server/http/handlers/peers/peers_handler_test.go @@ -17,7 +17,7 @@ import ( "golang.org/x/exp/maps" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" diff --git a/management/server/http/handlers/policies/geolocation_handler_test.go b/management/server/http/handlers/policies/geolocation_handler_test.go index b7b53f53f..cedd5ac88 100644 --- a/management/server/http/handlers/policies/geolocation_handler_test.go +++ b/management/server/http/handlers/policies/geolocation_handler_test.go @@ -16,7 +16,7 @@ import ( nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go index 84c8ea0aa..cb6995793 100644 --- a/management/server/http/handlers/policies/geolocations_handler.go +++ b/management/server/http/handlers/policies/geolocations_handler.go @@ -9,12 +9,12 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) var ( diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index 267d5744f..4d6bad5e3 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -10,9 +10,9 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go index 6f3dbc792..fd39ae2a3 100644 --- a/management/server/http/handlers/policies/policies_handler_test.go +++ b/management/server/http/handlers/policies/policies_handler_test.go @@ -14,9 +14,9 @@ import ( "github.com/stretchr/testify/assert" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go index 2925f96ef..3ebc4d1e1 100644 --- a/management/server/http/handlers/policies/posture_checks_handler.go +++ b/management/server/http/handlers/policies/posture_checks_handler.go @@ -9,10 +9,10 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) // postureChecksHandler is a handler that returns posture checks of the account. diff --git a/management/server/http/handlers/policies/posture_checks_handler_test.go b/management/server/http/handlers/policies/posture_checks_handler_test.go index e875b3738..c644b533a 100644 --- a/management/server/http/handlers/policies/posture_checks_handler_test.go +++ b/management/server/http/handlers/policies/posture_checks_handler_test.go @@ -16,10 +16,10 @@ import ( nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) var berlin = "Berlin" diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go index eba06fbec..7950db1e8 100644 --- a/management/server/http/handlers/routes/routes_handler.go +++ b/management/server/http/handlers/routes/routes_handler.go @@ -11,9 +11,9 @@ import ( "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/route" ) diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go index bed42514f..fc0e112f7 100644 --- a/management/server/http/handlers/routes/routes_handler_test.go +++ b/management/server/http/handlers/routes/routes_handler_test.go @@ -17,9 +17,9 @@ import ( "github.com/netbirdio/netbird/shared/management/domain" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" ) diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go index 38ba86fb1..2287dadfe 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go @@ -10,9 +10,9 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go index e9135469f..7b46b486b 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go @@ -15,9 +15,9 @@ import ( "github.com/stretchr/testify/assert" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go index 90913eac1..bae07af4a 100644 --- a/management/server/http/handlers/users/pat_handler.go +++ b/management/server/http/handlers/users/pat_handler.go @@ -8,9 +8,9 @@ import ( "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/users/pat_handler_test.go b/management/server/http/handlers/users/pat_handler_test.go index 6593de64a..92544c56d 100644 --- a/management/server/http/handlers/users/pat_handler_test.go +++ b/management/server/http/handlers/users/pat_handler_test.go @@ -17,9 +17,9 @@ import ( "github.com/netbirdio/netbird/management/server/util" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go index ac04b8e35..bcd637db4 100644 --- a/management/server/http/handlers/users/users_handler.go +++ b/management/server/http/handlers/users/users_handler.go @@ -9,9 +9,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/account" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go index 58e33a6d5..f7dc81919 100644 --- a/management/server/http/handlers/users/users_handler_test.go +++ b/management/server/http/handlers/users/users_handler_test.go @@ -16,11 +16,11 @@ import ( "github.com/stretchr/testify/require" nbcontext "github.com/netbirdio/netbird/management/server/context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/roles" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" ) diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index f2732fbf8..f221e64a9 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -13,8 +13,8 @@ import ( "github.com/netbirdio/netbird/management/server/auth" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/middleware/bypass" - "github.com/netbirdio/netbird/management/server/http/util" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go index d82e08be9..52737e4eb 100644 --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" ) diff --git a/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go b/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go index f99b541f8..9404c4ee4 100644 --- a/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" ) diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go index c0b641a70..844b3e7a6 100644 --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" ) diff --git a/management/server/http/testing/integration/setupkeys_handler_integration_test.go b/management/server/http/testing/integration/setupkeys_handler_integration_test.go index ed6e642a2..9f04e3c24 100644 --- a/management/server/http/testing/integration/setupkeys_handler_integration_test.go +++ b/management/server/http/testing/integration/setupkeys_handler_integration_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/http/handlers/setup_keys" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" ) diff --git a/management/server/nameserver.go b/management/server/nameserver.go index 18339335d..ef62d0eaf 100644 --- a/management/server/nameserver.go +++ b/management/server/nameserver.go @@ -13,7 +13,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/networks/manager.go b/management/server/networks/manager.go index 1c46e9281..cb1116b0e 100644 --- a/management/server/networks/manager.go +++ b/management/server/networks/manager.go @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" ) diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go index 21d1e54de..7c8c68e32 100644 --- a/management/server/networks/resources/manager.go +++ b/management/server/networks/resources/manager.go @@ -12,7 +12,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" nbtypes "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/networks/resources/manager_test.go b/management/server/networks/resources/manager_test.go index 3a91b4af8..c6cec6f7e 100644 --- a/management/server/networks/resources/manager_test.go +++ b/management/server/networks/resources/manager_test.go @@ -10,7 +10,7 @@ import ( "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/networks/resources/types" "github.com/netbirdio/netbird/management/server/permissions" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" ) diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 3033a1fa0..7874be858 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -14,7 +14,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) type NetworkResourceType string diff --git a/management/server/networks/routers/manager.go b/management/server/networks/routers/manager.go index 7b488b361..ff5a771d6 100644 --- a/management/server/networks/routers/manager.go +++ b/management/server/networks/routers/manager.go @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" ) diff --git a/management/server/networks/routers/manager_test.go b/management/server/networks/routers/manager_test.go index 541643222..8054d05c6 100644 --- a/management/server/networks/routers/manager_test.go +++ b/management/server/networks/routers/manager_test.go @@ -9,7 +9,7 @@ import ( "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" ) diff --git a/management/server/networks/routers/types/router.go b/management/server/networks/routers/types/router.go index 71465868f..72b15fd9a 100644 --- a/management/server/networks/routers/types/router.go +++ b/management/server/networks/routers/types/router.go @@ -5,7 +5,7 @@ import ( "github.com/rs/xid" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/networks/types" ) diff --git a/management/server/networks/types/network.go b/management/server/networks/types/network.go index d1c7f2b33..69d596f8b 100644 --- a/management/server/networks/types/network.go +++ b/management/server/networks/types/network.go @@ -3,7 +3,7 @@ package types import ( "github.com/rs/xid" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) type Network struct { diff --git a/management/server/peer.go b/management/server/peer.go index a8c944dce..6cd519f27 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -32,7 +32,7 @@ import ( "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 46df48345..a78f220d3 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -29,7 +29,7 @@ import ( "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/settings" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/peers/manager.go b/management/server/peers/manager.go index fe48bf576..7e0262ef9 100644 --- a/management/server/peers/manager.go +++ b/management/server/peers/manager.go @@ -10,7 +10,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" ) diff --git a/management/server/permissions/manager.go b/management/server/permissions/manager.go index ebbce5d4a..4b2d91c3b 100644 --- a/management/server/permissions/manager.go +++ b/management/server/permissions/manager.go @@ -11,7 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/permissions/roles" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/policy.go b/management/server/policy.go index b5440266f..b19f99960 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) // GetPolicy from the store diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index b2f308d76..d65dc5045 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -7,9 +7,9 @@ import ( "regexp" "github.com/hashicorp/go-version" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) const ( diff --git a/management/server/posture/network.go b/management/server/posture/network.go index 0fa6f6e71..f78744143 100644 --- a/management/server/posture/network.go +++ b/management/server/posture/network.go @@ -7,7 +7,7 @@ import ( "slices" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) type PeerNetworkRangeCheck struct { diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index f91e89b45..50f7e4776 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -13,7 +13,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" ) diff --git a/management/server/route.go b/management/server/route.go index ee5835a8b..50c4bdc4a 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -14,7 +14,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" diff --git a/management/server/settings/manager.go b/management/server/settings/manager.go index 94392ebf7..9cb142938 100644 --- a/management/server/settings/manager.go +++ b/management/server/settings/manager.go @@ -11,7 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" diff --git a/management/server/setupkey.go b/management/server/setupkey.go index b0903c8d0..e3647f6df 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -10,7 +10,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index c2f0dff6d..c0f683124 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -29,7 +29,7 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 44bb3f599..fdce9d735 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -27,7 +27,7 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" nbroute "github.com/netbirdio/netbird/route" diff --git a/management/server/types/account.go b/management/server/types/account.go index 18259b220..17a838aae 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -22,7 +22,7 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" diff --git a/management/server/types/network.go b/management/server/types/network.go index 4b0602cd6..f072a4294 100644 --- a/management/server/types/network.go +++ b/management/server/types/network.go @@ -14,7 +14,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/shared/management/proto" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" ) diff --git a/management/server/types/resource.go b/management/server/types/resource.go index 820872f20..84d8e4b88 100644 --- a/management/server/types/resource.go +++ b/management/server/types/resource.go @@ -1,7 +1,7 @@ package types import ( - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) type Resource struct { diff --git a/management/server/user.go b/management/server/user.go index 58d2d0c41..e33f89dfd 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -17,7 +17,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" diff --git a/management/server/user_test.go b/management/server/user_test.go index 8ab6584cf..7515439b8 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -15,7 +15,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/roles" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/users" "github.com/netbirdio/netbird/management/server/util" diff --git a/relay/server/handshake.go b/relay/server/handshake.go index eb72b3bae..922369798 100644 --- a/relay/server/handshake.go +++ b/relay/server/handshake.go @@ -6,11 +6,11 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" //nolint:staticcheck - "github.com/netbirdio/netbird/relay/messages/address" + "github.com/netbirdio/netbird/shared/relay/messages/address" //nolint:staticcheck - authmsg "github.com/netbirdio/netbird/relay/messages/auth" + authmsg "github.com/netbirdio/netbird/shared/relay/messages/auth" ) type Validator interface { diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go index 3a95951ee..8579fb137 100644 --- a/relay/server/listener/ws/listener.go +++ b/relay/server/listener/ws/listener.go @@ -10,10 +10,12 @@ import ( "github.com/coder/websocket" log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/shared/relay" ) // URLPath is the path for the websocket connection. -const URLPath = "/relay" +const URLPath = relay.WebSocketURLPath type Listener struct { // Address is the address to listen on. diff --git a/relay/server/peer.go b/relay/server/peer.go index 9caa5b06f..c47f2e960 100644 --- a/relay/server/peer.go +++ b/relay/server/peer.go @@ -9,8 +9,8 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/relay/healthcheck" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/healthcheck" + "github.com/netbirdio/netbird/shared/relay/messages" "github.com/netbirdio/netbird/relay/metrics" "github.com/netbirdio/netbird/relay/server/store" ) diff --git a/relay/server/server.go b/relay/server/server.go index f0b480ee4..59695e8a9 100644 --- a/relay/server/server.go +++ b/relay/server/server.go @@ -10,7 +10,7 @@ import ( "github.com/netbirdio/netbird/relay/server/listener" "github.com/netbirdio/netbird/relay/server/listener/quic" "github.com/netbirdio/netbird/relay/server/listener/ws" - quictls "github.com/netbirdio/netbird/relay/tls" + quictls "github.com/netbirdio/netbird/shared/relay/tls" log "github.com/sirupsen/logrus" ) diff --git a/relay/server/store/listener.go b/relay/server/store/listener.go index e9c77d953..f09f2ffdd 100644 --- a/relay/server/store/listener.go +++ b/relay/server/store/listener.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" ) type event struct { diff --git a/relay/server/store/notifier.go b/relay/server/store/notifier.go index 335522537..0140d6633 100644 --- a/relay/server/store/notifier.go +++ b/relay/server/store/notifier.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" ) type PeerNotifier struct { diff --git a/relay/server/store/store.go b/relay/server/store/store.go index fd0578603..556307885 100644 --- a/relay/server/store/store.go +++ b/relay/server/store/store.go @@ -3,7 +3,7 @@ package store import ( "sync" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" ) type IPeer interface { diff --git a/relay/server/store/store_test.go b/relay/server/store/store_test.go index ad549a62c..1bf68aa59 100644 --- a/relay/server/store/store_test.go +++ b/relay/server/store/store_test.go @@ -3,7 +3,7 @@ package store import ( "testing" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" ) type MocPeer struct { diff --git a/route/route.go b/route/route.go index c46dff8c8..bf62bf666 100644 --- a/route/route.go +++ b/route/route.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/netbirdio/netbird/shared/management/domain" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) // Windows has some limitation regarding metric size that differ from Unix-like systems. diff --git a/shared/context/keys.go b/shared/context/keys.go new file mode 100644 index 000000000..5345ee214 --- /dev/null +++ b/shared/context/keys.go @@ -0,0 +1,8 @@ +package context + +const ( + RequestIDKey = "requestID" + AccountIDKey = "accountID" + UserIDKey = "userID" + PeerIDKey = "peerID" +) \ No newline at end of file diff --git a/shared/management/client/client_test.go b/shared/management/client/client_test.go index 6431ccd45..061f21d44 100644 --- a/shared/management/client/client_test.go +++ b/shared/management/client/client_test.go @@ -52,7 +52,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { log.SetLevel(level) config := &types.Config{} - _, err := util.ReadJson("../server/testdata/management.json", config) + _, err := util.ReadJson("../../../management/server/testdata/management.json", config) if err != nil { t.Fatal(err) } @@ -62,7 +62,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { t.Fatal(err) } s := grpc.NewServer() - store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../server/testdata/store.sql", t.TempDir()) + store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../../management/server/testdata/store.sql", t.TempDir()) if err != nil { t.Fatal(err) } diff --git a/shared/management/client/rest/accounts.go b/shared/management/client/rest/accounts.go index fbe3010e1..2211f4a43 100644 --- a/shared/management/client/rest/accounts.go +++ b/shared/management/client/rest/accounts.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // AccountsAPI APIs for accounts, do not use directly diff --git a/shared/management/client/rest/accounts_test.go b/shared/management/client/rest/accounts_test.go index ae45de7ab..be0066488 100644 --- a/shared/management/client/rest/accounts_test.go +++ b/shared/management/client/rest/accounts_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/client.go b/shared/management/client/rest/client.go index b5945985f..2a5de5bbc 100644 --- a/shared/management/client/rest/client.go +++ b/shared/management/client/rest/client.go @@ -8,7 +8,7 @@ import ( "io" "net/http" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/util" ) // Client Management service HTTP REST API Client diff --git a/shared/management/client/rest/client_test.go b/shared/management/client/rest/client_test.go index 437ffa836..56c859652 100644 --- a/shared/management/client/rest/client_test.go +++ b/shared/management/client/rest/client_test.go @@ -26,7 +26,7 @@ func ptr[T any, PT *T](x T) PT { func withBlackBoxServer(t *testing.T, callback func(*rest.Client)) { t.Helper() - handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false) + handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../../../management/server/testdata/store.sql", nil, false) server := httptest.NewServer(handler) defer server.Close() c := rest.New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") diff --git a/shared/management/client/rest/dns.go b/shared/management/client/rest/dns.go index 3fb74d5f5..aeef02735 100644 --- a/shared/management/client/rest/dns.go +++ b/shared/management/client/rest/dns.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // DNSAPI APIs for DNS Management, do not use directly diff --git a/shared/management/client/rest/dns_test.go b/shared/management/client/rest/dns_test.go index e467659c1..58082abe8 100644 --- a/shared/management/client/rest/dns_test.go +++ b/shared/management/client/rest/dns_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/events.go b/shared/management/client/rest/events.go index 775d3ba2e..2d25333ae 100644 --- a/shared/management/client/rest/events.go +++ b/shared/management/client/rest/events.go @@ -3,7 +3,7 @@ package rest import ( "context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // EventsAPI APIs for Events, do not use directly diff --git a/shared/management/client/rest/events_test.go b/shared/management/client/rest/events_test.go index f3c5627f2..b28390001 100644 --- a/shared/management/client/rest/events_test.go +++ b/shared/management/client/rest/events_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/geo.go b/shared/management/client/rest/geo.go index dfecee09e..3c4a3ff9f 100644 --- a/shared/management/client/rest/geo.go +++ b/shared/management/client/rest/geo.go @@ -3,7 +3,7 @@ package rest import ( "context" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // GeoLocationAPI APIs for Geo-Location, do not use directly diff --git a/shared/management/client/rest/geo_test.go b/shared/management/client/rest/geo_test.go index 909744b47..fcb4808a1 100644 --- a/shared/management/client/rest/geo_test.go +++ b/shared/management/client/rest/geo_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/groups.go b/shared/management/client/rest/groups.go index 7d4bac62c..af068e077 100644 --- a/shared/management/client/rest/groups.go +++ b/shared/management/client/rest/groups.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // GroupsAPI APIs for Groups, do not use directly diff --git a/shared/management/client/rest/groups_test.go b/shared/management/client/rest/groups_test.go index 8b8207def..fcd759e9a 100644 --- a/shared/management/client/rest/groups_test.go +++ b/shared/management/client/rest/groups_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/impersonation_test.go b/shared/management/client/rest/impersonation_test.go index e5739b8f5..4fb8f24eb 100644 --- a/shared/management/client/rest/impersonation_test.go +++ b/shared/management/client/rest/impersonation_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) var ( diff --git a/shared/management/client/rest/networks.go b/shared/management/client/rest/networks.go index 9441780f3..cb25dcbef 100644 --- a/shared/management/client/rest/networks.go +++ b/shared/management/client/rest/networks.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // NetworksAPI APIs for Networks, do not use directly diff --git a/shared/management/client/rest/networks_test.go b/shared/management/client/rest/networks_test.go index 45cc81d8d..ca2a294ae 100644 --- a/shared/management/client/rest/networks_test.go +++ b/shared/management/client/rest/networks_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/peers.go b/shared/management/client/rest/peers.go index f4364bb62..359c21e42 100644 --- a/shared/management/client/rest/peers.go +++ b/shared/management/client/rest/peers.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // PeersAPI APIs for peers, do not use directly diff --git a/shared/management/client/rest/peers_test.go b/shared/management/client/rest/peers_test.go index 5701455de..a45f9d6ec 100644 --- a/shared/management/client/rest/peers_test.go +++ b/shared/management/client/rest/peers_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/policies.go b/shared/management/client/rest/policies.go index a6e0e38d3..206205984 100644 --- a/shared/management/client/rest/policies.go +++ b/shared/management/client/rest/policies.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // PoliciesAPI APIs for Policies, do not use directly diff --git a/shared/management/client/rest/policies_test.go b/shared/management/client/rest/policies_test.go index 6254077e9..a19d0a728 100644 --- a/shared/management/client/rest/policies_test.go +++ b/shared/management/client/rest/policies_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/posturechecks.go b/shared/management/client/rest/posturechecks.go index 2ab8f4549..1a440f058 100644 --- a/shared/management/client/rest/posturechecks.go +++ b/shared/management/client/rest/posturechecks.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // PostureChecksAPI APIs for PostureChecks, do not use directly diff --git a/shared/management/client/rest/posturechecks_test.go b/shared/management/client/rest/posturechecks_test.go index c8489ac31..9b1b618df 100644 --- a/shared/management/client/rest/posturechecks_test.go +++ b/shared/management/client/rest/posturechecks_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/routes.go b/shared/management/client/rest/routes.go index 183c363cf..31024fe92 100644 --- a/shared/management/client/rest/routes.go +++ b/shared/management/client/rest/routes.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // RoutesAPI APIs for Routes, do not use directly diff --git a/shared/management/client/rest/routes_test.go b/shared/management/client/rest/routes_test.go index dc83f6ba4..9452a07fc 100644 --- a/shared/management/client/rest/routes_test.go +++ b/shared/management/client/rest/routes_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/setupkeys.go b/shared/management/client/rest/setupkeys.go index 6f26cd0b7..34c07c6ab 100644 --- a/shared/management/client/rest/setupkeys.go +++ b/shared/management/client/rest/setupkeys.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // SetupKeysAPI APIs for Setup keys, do not use directly diff --git a/shared/management/client/rest/setupkeys_test.go b/shared/management/client/rest/setupkeys_test.go index eafdc54a5..0fa782da5 100644 --- a/shared/management/client/rest/setupkeys_test.go +++ b/shared/management/client/rest/setupkeys_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/tokens.go b/shared/management/client/rest/tokens.go index 7a63d0c9d..38b305722 100644 --- a/shared/management/client/rest/tokens.go +++ b/shared/management/client/rest/tokens.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // TokensAPI APIs for PATs, do not use directly diff --git a/shared/management/client/rest/tokens_test.go b/shared/management/client/rest/tokens_test.go index 8abbca0a9..ce3748751 100644 --- a/shared/management/client/rest/tokens_test.go +++ b/shared/management/client/rest/tokens_test.go @@ -15,8 +15,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/client/rest/users.go b/shared/management/client/rest/users.go index f0ef54be2..b0ea46d55 100644 --- a/shared/management/client/rest/users.go +++ b/shared/management/client/rest/users.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" - "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/shared/management/http/api" ) // UsersAPI APIs for users, do not use directly diff --git a/shared/management/client/rest/users_test.go b/shared/management/client/rest/users_test.go index 28f955380..d53c4eb6a 100644 --- a/shared/management/client/rest/users_test.go +++ b/shared/management/client/rest/users_test.go @@ -15,8 +15,8 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/client/rest" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" ) var ( diff --git a/shared/management/domain/go.sum b/shared/management/domain/go.sum deleted file mode 100644 index 4ce43119c..000000000 --- a/shared/management/domain/go.sum +++ /dev/null @@ -1 +0,0 @@ -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= diff --git a/management/server/http/api/cfg.yaml b/shared/management/http/api/cfg.yaml similarity index 100% rename from management/server/http/api/cfg.yaml rename to shared/management/http/api/cfg.yaml diff --git a/management/server/http/api/generate.sh b/shared/management/http/api/generate.sh similarity index 100% rename from management/server/http/api/generate.sh rename to shared/management/http/api/generate.sh diff --git a/management/server/http/api/openapi.yml b/shared/management/http/api/openapi.yml similarity index 100% rename from management/server/http/api/openapi.yml rename to shared/management/http/api/openapi.yml diff --git a/management/server/http/api/types.gen.go b/shared/management/http/api/types.gen.go similarity index 100% rename from management/server/http/api/types.gen.go rename to shared/management/http/api/types.gen.go diff --git a/management/server/http/util/util.go b/shared/management/http/util/util.go similarity index 98% rename from management/server/http/util/util.go rename to shared/management/http/util/util.go index 3d7eed498..3ae321023 100644 --- a/management/server/http/util/util.go +++ b/shared/management/http/util/util.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/shared/management/status" ) // EmptyObject is an empty struct used to return empty JSON object diff --git a/shared/management/operations/operation.go b/shared/management/operations/operation.go new file mode 100644 index 000000000..b9b500362 --- /dev/null +++ b/shared/management/operations/operation.go @@ -0,0 +1,4 @@ +package operations + +// Operation represents a permission operation type +type Operation string \ No newline at end of file diff --git a/management/server/status/error.go b/shared/management/status/error.go similarity index 99% rename from management/server/status/error.go rename to shared/management/status/error.go index e3cc27b29..7660174d6 100644 --- a/management/server/status/error.go +++ b/shared/management/status/error.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/netbirdio/netbird/management/server/permissions/operations" + "github.com/netbirdio/netbird/shared/management/operations" ) const ( diff --git a/shared/relay/client/client.go b/shared/relay/client/client.go index f9f5b70e4..37c9debc2 100644 --- a/shared/relay/client/client.go +++ b/shared/relay/client/client.go @@ -13,8 +13,8 @@ import ( "github.com/netbirdio/netbird/shared/relay/client/dialer" "github.com/netbirdio/netbird/shared/relay/client/dialer/quic" "github.com/netbirdio/netbird/shared/relay/client/dialer/ws" - "github.com/netbirdio/netbird/relay/healthcheck" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/healthcheck" + "github.com/netbirdio/netbird/shared/relay/messages" ) const ( diff --git a/shared/relay/client/conn.go b/shared/relay/client/conn.go index d8cffa695..4e151aaa4 100644 --- a/shared/relay/client/conn.go +++ b/shared/relay/client/conn.go @@ -4,7 +4,7 @@ import ( "net" "time" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" ) // Conn represent a connection to a relayed remote peer. diff --git a/shared/relay/client/dialer/quic/quic.go b/shared/relay/client/dialer/quic/quic.go index 3fd48fb19..b496f6a9b 100644 --- a/shared/relay/client/dialer/quic/quic.go +++ b/shared/relay/client/dialer/quic/quic.go @@ -11,7 +11,7 @@ import ( "github.com/quic-go/quic-go" log "github.com/sirupsen/logrus" - quictls "github.com/netbirdio/netbird/relay/tls" + quictls "github.com/netbirdio/netbird/shared/relay/tls" nbnet "github.com/netbirdio/netbird/util/net" ) diff --git a/shared/relay/client/dialer/ws/ws.go b/shared/relay/client/dialer/ws/ws.go index cb525865b..109651f5d 100644 --- a/shared/relay/client/dialer/ws/ws.go +++ b/shared/relay/client/dialer/ws/ws.go @@ -14,7 +14,7 @@ import ( "github.com/coder/websocket" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/relay/server/listener/ws" + "github.com/netbirdio/netbird/shared/relay" "github.com/netbirdio/netbird/util/embeddedroots" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -40,7 +40,7 @@ func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) { if err != nil { return nil, err } - parsedURL.Path = ws.URLPath + parsedURL.Path = relay.WebSocketURLPath wsConn, resp, err := websocket.Dial(ctx, parsedURL.String(), opts) if err != nil { diff --git a/shared/relay/client/peer_subscription.go b/shared/relay/client/peer_subscription.go index 85bd41cbd..b594b65b7 100644 --- a/shared/relay/client/peer_subscription.go +++ b/shared/relay/client/peer_subscription.go @@ -9,7 +9,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" ) const ( diff --git a/shared/relay/client/peer_subscription_test.go b/shared/relay/client/peer_subscription_test.go index 0437efa04..bcc7a552d 100644 --- a/shared/relay/client/peer_subscription_test.go +++ b/shared/relay/client/peer_subscription_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/relay/messages" + "github.com/netbirdio/netbird/shared/relay/messages" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" diff --git a/shared/relay/constants.go b/shared/relay/constants.go new file mode 100644 index 000000000..3c7c3cd29 --- /dev/null +++ b/shared/relay/constants.go @@ -0,0 +1,6 @@ +package relay + +const ( + // WebSocketURLPath is the path for the websocket relay connection + WebSocketURLPath = "/relay" +) \ No newline at end of file diff --git a/relay/healthcheck/doc.go b/shared/relay/healthcheck/doc.go similarity index 100% rename from relay/healthcheck/doc.go rename to shared/relay/healthcheck/doc.go diff --git a/relay/healthcheck/receiver.go b/shared/relay/healthcheck/receiver.go similarity index 100% rename from relay/healthcheck/receiver.go rename to shared/relay/healthcheck/receiver.go diff --git a/relay/healthcheck/receiver_test.go b/shared/relay/healthcheck/receiver_test.go similarity index 100% rename from relay/healthcheck/receiver_test.go rename to shared/relay/healthcheck/receiver_test.go diff --git a/relay/healthcheck/sender.go b/shared/relay/healthcheck/sender.go similarity index 100% rename from relay/healthcheck/sender.go rename to shared/relay/healthcheck/sender.go diff --git a/relay/healthcheck/sender_test.go b/shared/relay/healthcheck/sender_test.go similarity index 100% rename from relay/healthcheck/sender_test.go rename to shared/relay/healthcheck/sender_test.go diff --git a/relay/messages/address/address.go b/shared/relay/messages/address/address.go similarity index 100% rename from relay/messages/address/address.go rename to shared/relay/messages/address/address.go diff --git a/relay/messages/auth/auth.go b/shared/relay/messages/auth/auth.go similarity index 100% rename from relay/messages/auth/auth.go rename to shared/relay/messages/auth/auth.go diff --git a/relay/messages/doc.go b/shared/relay/messages/doc.go similarity index 100% rename from relay/messages/doc.go rename to shared/relay/messages/doc.go diff --git a/relay/messages/id.go b/shared/relay/messages/id.go similarity index 100% rename from relay/messages/id.go rename to shared/relay/messages/id.go diff --git a/relay/messages/message.go b/shared/relay/messages/message.go similarity index 100% rename from relay/messages/message.go rename to shared/relay/messages/message.go diff --git a/relay/messages/message_test.go b/shared/relay/messages/message_test.go similarity index 100% rename from relay/messages/message_test.go rename to shared/relay/messages/message_test.go diff --git a/relay/messages/peer_state.go b/shared/relay/messages/peer_state.go similarity index 100% rename from relay/messages/peer_state.go rename to shared/relay/messages/peer_state.go diff --git a/relay/messages/peer_state_test.go b/shared/relay/messages/peer_state_test.go similarity index 100% rename from relay/messages/peer_state_test.go rename to shared/relay/messages/peer_state_test.go diff --git a/relay/tls/alpn.go b/shared/relay/tls/alpn.go similarity index 100% rename from relay/tls/alpn.go rename to shared/relay/tls/alpn.go diff --git a/relay/tls/client_dev.go b/shared/relay/tls/client_dev.go similarity index 100% rename from relay/tls/client_dev.go rename to shared/relay/tls/client_dev.go diff --git a/relay/tls/client_prod.go b/shared/relay/tls/client_prod.go similarity index 100% rename from relay/tls/client_prod.go rename to shared/relay/tls/client_prod.go diff --git a/relay/tls/doc.go b/shared/relay/tls/doc.go similarity index 100% rename from relay/tls/doc.go rename to shared/relay/tls/doc.go diff --git a/relay/tls/server_dev.go b/shared/relay/tls/server_dev.go similarity index 100% rename from relay/tls/server_dev.go rename to shared/relay/tls/server_dev.go diff --git a/relay/tls/server_prod.go b/shared/relay/tls/server_prod.go similarity index 100% rename from relay/tls/server_prod.go rename to shared/relay/tls/server_prod.go From dfd8bbc015509d02cacf4bbbb4f2f303a635d651 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Wed, 6 Aug 2025 18:32:35 +0200 Subject: [PATCH 200/213] Change Netbird to NetBird in CMD (#4296) --- client/cmd/debug.go | 18 +++++++++--------- client/cmd/login.go | 2 +- client/cmd/logout.go | 2 +- client/cmd/profile.go | 12 ++++++------ client/cmd/root.go | 6 +++--- client/cmd/root_test.go | 6 +++--- client/cmd/service.go | 4 ++-- client/cmd/service_controller.go | 22 +++++++++++----------- client/cmd/service_installer.go | 20 ++++++++++---------- client/cmd/state.go | 2 +- client/cmd/up.go | 8 ++++---- client/cmd/version.go | 2 +- 12 files changed, 52 insertions(+), 52 deletions(-) diff --git a/client/cmd/debug.go b/client/cmd/debug.go index c59847717..bfb2e61c1 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -33,7 +33,7 @@ var ( var debugCmd = &cobra.Command{ Use: "debug", Short: "Debugging commands", - Long: "Provides commands for debugging and logging control within the Netbird daemon.", + Long: "Provides commands for debugging and logging control within the NetBird daemon.", } var debugBundleCmd = &cobra.Command{ @@ -46,8 +46,8 @@ var debugBundleCmd = &cobra.Command{ var logCmd = &cobra.Command{ Use: "log", - Short: "Manage logging for the Netbird daemon", - Long: `Commands to manage logging settings for the Netbird daemon, including ICE, gRPC, and general log levels.`, + Short: "Manage logging for the NetBird daemon", + Long: `Commands to manage logging settings for the NetBird daemon, including ICE, gRPC, and general log levels.`, } var logLevelCmd = &cobra.Command{ @@ -184,7 +184,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { return fmt.Errorf("failed to up: %v", status.Convert(err).Message()) } - cmd.Println("Netbird up") + cmd.Println("netbird up") time.Sleep(time.Second * 10) } @@ -202,7 +202,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) } - cmd.Println("Netbird down") + cmd.Println("netbird down") time.Sleep(1 * time.Second) @@ -216,11 +216,11 @@ func runForDuration(cmd *cobra.Command, args []string) error { if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { return fmt.Errorf("failed to up: %v", status.Convert(err).Message()) } - cmd.Println("Netbird up") + cmd.Println("netbird up") time.Sleep(3 * time.Second) - headerPostUp := fmt.Sprintf("----- Netbird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) + headerPostUp := fmt.Sprintf("----- NetBird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd, anonymizeFlag)) if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil { @@ -230,7 +230,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { cmd.Println("Creating debug bundle...") - headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) + headerPreDown := fmt.Sprintf("----- NetBird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag)) request := &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, @@ -250,7 +250,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) } - cmd.Println("Netbird down") + cmd.Println("netbird down") } if !initialLevelTrace { diff --git a/client/cmd/login.go b/client/cmd/login.go index d6381f6e2..a6ae13ed8 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -31,7 +31,7 @@ func init() { var loginCmd = &cobra.Command{ Use: "login", - Short: "login to the Netbird Management Service (first run)", + Short: "login to the NetBird Management Service (first run)", RunE: func(cmd *cobra.Command, args []string) error { if err := setEnvAndFlags(cmd); err != nil { return fmt.Errorf("set env and flags: %v", err) diff --git a/client/cmd/logout.go b/client/cmd/logout.go index 071be5ca9..054571fbb 100644 --- a/client/cmd/logout.go +++ b/client/cmd/logout.go @@ -13,7 +13,7 @@ import ( var logoutCmd = &cobra.Command{ Use: "logout", - Short: "logout from the Netbird Management Service and delete peer", + Short: "logout from the NetBird Management Service and delete peer", RunE: func(cmd *cobra.Command, args []string) error { SetFlagsFromEnvVars(rootCmd) diff --git a/client/cmd/profile.go b/client/cmd/profile.go index d420dcbd9..0cb068d05 100644 --- a/client/cmd/profile.go +++ b/client/cmd/profile.go @@ -16,14 +16,14 @@ import ( var profileCmd = &cobra.Command{ Use: "profile", - Short: "manage Netbird profiles", - Long: `Manage Netbird profiles, allowing you to list, switch, and remove profiles.`, + Short: "manage NetBird profiles", + Long: `Manage NetBird profiles, allowing you to list, switch, and remove profiles.`, } var profileListCmd = &cobra.Command{ Use: "list", Short: "list all profiles", - Long: `List all available profiles in the Netbird client.`, + Long: `List all available profiles in the NetBird client.`, Aliases: []string{"ls"}, RunE: listProfilesFunc, } @@ -31,7 +31,7 @@ var profileListCmd = &cobra.Command{ var profileAddCmd = &cobra.Command{ Use: "add ", Short: "add a new profile", - Long: `Add a new profile to the Netbird client. The profile name must be unique.`, + Long: `Add a new profile to the NetBird client. The profile name must be unique.`, Args: cobra.ExactArgs(1), RunE: addProfileFunc, } @@ -39,7 +39,7 @@ var profileAddCmd = &cobra.Command{ var profileRemoveCmd = &cobra.Command{ Use: "remove ", Short: "remove a profile", - Long: `Remove a profile from the Netbird client. The profile must not be active.`, + Long: `Remove a profile from the NetBird client. The profile must not be active.`, Args: cobra.ExactArgs(1), RunE: removeProfileFunc, } @@ -47,7 +47,7 @@ var profileRemoveCmd = &cobra.Command{ var profileSelectCmd = &cobra.Command{ Use: "select ", Short: "select a profile", - Long: `Select a profile to be the active profile in the Netbird client. The profile must exist.`, + Long: `Select a profile to be the active profile in the NetBird client. The profile must exist.`, Args: cobra.ExactArgs(1), RunE: selectProfileFunc, } diff --git a/client/cmd/root.go b/client/cmd/root.go index 86c76e6ab..0f9330601 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -119,12 +119,12 @@ func init() { rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultManagementURL)) rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultAdminURL)) - rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") - rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character") + rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets NetBird log level") + rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets NetBird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character") rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") rootCmd.PersistentFlags().StringVar(&setupKeyPath, "setup-key-file", "", "The path to a setup key obtained from the Management Service Dashboard (used to register peer) This is ignored if the setup-key flag is provided.") rootCmd.MarkFlagsMutuallyExclusive("setup-key", "setup-key-file") - rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.") + rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets WireGuard PreSharedKey property. If set, then only peers that have the same key can communicate.") rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device") rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Overrides the default profile file location") diff --git a/client/cmd/root_test.go b/client/cmd/root_test.go index 4cbbe8783..844eea853 100644 --- a/client/cmd/root_test.go +++ b/client/cmd/root_test.go @@ -50,10 +50,10 @@ func TestSetFlagsFromEnvVars(t *testing.T) { } cmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil, - `comma separated list of external IPs to map to the Wireguard interface`) - cmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name") + `comma separated list of external IPs to map to the WireGuard interface`) + cmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name") cmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "Enable Rosenpass feature Rosenpass.") - cmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port") + cmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port") t.Setenv("NB_EXTERNAL_IP_MAP", "abc,dec") t.Setenv("NB_INTERFACE_NAME", "test-name") diff --git a/client/cmd/service.go b/client/cmd/service.go index d8745f1c4..0c566370b 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -19,7 +19,7 @@ import ( var serviceCmd = &cobra.Command{ Use: "service", - Short: "manages Netbird service", + Short: "manages NetBird service", } var ( @@ -64,7 +64,7 @@ func newSVCConfig() (*service.Config, error) { config := &service.Config{ Name: serviceName, DisplayName: "Netbird", - Description: "Netbird mesh network client", + Description: "NetBird mesh network client", Option: make(service.KeyValue), EnvVars: make(map[string]string), } diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 14a41e607..f67b294d4 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -24,7 +24,7 @@ import ( func (p *program) Start(svc service.Service) error { // Start should not block. Do the actual work async. - log.Info("starting Netbird service") //nolint + log.Info("starting NetBird service") //nolint // Collect static system and platform information system.UpdateStaticInfo() @@ -97,7 +97,7 @@ func (p *program) Stop(srv service.Service) error { } time.Sleep(time.Second * 2) - log.Info("stopped Netbird service") //nolint + log.Info("stopped NetBird service") //nolint return nil } @@ -131,7 +131,7 @@ func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel var runCmd = &cobra.Command{ Use: "run", - Short: "runs Netbird as service", + Short: "runs NetBird as service", RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) @@ -149,7 +149,7 @@ var runCmd = &cobra.Command{ var startCmd = &cobra.Command{ Use: "start", - Short: "starts Netbird service", + Short: "starts NetBird service", RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) s, err := setupServiceControlCommand(cmd, ctx, cancel) @@ -160,14 +160,14 @@ var startCmd = &cobra.Command{ if err := s.Start(); err != nil { return fmt.Errorf("start service: %w", err) } - cmd.Println("Netbird service has been started") + cmd.Println("NetBird service has been started") return nil }, } var stopCmd = &cobra.Command{ Use: "stop", - Short: "stops Netbird service", + Short: "stops NetBird service", RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) s, err := setupServiceControlCommand(cmd, ctx, cancel) @@ -178,14 +178,14 @@ var stopCmd = &cobra.Command{ if err := s.Stop(); err != nil { return fmt.Errorf("stop service: %w", err) } - cmd.Println("Netbird service has been stopped") + cmd.Println("NetBird service has been stopped") return nil }, } var restartCmd = &cobra.Command{ Use: "restart", - Short: "restarts Netbird service", + Short: "restarts NetBird service", RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) s, err := setupServiceControlCommand(cmd, ctx, cancel) @@ -196,14 +196,14 @@ var restartCmd = &cobra.Command{ if err := s.Restart(); err != nil { return fmt.Errorf("restart service: %w", err) } - cmd.Println("Netbird service has been restarted") + cmd.Println("NetBird service has been restarted") return nil }, } var svcStatusCmd = &cobra.Command{ Use: "status", - Short: "shows Netbird service status", + Short: "shows NetBird service status", RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) s, err := setupServiceControlCommand(cmd, ctx, cancel) @@ -228,7 +228,7 @@ var svcStatusCmd = &cobra.Command{ statusText = fmt.Sprintf("Unknown (%d)", status) } - cmd.Printf("Netbird service status: %s\n", statusText) + cmd.Printf("NetBird service status: %s\n", statusText) return nil }, } diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index ac22000bd..92f935d60 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -99,7 +99,7 @@ func createServiceConfigForInstall() (*service.Config, error) { var installCmd = &cobra.Command{ Use: "install", - Short: "installs Netbird service", + Short: "installs NetBird service", RunE: func(cmd *cobra.Command, args []string) error { if err := setupServiceCommand(cmd); err != nil { return err @@ -122,14 +122,14 @@ var installCmd = &cobra.Command{ return fmt.Errorf("install service: %w", err) } - cmd.Println("Netbird service has been installed") + cmd.Println("NetBird service has been installed") return nil }, } var uninstallCmd = &cobra.Command{ Use: "uninstall", - Short: "uninstalls Netbird service from system", + Short: "uninstalls NetBird service from system", RunE: func(cmd *cobra.Command, args []string) error { if err := setupServiceCommand(cmd); err != nil { return err @@ -152,15 +152,15 @@ var uninstallCmd = &cobra.Command{ return fmt.Errorf("uninstall service: %w", err) } - cmd.Println("Netbird service has been uninstalled") + cmd.Println("NetBird service has been uninstalled") return nil }, } var reconfigureCmd = &cobra.Command{ Use: "reconfigure", - Short: "reconfigures Netbird service with new settings", - Long: `Reconfigures the Netbird service with new settings without manual uninstall/install. + Short: "reconfigures NetBird service with new settings", + Long: `Reconfigures the NetBird service with new settings without manual uninstall/install. This command will temporarily stop the service, update its configuration, and restart it if it was running.`, RunE: func(cmd *cobra.Command, args []string) error { if err := setupServiceCommand(cmd); err != nil { @@ -186,7 +186,7 @@ This command will temporarily stop the service, update its configuration, and re } if wasRunning { - cmd.Println("Stopping Netbird service...") + cmd.Println("Stopping NetBird service...") if err := s.Stop(); err != nil { cmd.Printf("Warning: failed to stop service: %v\n", err) } @@ -203,13 +203,13 @@ This command will temporarily stop the service, update its configuration, and re } if wasRunning { - cmd.Println("Starting Netbird service...") + cmd.Println("Starting NetBird service...") if err := s.Start(); err != nil { return fmt.Errorf("start service after reconfigure: %w", err) } - cmd.Println("Netbird service has been reconfigured and started") + cmd.Println("NetBird service has been reconfigured and started") } else { - cmd.Println("Netbird service has been reconfigured") + cmd.Println("NetBird service has been reconfigured") } return nil diff --git a/client/cmd/state.go b/client/cmd/state.go index 21a5508f4..b4612e601 100644 --- a/client/cmd/state.go +++ b/client/cmd/state.go @@ -17,7 +17,7 @@ var ( var stateCmd = &cobra.Command{ Use: "state", Short: "Manage daemon state", - Long: "Provides commands for managing and inspecting the Netbird daemon state.", + Long: "Provides commands for managing and inspecting the NetBird daemon state.", } var stateListCmd = &cobra.Command{ diff --git a/client/cmd/up.go b/client/cmd/up.go index b62925e5e..8732a687d 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -53,15 +53,15 @@ var ( upCmd = &cobra.Command{ Use: "up", - Short: "install, login and start Netbird client", + Short: "install, login and start NetBird client", RunE: upFunc, } ) func init() { upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground") - upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name") - upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port") + upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name") + upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port") upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor, `Manage network monitoring. Defaults to true on Windows and macOS, false on Linux and FreeBSD. `+ `E.g. --network-monitor=false to disable or --network-monitor=true to enable.`, @@ -79,7 +79,7 @@ func init() { upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) - upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location. ") + upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ") } diff --git a/client/cmd/version.go b/client/cmd/version.go index 99f2da698..03541b85e 100644 --- a/client/cmd/version.go +++ b/client/cmd/version.go @@ -9,7 +9,7 @@ import ( var ( versionCmd = &cobra.Command{ Use: "version", - Short: "prints Netbird version", + Short: "prints NetBird version", Run: func(cmd *cobra.Command, args []string) { cmd.SetOut(cmd.OutOrStdout()) cmd.Println(version.NetbirdVersion()) From 5860e5343f794d4cb5b136d020d98ada870b7a48 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:55:14 +0200 Subject: [PATCH 201/213] [management] Rework DB locks (#4291) --- management/server/account.go | 88 +++--- management/server/account_test.go | 62 ++-- management/server/auth/manager.go | 10 +- management/server/dns.go | 14 +- management/server/ephemeral.go | 2 +- management/server/event.go | 6 +- management/server/group.go | 74 ++--- management/server/group_test.go | 6 +- management/server/groups/manager.go | 12 +- management/server/grpcserver.go | 6 +- management/server/integrated_validator.go | 8 +- management/server/management_proto_test.go | 4 +- management/server/nameserver.go | 24 +- management/server/networks/manager.go | 14 +- .../server/networks/resources/manager.go | 30 +- management/server/networks/routers/manager.go | 26 +- management/server/peer.go | 82 ++--- management/server/peer_test.go | 16 +- management/server/peers/manager.go | 10 +- management/server/permissions/manager.go | 4 +- management/server/policy.go | 22 +- management/server/posture_checks.go | 24 +- management/server/route.go | 32 +- management/server/route_test.go | 4 +- management/server/settings/manager.go | 6 +- management/server/setupkey.go | 20 +- management/server/store/sql_store.go | 268 ++++++++-------- management/server/store/sql_store_test.go | 298 +++++++++--------- management/server/store/store.go | 74 ++--- management/server/user.go | 86 ++--- management/server/user_test.go | 6 +- management/server/users/manager.go | 2 +- 32 files changed, 667 insertions(+), 673 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 49ea38fe1..0f60bc91c 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -40,12 +40,12 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/settings" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/status" ) const ( @@ -346,12 +346,12 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco } if updateAccountPeers || groupsUpdated { - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } } - return transaction.SaveAccountSettings(ctx, store.LockingStrengthUpdate, accountID, newSettings) + return transaction.SaveAccountSettings(ctx, accountID, newSettings) }) if err != nil { return nil, err @@ -405,7 +405,7 @@ func (am *DefaultAccountManager) validateSettingsUpdate(ctx context.Context, tra return status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain) } - peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "") if err != nil { return err } @@ -746,7 +746,7 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u // AccountExists checks if an account exists. func (am *DefaultAccountManager) AccountExists(ctx context.Context, accountID string) (bool, error) { - return am.Store.AccountExists(ctx, store.LockingStrengthShare, accountID) + return am.Store.AccountExists(ctx, store.LockingStrengthNone, accountID) } // GetAccountIDByUserID retrieves the account ID based on the userID provided. @@ -758,7 +758,7 @@ func (am *DefaultAccountManager) GetAccountIDByUserID(ctx context.Context, userI return "", status.Errorf(status.NotFound, "no valid userID provided") } - accountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userID) + accountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { if s, ok := status.FromError(err); ok && s.Type() == status.NotFound { account, err := am.GetOrCreateAccountByUser(ctx, userID, domain) @@ -813,7 +813,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any) log.WithContext(ctx).Debugf("account %s not found in cache, reloading", accountID) accountIDString := fmt.Sprintf("%v", accountID) - accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountIDString) + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthNone, accountIDString) if err != nil { return nil, nil, err } @@ -867,7 +867,7 @@ func (am *DefaultAccountManager) lookupUserInCacheByEmail(ctx context.Context, e // lookupUserInCache looks up user in the IdP cache and returns it. If the user wasn't found, the function returns nil func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, accountID string) (*idp.UserData, error) { - accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -897,7 +897,7 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID s // add extra check on external cache manager. We may get to this point when the user is not yet findable in IDP, // or it didn't have its metadata updated with am.addAccountIDToIDPAppMeta - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, accountID) return nil, err @@ -1048,7 +1048,7 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx unlockAccount := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlockAccount() - accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, accountID) + accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error getting account domain and category: %v", err) return err @@ -1058,7 +1058,7 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx return nil } - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userAuth.UserId) if err != nil { log.WithContext(ctx).Errorf("error getting user: %v", err) return err @@ -1145,7 +1145,7 @@ func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, newUser := types.NewRegularUser(userAuth.UserId) newUser.AccountID = domainAccountID - err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser) + err := am.Store.SaveUser(ctx, newUser) if err != nil { return "", err } @@ -1223,7 +1223,7 @@ func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID s return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountMeta(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountMeta(ctx, store.LockingStrengthNone, accountID) } // GetAccountOnboarding retrieves the onboarding information for a specific account. @@ -1308,7 +1308,7 @@ func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, u return "", "", err } - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userAuth.UserId) if err != nil { // this is not really possible because we got an account by user ID return "", "", status.Errorf(status.NotFound, "user %s not found", userAuth.UserId) @@ -1340,7 +1340,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth return nil } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, userAuth.AccountId) if err != nil { return err } @@ -1366,12 +1366,12 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth var hasChanges bool var user *types.User err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) + user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthNone, userAuth.UserId) if err != nil { return fmt.Errorf("error getting user: %w", err) } - groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId) + groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthNone, userAuth.AccountId) if err != nil { return fmt.Errorf("error getting account groups: %w", err) } @@ -1387,7 +1387,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth return nil } - if err = transaction.CreateGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, newGroupsToCreate); err != nil { + if err = transaction.CreateGroups(ctx, userAuth.AccountId, newGroupsToCreate); err != nil { return fmt.Errorf("error saving groups: %w", err) } @@ -1395,13 +1395,13 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth removeOldGroups = util.Difference(user.AutoGroups, updatedAutoGroups) user.AutoGroups = updatedAutoGroups - if err = transaction.SaveUser(ctx, store.LockingStrengthUpdate, user); err != nil { + if err = transaction.SaveUser(ctx, user); err != nil { return fmt.Errorf("error saving user: %w", err) } // Propagate changes to peers if group propagation is enabled if settings.GroupsPropagationEnabled { - peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, userAuth.AccountId, userAuth.UserId) + peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthNone, userAuth.AccountId, userAuth.UserId) if err != nil { return fmt.Errorf("error getting user peers: %w", err) } @@ -1419,7 +1419,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth } } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, userAuth.AccountId); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, userAuth.AccountId); err != nil { return fmt.Errorf("error incrementing network serial: %w", err) } } @@ -1437,7 +1437,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth } for _, g := range addNewGroups { - group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g) + group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthNone, userAuth.AccountId, g) if err != nil { log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId) } else { @@ -1450,7 +1450,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth } for _, g := range removeOldGroups { - group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g) + group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthNone, userAuth.AccountId, g) if err != nil { log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId) } else { @@ -1511,7 +1511,7 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context } if userAuth.IsChild { - exists, err := am.Store.AccountExists(ctx, store.LockingStrengthShare, userAuth.AccountId) + exists, err := am.Store.AccountExists(ctx, store.LockingStrengthNone, userAuth.AccountId) if err != nil || !exists { return "", err } @@ -1535,7 +1535,7 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context return "", err } - userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) + userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthNone, userAuth.UserId) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err) return "", err @@ -1556,7 +1556,7 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context return am.addNewPrivateAccount(ctx, domainAccountID, userAuth) } func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Context, domain string) (string, context.CancelFunc, error) { - domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain) + domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthNone, domain) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err) @@ -1571,7 +1571,7 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont cancel := am.Store.AcquireGlobalLock(ctx) // check again if the domain has a primary account because of simultaneous requests - domainAccountID, err = am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain) + domainAccountID, err = am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthNone, domain) if handleNotFound(err) != nil { cancel() log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err) @@ -1582,7 +1582,7 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont } func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) { - userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) + userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthNone, userAuth.UserId) if err != nil { log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err) return "", err @@ -1592,7 +1592,7 @@ func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context return "", fmt.Errorf("user %s is not part of the account id %s", userAuth.UserId, userAuth.AccountId) } - accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, userAuth.AccountId) + accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthNone, userAuth.AccountId) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf("error getting account domain and category: %v", err) return "", err @@ -1603,7 +1603,7 @@ func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context } // We checked if the domain has a primary account already - domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, userAuth.Domain) + domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthNone, userAuth.Domain) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err) return "", err @@ -1751,7 +1751,7 @@ func (am *DefaultAccountManager) GetAccountIDForPeerKey(ctx context.Context, pee } func (am *DefaultAccountManager) handleUserPeer(ctx context.Context, transaction store.Store, peer *nbpeer.Peer, settings *types.Settings) (bool, error) { - user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, peer.UserID) + user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthNone, peer.UserID) if err != nil { return false, err } @@ -1780,7 +1780,7 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account if !allowed { return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) } // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id @@ -1870,7 +1870,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.C cancel := am.Store.AcquireGlobalLock(ctx) defer cancel() - existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain) + existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthNone, domain) if handleNotFound(err) != nil { return nil, false, err } @@ -1890,7 +1890,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.C for range 2 { accountId := xid.New().String() - exists, err := am.Store.AccountExists(ctx, store.LockingStrengthShare, accountId) + exists, err := am.Store.AccountExists(ctx, store.LockingStrengthNone, accountId) if err != nil || exists { continue } @@ -1965,7 +1965,7 @@ func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, acc return nil } - existingPrimaryAccountID, err := transaction.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, account.Domain) + existingPrimaryAccountID, err := transaction.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthNone, account.Domain) // error is not a not found error if handleNotFound(err) != nil { @@ -2002,17 +2002,17 @@ func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, acc // propagateUserGroupMemberships propagates all account users' group memberships to their peers. // Returns true if any groups were modified, true if those updates affect peers and an error. func propagateUserGroupMemberships(ctx context.Context, transaction store.Store, accountID string) (groupsUpdated bool, peersAffected bool, err error) { - users, err := transaction.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + users, err := transaction.GetAccountUsers(ctx, store.LockingStrengthNone, accountID) if err != nil { return false, false, err } - accountGroupPeers, err := transaction.GetAccountGroupPeers(ctx, store.LockingStrengthShare, accountID) + accountGroupPeers, err := transaction.GetAccountGroupPeers(ctx, store.LockingStrengthNone, accountID) if err != nil { return false, false, fmt.Errorf("error getting account group peers: %w", err) } - accountGroups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + accountGroups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthNone, accountID) if err != nil { return false, false, fmt.Errorf("error getting account groups: %w", err) } @@ -2025,7 +2025,7 @@ func propagateUserGroupMemberships(ctx context.Context, transaction store.Store, updatedGroups := []string{} for _, user := range users { - userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, user.Id) + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthNone, accountID, user.Id) if err != nil { return false, false, err } @@ -2074,7 +2074,7 @@ func (am *DefaultAccountManager) reallocateAccountPeerIPs(ctx context.Context, t account.Network.Net = newIPNet - peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "") if err != nil { return err } @@ -2099,7 +2099,7 @@ func (am *DefaultAccountManager) reallocateAccountPeerIPs(ctx context.Context, t } for _, peer := range peers { - if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { + if err = transaction.SavePeer(ctx, accountID, peer); err != nil { return status.Errorf(status.Internal, "save updated peer %s: %v", peer.ID, err) } } @@ -2154,7 +2154,7 @@ func (am *DefaultAccountManager) updatePeerIPInTransaction(ctx context.Context, return fmt.Errorf("get account: %w", err) } - existingPeer, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID) + existingPeer, err := transaction.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID) if err != nil { return fmt.Errorf("get peer: %w", err) } @@ -2185,7 +2185,7 @@ func (am *DefaultAccountManager) updatePeerIPInTransaction(ctx context.Context, func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transaction store.Store, accountID, userID string, peer *nbpeer.Peer, newIP netip.Addr) error { log.WithContext(ctx).Infof("updating peer %s IP from %s to %s", peer.ID, peer.IP, newIP) - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return fmt.Errorf("get account settings: %w", err) } @@ -2195,7 +2195,7 @@ func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transacti oldIP := peer.IP.String() peer.IP = newIP.AsSlice() - err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer) + err = transaction.SavePeer(ctx, accountID, peer) if err != nil { return fmt.Errorf("save peer: %w", err) } diff --git a/management/server/account_test.go b/management/server/account_test.go index 77014855f..0c618a8a3 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -783,7 +783,7 @@ func TestAccountManager_GetAccountByUserID(t *testing.T) { return } - exists, err := manager.Store.AccountExists(context.Background(), store.LockingStrengthShare, accountID) + exists, err := manager.Store.AccountExists(context.Background(), store.LockingStrengthNone, accountID) assert.NoError(t, err) assert.True(t, exists, "expected to get existing account after creation using userid") @@ -900,11 +900,11 @@ func TestAccountManager_DeleteAccount(t *testing.T) { t.Fatal(fmt.Errorf("expected to get an error when trying to get deleted account, got %v", getAccount)) } - pats, err := manager.Store.GetUserPATs(context.Background(), store.LockingStrengthShare, "service-user-1") + pats, err := manager.Store.GetUserPATs(context.Background(), store.LockingStrengthNone, "service-user-1") require.NoError(t, err) assert.Len(t, pats, 0) - pats, err = manager.Store.GetUserPATs(context.Background(), store.LockingStrengthShare, userId) + pats, err = manager.Store.GetUserPATs(context.Background(), store.LockingStrengthNone, userId) require.NoError(t, err) assert.Len(t, pats, 0) } @@ -1786,7 +1786,7 @@ func TestDefaultAccountManager_DefaultAccountSettings(t *testing.T) { accountID, err := manager.GetAccountIDByUserID(context.Background(), userID, "") require.NoError(t, err, "unable to create an account") - settings, err := manager.Store.GetAccountSettings(context.Background(), store.LockingStrengthShare, accountID) + settings, err := manager.Store.GetAccountSettings(context.Background(), store.LockingStrengthNone, accountID) require.NoError(t, err, "unable to get account settings") assert.NotNil(t, settings) @@ -1971,7 +1971,7 @@ func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) { assert.False(t, updatedSettings.PeerLoginExpirationEnabled) assert.Equal(t, updatedSettings.PeerLoginExpiration, time.Hour) - settings, err := manager.Store.GetAccountSettings(context.Background(), store.LockingStrengthShare, accountID) + settings, err := manager.Store.GetAccountSettings(context.Background(), store.LockingStrengthNone, accountID) require.NoError(t, err, "unable to get account settings") assert.False(t, settings.PeerLoginExpirationEnabled) @@ -2655,7 +2655,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 0, "JWT groups should not be synced") }) @@ -2669,7 +2669,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { err := manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Empty(t, user.AutoGroups, "auto groups must be empty") }) @@ -2683,18 +2683,18 @@ func TestAccount_SetJWTGroups(t *testing.T) { err := manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 0) - group1, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthShare, "accountID", "group1") + group1, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthNone, "accountID", "group1") assert.NoError(t, err, "unable to get group") assert.Equal(t, group1.Issued, types.GroupIssuedAPI, "group should be api issued") }) t.Run("jwt match existing api group in user auto groups", func(t *testing.T) { account.Users["user1"].AutoGroups = []string{"group1"} - assert.NoError(t, manager.Store.SaveUser(context.Background(), store.LockingStrengthUpdate, account.Users["user1"])) + assert.NoError(t, manager.Store.SaveUser(context.Background(), account.Users["user1"])) claims := nbcontext.UserAuth{ UserId: "user1", @@ -2704,11 +2704,11 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 1) - group1, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthShare, "accountID", "group1") + group1, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthNone, "accountID", "group1") assert.NoError(t, err, "unable to get group") assert.Equal(t, group1.Issued, types.GroupIssuedAPI, "group should be api issued") }) @@ -2722,7 +2722,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 2, "groups count should not be change") }) @@ -2736,7 +2736,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 2, "groups count should not be change") }) @@ -2750,11 +2750,11 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - groups, err := manager.Store.GetAccountGroups(context.Background(), store.LockingStrengthShare, "accountID") + groups, err := manager.Store.GetAccountGroups(context.Background(), store.LockingStrengthNone, "accountID") assert.NoError(t, err) assert.Len(t, groups, 3, "new group3 should be added") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user2") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 1, "new group should be added") }) @@ -2768,7 +2768,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user1") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 1, "only non-JWT groups should remain") assert.Contains(t, user.AutoGroups, "group1", "group1 should still be present") @@ -2783,7 +2783,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { err = manager.SyncUserJWTGroups(context.Background(), claims) assert.NoError(t, err, "unable to sync jwt groups") - user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2") + user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user2") assert.NoError(t, err, "unable to get user") assert.Len(t, user.AutoGroups, 0, "all JWT groups should be removed") }) @@ -3348,11 +3348,11 @@ func TestPropagateUserGroupMemberships(t *testing.T) { require.NoError(t, err) peer1 := &nbpeer.Peer{ID: "peer1", AccountID: account.Id, UserID: initiatorId, IP: net.IP{1, 1, 1, 1}, DNSLabel: "peer1.domain.test"} - err = manager.Store.AddPeerToAccount(ctx, store.LockingStrengthUpdate, peer1) + err = manager.Store.AddPeerToAccount(ctx, peer1) require.NoError(t, err) peer2 := &nbpeer.Peer{ID: "peer2", AccountID: account.Id, UserID: initiatorId, IP: net.IP{2, 2, 2, 2}, DNSLabel: "peer2.domain.test"} - err = manager.Store.AddPeerToAccount(ctx, store.LockingStrengthUpdate, peer2) + err = manager.Store.AddPeerToAccount(ctx, peer2) require.NoError(t, err) t.Run("should skip propagation when the user has no groups", func(t *testing.T) { @@ -3364,20 +3364,20 @@ func TestPropagateUserGroupMemberships(t *testing.T) { t.Run("should update membership but no account peers update for unused groups", func(t *testing.T) { group1 := &types.Group{ID: "group1", Name: "Group 1", AccountID: account.Id} - require.NoError(t, manager.Store.CreateGroup(ctx, store.LockingStrengthUpdate, group1)) + require.NoError(t, manager.Store.CreateGroup(ctx, group1)) - user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) + user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorId) require.NoError(t, err) user.AutoGroups = append(user.AutoGroups, group1.ID) - require.NoError(t, manager.Store.SaveUser(ctx, store.LockingStrengthUpdate, user)) + require.NoError(t, manager.Store.SaveUser(ctx, user)) groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) require.NoError(t, err) assert.True(t, groupsUpdated) assert.False(t, groupChangesAffectPeers) - group, err := manager.Store.GetGroupByID(ctx, store.LockingStrengthShare, account.Id, group1.ID) + group, err := manager.Store.GetGroupByID(ctx, store.LockingStrengthNone, account.Id, group1.ID) require.NoError(t, err) assert.Len(t, group.Peers, 2) assert.Contains(t, group.Peers, "peer1") @@ -3386,13 +3386,13 @@ func TestPropagateUserGroupMemberships(t *testing.T) { t.Run("should update membership and account peers for used groups", func(t *testing.T) { group2 := &types.Group{ID: "group2", Name: "Group 2", AccountID: account.Id} - require.NoError(t, manager.Store.CreateGroup(ctx, store.LockingStrengthUpdate, group2)) + require.NoError(t, manager.Store.CreateGroup(ctx, group2)) - user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) + user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorId) require.NoError(t, err) user.AutoGroups = append(user.AutoGroups, group2.ID) - require.NoError(t, manager.Store.SaveUser(ctx, store.LockingStrengthUpdate, user)) + require.NoError(t, manager.Store.SaveUser(ctx, user)) _, err = manager.SavePolicy(context.Background(), account.Id, initiatorId, &types.Policy{ Name: "Group1 Policy", @@ -3415,7 +3415,7 @@ func TestPropagateUserGroupMemberships(t *testing.T) { assert.True(t, groupsUpdated) assert.True(t, groupChangesAffectPeers) - groups, err := manager.Store.GetGroupsByIDs(ctx, store.LockingStrengthShare, account.Id, []string{"group1", "group2"}) + groups, err := manager.Store.GetGroupsByIDs(ctx, store.LockingStrengthNone, account.Id, []string{"group1", "group2"}) require.NoError(t, err) for _, group := range groups { assert.Len(t, group.Peers, 2) @@ -3432,18 +3432,18 @@ func TestPropagateUserGroupMemberships(t *testing.T) { }) t.Run("should not remove peers when groups are removed from user", func(t *testing.T) { - user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorId) + user, err := manager.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorId) require.NoError(t, err) user.AutoGroups = []string{"group1"} - require.NoError(t, manager.Store.SaveUser(ctx, store.LockingStrengthUpdate, user)) + require.NoError(t, manager.Store.SaveUser(ctx, user)) groupsUpdated, groupChangesAffectPeers, err := propagateUserGroupMemberships(ctx, manager.Store, account.Id) require.NoError(t, err) assert.False(t, groupsUpdated) assert.False(t, groupChangesAffectPeers) - groups, err := manager.Store.GetGroupsByIDs(ctx, store.LockingStrengthShare, account.Id, []string{"group1", "group2"}) + groups, err := manager.Store.GetGroupsByIDs(ctx, store.LockingStrengthNone, account.Id, []string{"group1", "group2"}) require.NoError(t, err) for _, group := range groups { assert.Len(t, group.Peers, 2) diff --git a/management/server/auth/manager.go b/management/server/auth/manager.go index 6835a3ced..53d479c90 100644 --- a/management/server/auth/manager.go +++ b/management/server/auth/manager.go @@ -73,7 +73,7 @@ func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbco return userAuth, nil } - settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId) + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, userAuth.AccountId) if err != nil { return userAuth, err } @@ -94,7 +94,7 @@ func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbco // MarkPATUsed marks a personal access token as used func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error { - return am.store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID) + return am.store.MarkPATUsed(ctx, tokenID) } // GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token. @@ -104,7 +104,7 @@ func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.Us return nil, nil, "", "", err } - domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID) + domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthNone, user.AccountID) if err != nil { return nil, nil, "", "", err } @@ -142,12 +142,12 @@ func (am *manager) extractPATFromToken(ctx context.Context, token string) (*type var pat *types.PersonalAccessToken err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken) + pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthNone, encodedHashedToken) if err != nil { return err } - user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID) + user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthNone, pat.ID) return err }) if err != nil { diff --git a/management/server/dns.go b/management/server/dns.go index 489618c17..12aa6e21c 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -8,14 +8,14 @@ import ( log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/shared/management/proto" + "github.com/netbirdio/netbird/shared/management/status" ) // DNSConfigCache is a thread-safe cache for DNS configuration components @@ -72,7 +72,7 @@ func (am *DefaultAccountManager) GetDNSSettings(ctx context.Context, accountID s return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountDNSSettings(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountDNSSettings(ctx, store.LockingStrengthNone, accountID) } // SaveDNSSettings validates a user role and updates the account's DNS settings @@ -113,11 +113,11 @@ func (am *DefaultAccountManager) SaveDNSSettings(ctx context.Context, accountID events := am.prepareDNSSettingsEvents(ctx, transaction, accountID, userID, addedGroups, removedGroups) eventsToStore = append(eventsToStore, events...) - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.SaveDNSSettings(ctx, store.LockingStrengthUpdate, accountID, dnsSettingsToSave) + return transaction.SaveDNSSettings(ctx, accountID, dnsSettingsToSave) }) if err != nil { return err @@ -139,7 +139,7 @@ func (am *DefaultAccountManager) prepareDNSSettingsEvents(ctx context.Context, t var eventsToStore []func() modifiedGroups := slices.Concat(addedGroups, removedGroups) - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, modifiedGroups) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, modifiedGroups) if err != nil { log.WithContext(ctx).Debugf("failed to get groups for dns settings events: %v", err) return nil @@ -195,7 +195,7 @@ func validateDNSSettings(ctx context.Context, transaction store.Store, accountID return nil } - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, settings.DisabledManagementGroups) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, settings.DisabledManagementGroups) if err != nil { return err } diff --git a/management/server/ephemeral.go b/management/server/ephemeral.go index 9f4348ebb..e3cb5459a 100644 --- a/management/server/ephemeral.go +++ b/management/server/ephemeral.go @@ -134,7 +134,7 @@ func (e *EphemeralManager) OnPeerDisconnected(ctx context.Context, peer *nbpeer. } func (e *EphemeralManager) loadEphemeralPeers(ctx context.Context) { - peers, err := e.store.GetAllEphemeralPeers(ctx, store.LockingStrengthShare) + peers, err := e.store.GetAllEphemeralPeers(ctx, store.LockingStrengthNone) if err != nil { log.WithContext(ctx).Debugf("failed to load ephemeral peers: %s", err) return diff --git a/management/server/event.go b/management/server/event.go index 3144c52ea..d26c569ae 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -11,9 +11,9 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/status" ) func isEnabled() bool { @@ -103,7 +103,7 @@ func (am *DefaultAccountManager) fillEventsWithUserInfo(ctx context.Context, eve } func (am *DefaultAccountManager) getEventsUserInfo(ctx context.Context, events []*activity.Event, accountId string, userId string) (map[string]eventUserInfo, error) { - accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountId) + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthNone, accountId) if err != nil { return nil, err } @@ -154,7 +154,7 @@ func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, continue } - externalUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id) + externalUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, id) if err != nil { // @todo consider logging continue diff --git a/management/server/group.go b/management/server/group.go index 2b804b5f6..915a87086 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -14,11 +14,11 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/status" ) type GroupLinkError struct { @@ -49,7 +49,7 @@ func (am *DefaultAccountManager) GetGroup(ctx context.Context, accountID, groupI if err := am.CheckGroupPermissions(ctx, accountID, userID); err != nil { return nil, err } - return am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, groupID) + return am.Store.GetGroupByID(ctx, store.LockingStrengthNone, accountID, groupID) } // GetAllGroups returns all groups in an account @@ -57,12 +57,12 @@ func (am *DefaultAccountManager) GetAllGroups(ctx context.Context, accountID, us if err := am.CheckGroupPermissions(ctx, accountID, userID); err != nil { return nil, err } - return am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountGroups(ctx, store.LockingStrengthNone, accountID) } // GetGroupByName filters all groups in an account by name and returns the one with the most peers func (am *DefaultAccountManager) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) { - return am.Store.GetGroupByName(ctx, store.LockingStrengthShare, accountID, groupName) + return am.Store.GetGroupByName(ctx, store.LockingStrengthNone, accountID, groupName) } // CreateGroup object of the peers @@ -96,11 +96,11 @@ func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, use return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - if err := transaction.CreateGroup(ctx, store.LockingStrengthUpdate, newGroup); err != nil { + if err := transaction.CreateGroup(ctx, newGroup); err != nil { return status.Errorf(status.Internal, "failed to create group: %v", err) } @@ -147,7 +147,7 @@ func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, use return err } - oldGroup, err := transaction.GetGroupByID(ctx, store.LockingStrengthShare, accountID, newGroup.ID) + oldGroup, err := transaction.GetGroupByID(ctx, store.LockingStrengthNone, accountID, newGroup.ID) if err != nil { return status.Errorf(status.NotFound, "group with ID %s not found", newGroup.ID) } @@ -176,11 +176,11 @@ func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, use return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.UpdateGroup(ctx, store.LockingStrengthUpdate, newGroup) + return transaction.UpdateGroup(ctx, newGroup) }) if err != nil { return err @@ -234,11 +234,11 @@ func (am *DefaultAccountManager) CreateGroups(ctx context.Context, accountID, us return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.CreateGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave) + return transaction.CreateGroups(ctx, accountID, groupsToSave) }) if err != nil { return err @@ -292,11 +292,11 @@ func (am *DefaultAccountManager) UpdateGroups(ctx context.Context, accountID, us return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.UpdateGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave) + return transaction.UpdateGroups(ctx, accountID, groupsToSave) }) if err != nil { return err @@ -320,7 +320,7 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac addedPeers := make([]string, 0) removedPeers := make([]string, 0) - oldGroup, err := transaction.GetGroupByID(ctx, store.LockingStrengthShare, accountID, newGroup.ID) + oldGroup, err := transaction.GetGroupByID(ctx, store.LockingStrengthNone, accountID, newGroup.ID) if err == nil && oldGroup != nil { addedPeers = util.Difference(newGroup.Peers, oldGroup.Peers) removedPeers = util.Difference(oldGroup.Peers, newGroup.Peers) @@ -332,13 +332,13 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac } modifiedPeers := slices.Concat(addedPeers, removedPeers) - peers, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthShare, accountID, modifiedPeers) + peers, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthNone, accountID, modifiedPeers) if err != nil { log.WithContext(ctx).Debugf("failed to get peers for group events: %v", err) return nil } - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Debugf("failed to get account settings for group events: %v", err) return nil @@ -423,11 +423,11 @@ func (am *DefaultAccountManager) DeleteGroups(ctx context.Context, accountID, us deletedGroups = append(deletedGroups, group) } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.DeleteGroups(ctx, store.LockingStrengthUpdate, accountID, groupIDsToDelete) + return transaction.DeleteGroups(ctx, accountID, groupIDsToDelete) }) if err != nil { return err @@ -454,7 +454,7 @@ func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, gr return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } @@ -495,11 +495,11 @@ func (am *DefaultAccountManager) GroupAddResource(ctx context.Context, accountID return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.UpdateGroup(ctx, store.LockingStrengthUpdate, group) + return transaction.UpdateGroup(ctx, group) }) if err != nil { return err @@ -526,7 +526,7 @@ func (am *DefaultAccountManager) GroupDeletePeer(ctx context.Context, accountID, return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } @@ -567,11 +567,11 @@ func (am *DefaultAccountManager) GroupDeleteResource(ctx context.Context, accoun return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.UpdateGroup(ctx, store.LockingStrengthUpdate, group) + return transaction.UpdateGroup(ctx, group) }) if err != nil { return err @@ -591,7 +591,7 @@ func validateNewGroup(ctx context.Context, transaction store.Store, accountID st } if newGroup.ID == "" && newGroup.Issued == types.GroupIssuedAPI { - existingGroup, err := transaction.GetGroupByName(ctx, store.LockingStrengthShare, accountID, newGroup.Name) + existingGroup, err := transaction.GetGroupByName(ctx, store.LockingStrengthNone, accountID, newGroup.Name) if err != nil { if s, ok := status.FromError(err); !ok || s.Type() != status.NotFound { return err @@ -608,7 +608,7 @@ func validateNewGroup(ctx context.Context, transaction store.Store, accountID st } for _, peerID := range newGroup.Peers { - _, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID) + _, err := transaction.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID) if err != nil { return status.Errorf(status.InvalidArgument, "peer with ID \"%s\" not found", peerID) } @@ -620,7 +620,7 @@ func validateNewGroup(ctx context.Context, transaction store.Store, accountID st func validateDeleteGroup(ctx context.Context, transaction store.Store, group *types.Group, userID string) error { // disable a deleting integration group if the initiator is not an admin service user if group.Issued == types.GroupIssuedIntegration { - executingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + executingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return status.Errorf(status.Internal, "failed to get user") } @@ -666,7 +666,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty // checkGroupLinkedToSettings verifies if a group is linked to any settings in the account. func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, group *types.Group) error { - dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthShare, group.AccountID) + dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthNone, group.AccountID) if err != nil { return status.Errorf(status.Internal, "failed to get DNS settings") } @@ -675,7 +675,7 @@ func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, gr return &GroupLinkError{"disabled DNS management groups", group.Name} } - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, group.AccountID) + settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthNone, group.AccountID) if err != nil { return status.Errorf(status.Internal, "failed to get account settings") } @@ -689,7 +689,7 @@ func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, gr // isGroupLinkedToRoute checks if a group is linked to any route in the account. func isGroupLinkedToRoute(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *route.Route) { - routes, err := transaction.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID) + routes, err := transaction.GetAccountRoutes(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving routes while checking group linkage: %v", err) return false, nil @@ -709,7 +709,7 @@ func isGroupLinkedToRoute(ctx context.Context, transaction store.Store, accountI // isGroupLinkedToPolicy checks if a group is linked to any policy in the account. func isGroupLinkedToPolicy(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *types.Policy) { - policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthShare, accountID) + policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving policies while checking group linkage: %v", err) return false, nil @@ -727,7 +727,7 @@ func isGroupLinkedToPolicy(ctx context.Context, transaction store.Store, account // isGroupLinkedToDns checks if a group is linked to any nameserver group in the account. func isGroupLinkedToDns(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *nbdns.NameServerGroup) { - nameServerGroups, err := transaction.GetAccountNameServerGroups(ctx, store.LockingStrengthShare, accountID) + nameServerGroups, err := transaction.GetAccountNameServerGroups(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving name server groups while checking group linkage: %v", err) return false, nil @@ -746,7 +746,7 @@ func isGroupLinkedToDns(ctx context.Context, transaction store.Store, accountID // isGroupLinkedToSetupKey checks if a group is linked to any setup key in the account. func isGroupLinkedToSetupKey(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *types.SetupKey) { - setupKeys, err := transaction.GetAccountSetupKeys(ctx, store.LockingStrengthShare, accountID) + setupKeys, err := transaction.GetAccountSetupKeys(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving setup keys while checking group linkage: %v", err) return false, nil @@ -762,7 +762,7 @@ func isGroupLinkedToSetupKey(ctx context.Context, transaction store.Store, accou // isGroupLinkedToUser checks if a group is linked to any user in the account. func isGroupLinkedToUser(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *types.User) { - users, err := transaction.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + users, err := transaction.GetAccountUsers(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving users while checking group linkage: %v", err) return false, nil @@ -778,7 +778,7 @@ func isGroupLinkedToUser(ctx context.Context, transaction store.Store, accountID // isGroupLinkedToNetworkRouter checks if a group is linked to any network router in the account. func isGroupLinkedToNetworkRouter(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *routerTypes.NetworkRouter) { - routers, err := transaction.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthShare, accountID) + routers, err := transaction.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving network routers while checking group linkage: %v", err) return false, nil @@ -798,7 +798,7 @@ func areGroupChangesAffectPeers(ctx context.Context, transaction store.Store, ac return false, nil } - dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthShare, accountID) + dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return false, err } @@ -826,7 +826,7 @@ func areGroupChangesAffectPeers(ctx context.Context, transaction store.Store, ac // anyGroupHasPeersOrResources checks if any of the given groups in the account have peers or resources. func anyGroupHasPeersOrResources(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) (bool, error) { - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, groupIDs) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, groupIDs) if err != nil { return false, err } diff --git a/management/server/group_test.go b/management/server/group_test.go index c31280156..1626a0464 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -26,10 +26,10 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" peer2 "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/permissions" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/status" ) const ( @@ -898,7 +898,7 @@ func Test_AddPeerAndAddToAll(t *testing.T) { } err = manager.Store.ExecuteInTransaction(context.Background(), func(transaction store.Store) error { - err = transaction.AddPeerToAccount(context.Background(), store.LockingStrengthUpdate, peer) + err = transaction.AddPeerToAccount(context.Background(), peer) if err != nil { return fmt.Errorf("AddPeer failed for peer %d: %w", i, err) } @@ -971,7 +971,7 @@ func Test_IncrementNetworkSerial(t *testing.T) { <-start err = manager.Store.ExecuteInTransaction(context.Background(), func(transaction store.Store) error { - err = transaction.IncrementNetworkSerial(context.Background(), store.LockingStrengthNone, accountID) + err = transaction.IncrementNetworkSerial(context.Background(), accountID) if err != nil { return fmt.Errorf("failed to get account %s: %v", accountID, err) } diff --git a/management/server/groups/manager.go b/management/server/groups/manager.go index 8647eeb68..dd11f862f 100644 --- a/management/server/groups/manager.go +++ b/management/server/groups/manager.go @@ -6,12 +6,12 @@ import ( "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" - "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/http/api" ) type Manager interface { @@ -49,7 +49,7 @@ func (m *managerImpl) GetAllGroups(ctx context.Context, accountID, userID string return nil, err } - groups, err := m.store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + groups, err := m.store.GetAccountGroups(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("error getting account groups: %w", err) } @@ -96,13 +96,13 @@ func (m *managerImpl) AddResourceToGroupInTransaction(ctx context.Context, trans return nil, fmt.Errorf("error adding resource to group: %w", err) } - group, err := transaction.GetGroupByID(ctx, store.LockingStrengthShare, accountID, groupID) + group, err := transaction.GetGroupByID(ctx, store.LockingStrengthNone, accountID, groupID) if err != nil { return nil, fmt.Errorf("error getting group: %w", err) } // TODO: at some point, this will need to become a switch statement - networkResource, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, accountID, resource.ID) + networkResource, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthNone, accountID, resource.ID) if err != nil { return nil, fmt.Errorf("error getting network resource: %w", err) } @@ -120,13 +120,13 @@ func (m *managerImpl) RemoveResourceFromGroupInTransaction(ctx context.Context, return nil, fmt.Errorf("error removing resource from group: %w", err) } - group, err := transaction.GetGroupByID(ctx, store.LockingStrengthShare, accountID, groupID) + group, err := transaction.GetGroupByID(ctx, store.LockingStrengthNone, accountID, groupID) if err != nil { return nil, fmt.Errorf("error getting group: %w", err) } // TODO: at some point, this will need to become a switch statement - networkResource, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, accountID, resourceID) + networkResource, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthNone, accountID, resourceID) if err != nil { return nil, fmt.Errorf("error getting network resource: %w", err) } diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index f5d9c250b..8893ad2e2 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -24,7 +24,6 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/auth" @@ -32,9 +31,10 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/settings" - internalStatus "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/proto" + internalStatus "github.com/netbirdio/netbird/shared/management/status" ) // GRPCServer an instance of a Management gRPC API server @@ -920,7 +920,7 @@ func (s *GRPCServer) Logout(ctx context.Context, req *proto.EncryptedMessage) (* return nil, err } - peer, err := s.accountManager.GetStore().GetPeerByPeerPubKey(ctx, store.LockingStrengthShare, peerKey.String()) + peer, err := s.accountManager.GetStore().GetPeerByPeerPubKey(ctx, store.LockingStrengthNone, peerKey.String()) if err != nil { log.WithContext(ctx).Debugf("peer %s is not registered for logout", peerKey.String()) // TODO: consider idempotency diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index 1e92e0c50..73abacc36 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -77,7 +77,7 @@ func (am *DefaultAccountManager) GroupValidation(ctx context.Context, accountID err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { for _, groupID := range groupIDs { - _, err := transaction.GetGroupByID(context.Background(), store.LockingStrengthShare, accountID, groupID) + _, err := transaction.GetGroupByID(context.Background(), store.LockingStrengthNone, accountID, groupID) if err != nil { return err } @@ -97,17 +97,17 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI var peers []*nbpeer.Peer var settings *types.Settings - groups, err = am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + groups, err = am.Store.GetAccountGroups(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } - peers, err = am.Store.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + peers, err = am.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "") if err != nil { return nil, err } - settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index b896c52da..c9f8b5448 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -22,7 +22,6 @@ import ( "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/formatter/hook" - mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/permissions" @@ -30,6 +29,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/util" ) @@ -645,7 +645,7 @@ func testSyncStatusRace(t *testing.T) { } time.Sleep(10 * time.Millisecond) - peer, err := am.Store.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, peerWithInvalidStatus.PublicKey().String()) + peer, err := am.Store.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthNone, peerWithInvalidStatus.PublicKey().String()) if err != nil { t.Fatal(err) return diff --git a/management/server/nameserver.go b/management/server/nameserver.go index ef62d0eaf..1ee8805fc 100644 --- a/management/server/nameserver.go +++ b/management/server/nameserver.go @@ -13,9 +13,9 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/status" ) const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*[*.a-z]{1,}$` @@ -32,7 +32,7 @@ func (am *DefaultAccountManager) GetNameServerGroup(ctx context.Context, account return nil, status.NewPermissionDeniedError() } - return am.Store.GetNameServerGroupByID(ctx, store.LockingStrengthShare, accountID, nsGroupID) + return am.Store.GetNameServerGroupByID(ctx, store.LockingStrengthNone, accountID, nsGroupID) } // CreateNameServerGroup creates and saves a new nameserver group @@ -73,11 +73,11 @@ func (am *DefaultAccountManager) CreateNameServerGroup(ctx context.Context, acco return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.SaveNameServerGroup(ctx, store.LockingStrengthUpdate, newNSGroup) + return transaction.SaveNameServerGroup(ctx, newNSGroup) }) if err != nil { return nil, err @@ -112,7 +112,7 @@ func (am *DefaultAccountManager) SaveNameServerGroup(ctx context.Context, accoun var updateAccountPeers bool err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - oldNSGroup, err := transaction.GetNameServerGroupByID(ctx, store.LockingStrengthShare, accountID, nsGroupToSave.ID) + oldNSGroup, err := transaction.GetNameServerGroupByID(ctx, store.LockingStrengthNone, accountID, nsGroupToSave.ID) if err != nil { return err } @@ -127,11 +127,11 @@ func (am *DefaultAccountManager) SaveNameServerGroup(ctx context.Context, accoun return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.SaveNameServerGroup(ctx, store.LockingStrengthUpdate, nsGroupToSave) + return transaction.SaveNameServerGroup(ctx, nsGroupToSave) }) if err != nil { return err @@ -173,11 +173,11 @@ func (am *DefaultAccountManager) DeleteNameServerGroup(ctx context.Context, acco return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.DeleteNameServerGroup(ctx, store.LockingStrengthUpdate, accountID, nsGroupID) + return transaction.DeleteNameServerGroup(ctx, accountID, nsGroupID) }) if err != nil { return err @@ -202,7 +202,7 @@ func (am *DefaultAccountManager) ListNameServerGroups(ctx context.Context, accou return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountNameServerGroups(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountNameServerGroups(ctx, store.LockingStrengthNone, accountID) } func validateNameServerGroup(ctx context.Context, transaction store.Store, accountID string, nameserverGroup *nbdns.NameServerGroup) error { @@ -216,7 +216,7 @@ func validateNameServerGroup(ctx context.Context, transaction store.Store, accou return err } - nsServerGroups, err := transaction.GetAccountNameServerGroups(ctx, store.LockingStrengthShare, accountID) + nsServerGroups, err := transaction.GetAccountNameServerGroups(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } @@ -226,7 +226,7 @@ func validateNameServerGroup(ctx context.Context, transaction store.Store, accou return err } - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, nameserverGroup.Groups) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, nameserverGroup.Groups) if err != nil { return err } diff --git a/management/server/networks/manager.go b/management/server/networks/manager.go index cb1116b0e..2bab0e289 100644 --- a/management/server/networks/manager.go +++ b/management/server/networks/manager.go @@ -14,8 +14,8 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/shared/management/status" ) type Manager interface { @@ -56,7 +56,7 @@ func (m *managerImpl) GetAllNetworks(ctx context.Context, accountID, userID stri return nil, status.NewPermissionDeniedError() } - return m.store.GetAccountNetworks(ctx, store.LockingStrengthShare, accountID) + return m.store.GetAccountNetworks(ctx, store.LockingStrengthNone, accountID) } func (m *managerImpl) CreateNetwork(ctx context.Context, userID string, network *types.Network) (*types.Network, error) { @@ -73,7 +73,7 @@ func (m *managerImpl) CreateNetwork(ctx context.Context, userID string, network unlock := m.store.AcquireWriteLockByUID(ctx, network.AccountID) defer unlock() - err = m.store.SaveNetwork(ctx, store.LockingStrengthUpdate, network) + err = m.store.SaveNetwork(ctx, network) if err != nil { return nil, fmt.Errorf("failed to save network: %w", err) } @@ -92,7 +92,7 @@ func (m *managerImpl) GetNetwork(ctx context.Context, accountID, userID, network return nil, status.NewPermissionDeniedError() } - return m.store.GetNetworkByID(ctx, store.LockingStrengthShare, accountID, networkID) + return m.store.GetNetworkByID(ctx, store.LockingStrengthNone, accountID, networkID) } func (m *managerImpl) UpdateNetwork(ctx context.Context, userID string, network *types.Network) (*types.Network, error) { @@ -114,7 +114,7 @@ func (m *managerImpl) UpdateNetwork(ctx context.Context, userID string, network m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkUpdated, network.EventMeta()) - return network, m.store.SaveNetwork(ctx, store.LockingStrengthUpdate, network) + return network, m.store.SaveNetwork(ctx, network) } func (m *managerImpl) DeleteNetwork(ctx context.Context, accountID, userID, networkID string) error { @@ -162,12 +162,12 @@ func (m *managerImpl) DeleteNetwork(ctx context.Context, accountID, userID, netw eventsToStore = append(eventsToStore, event) } - err = transaction.DeleteNetwork(ctx, store.LockingStrengthUpdate, accountID, networkID) + err = transaction.DeleteNetwork(ctx, accountID, networkID) if err != nil { return fmt.Errorf("failed to delete network: %w", err) } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID) + err = transaction.IncrementNetworkSerial(ctx, accountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go index 7c8c68e32..d0b29075b 100644 --- a/management/server/networks/resources/manager.go +++ b/management/server/networks/resources/manager.go @@ -12,10 +12,10 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" nbtypes "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/shared/management/status" ) type Manager interface { @@ -57,7 +57,7 @@ func (m *managerImpl) GetAllResourcesInNetwork(ctx context.Context, accountID, u return nil, status.NewPermissionDeniedError() } - return m.store.GetNetworkResourcesByNetID(ctx, store.LockingStrengthShare, accountID, networkID) + return m.store.GetNetworkResourcesByNetID(ctx, store.LockingStrengthNone, accountID, networkID) } func (m *managerImpl) GetAllResourcesInAccount(ctx context.Context, accountID, userID string) ([]*types.NetworkResource, error) { @@ -69,7 +69,7 @@ func (m *managerImpl) GetAllResourcesInAccount(ctx context.Context, accountID, u return nil, status.NewPermissionDeniedError() } - return m.store.GetNetworkResourcesByAccountID(ctx, store.LockingStrengthShare, accountID) + return m.store.GetNetworkResourcesByAccountID(ctx, store.LockingStrengthNone, accountID) } func (m *managerImpl) GetAllResourceIDsInAccount(ctx context.Context, accountID, userID string) (map[string][]string, error) { @@ -81,7 +81,7 @@ func (m *managerImpl) GetAllResourceIDsInAccount(ctx context.Context, accountID, return nil, status.NewPermissionDeniedError() } - resources, err := m.store.GetNetworkResourcesByAccountID(ctx, store.LockingStrengthShare, accountID) + resources, err := m.store.GetNetworkResourcesByAccountID(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("failed to get network resources: %w", err) } @@ -113,7 +113,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc var eventsToStore []func() err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - _, err = transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name) + _, err = transaction.GetNetworkResourceByName(ctx, store.LockingStrengthNone, resource.AccountID, resource.Name) if err == nil { return status.Errorf(status.InvalidArgument, "resource with name %s already exists", resource.Name) } @@ -123,7 +123,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc return fmt.Errorf("failed to get network: %w", err) } - err = transaction.SaveNetworkResource(ctx, store.LockingStrengthUpdate, resource) + err = transaction.SaveNetworkResource(ctx, resource) if err != nil { return fmt.Errorf("failed to save network resource: %w", err) } @@ -145,7 +145,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc eventsToStore = append(eventsToStore, event) } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, resource.AccountID) + err = transaction.IncrementNetworkSerial(ctx, resource.AccountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -174,7 +174,7 @@ func (m *managerImpl) GetResource(ctx context.Context, accountID, userID, networ return nil, status.NewPermissionDeniedError() } - resource, err := m.store.GetNetworkResourceByID(ctx, store.LockingStrengthShare, accountID, resourceID) + resource, err := m.store.GetNetworkResourceByID(ctx, store.LockingStrengthNone, accountID, resourceID) if err != nil { return nil, fmt.Errorf("failed to get network resource: %w", err) } @@ -218,22 +218,22 @@ func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resourc return status.NewResourceNotPartOfNetworkError(resource.ID, resource.NetworkID) } - _, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID) + _, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthNone, resource.AccountID, resource.ID) if err != nil { return fmt.Errorf("failed to get network resource: %w", err) } - oldResource, err := transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name) + oldResource, err := transaction.GetNetworkResourceByName(ctx, store.LockingStrengthNone, resource.AccountID, resource.Name) if err == nil && oldResource.ID != resource.ID { return status.Errorf(status.InvalidArgument, "new resource name already exists") } - oldResource, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID) + oldResource, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthNone, resource.AccountID, resource.ID) if err != nil { return fmt.Errorf("failed to get network resource: %w", err) } - err = transaction.SaveNetworkResource(ctx, store.LockingStrengthUpdate, resource) + err = transaction.SaveNetworkResource(ctx, resource) if err != nil { return fmt.Errorf("failed to save network resource: %w", err) } @@ -248,7 +248,7 @@ func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resourc m.accountManager.StoreEvent(ctx, userID, resource.ID, resource.AccountID, activity.NetworkResourceUpdated, resource.EventMeta(network)) }) - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, resource.AccountID) + err = transaction.IncrementNetworkSerial(ctx, resource.AccountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -325,7 +325,7 @@ func (m *managerImpl) DeleteResource(ctx context.Context, accountID, userID, net return fmt.Errorf("failed to delete resource: %w", err) } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID) + err = transaction.IncrementNetworkSerial(ctx, accountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -375,7 +375,7 @@ func (m *managerImpl) DeleteResourceInTransaction(ctx context.Context, transacti eventsToStore = append(eventsToStore, event) } - err = transaction.DeleteNetworkResource(ctx, store.LockingStrengthUpdate, accountID, resourceID) + err = transaction.DeleteNetworkResource(ctx, accountID, resourceID) if err != nil { return nil, fmt.Errorf("failed to delete network resource: %w", err) } diff --git a/management/server/networks/routers/manager.go b/management/server/networks/routers/manager.go index ff5a771d6..ca99e4fd1 100644 --- a/management/server/networks/routers/manager.go +++ b/management/server/networks/routers/manager.go @@ -14,8 +14,8 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/shared/management/status" ) type Manager interface { @@ -54,7 +54,7 @@ func (m *managerImpl) GetAllRoutersInNetwork(ctx context.Context, accountID, use return nil, status.NewPermissionDeniedError() } - return m.store.GetNetworkRoutersByNetID(ctx, store.LockingStrengthShare, accountID, networkID) + return m.store.GetNetworkRoutersByNetID(ctx, store.LockingStrengthNone, accountID, networkID) } func (m *managerImpl) GetAllRoutersInAccount(ctx context.Context, accountID, userID string) (map[string][]*types.NetworkRouter, error) { @@ -66,7 +66,7 @@ func (m *managerImpl) GetAllRoutersInAccount(ctx context.Context, accountID, use return nil, status.NewPermissionDeniedError() } - routers, err := m.store.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthShare, accountID) + routers, err := m.store.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("failed to get network routers: %w", err) } @@ -93,7 +93,7 @@ func (m *managerImpl) CreateRouter(ctx context.Context, userID string, router *t var network *networkTypes.Network err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - network, err = transaction.GetNetworkByID(ctx, store.LockingStrengthShare, router.AccountID, router.NetworkID) + network, err = transaction.GetNetworkByID(ctx, store.LockingStrengthNone, router.AccountID, router.NetworkID) if err != nil { return fmt.Errorf("failed to get network: %w", err) } @@ -104,12 +104,12 @@ func (m *managerImpl) CreateRouter(ctx context.Context, userID string, router *t router.ID = xid.New().String() - err = transaction.SaveNetworkRouter(ctx, store.LockingStrengthUpdate, router) + err = transaction.SaveNetworkRouter(ctx, router) if err != nil { return fmt.Errorf("failed to create network router: %w", err) } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, router.AccountID) + err = transaction.IncrementNetworkSerial(ctx, router.AccountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -136,7 +136,7 @@ func (m *managerImpl) GetRouter(ctx context.Context, accountID, userID, networkI return nil, status.NewPermissionDeniedError() } - router, err := m.store.GetNetworkRouterByID(ctx, store.LockingStrengthShare, accountID, routerID) + router, err := m.store.GetNetworkRouterByID(ctx, store.LockingStrengthNone, accountID, routerID) if err != nil { return nil, fmt.Errorf("failed to get network router: %w", err) } @@ -162,7 +162,7 @@ func (m *managerImpl) UpdateRouter(ctx context.Context, userID string, router *t var network *networkTypes.Network err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - network, err = transaction.GetNetworkByID(ctx, store.LockingStrengthShare, router.AccountID, router.NetworkID) + network, err = transaction.GetNetworkByID(ctx, store.LockingStrengthNone, router.AccountID, router.NetworkID) if err != nil { return fmt.Errorf("failed to get network: %w", err) } @@ -171,12 +171,12 @@ func (m *managerImpl) UpdateRouter(ctx context.Context, userID string, router *t return status.NewRouterNotPartOfNetworkError(router.ID, router.NetworkID) } - err = transaction.SaveNetworkRouter(ctx, store.LockingStrengthUpdate, router) + err = transaction.SaveNetworkRouter(ctx, router) if err != nil { return fmt.Errorf("failed to update network router: %w", err) } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, router.AccountID) + err = transaction.IncrementNetworkSerial(ctx, router.AccountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -213,7 +213,7 @@ func (m *managerImpl) DeleteRouter(ctx context.Context, accountID, userID, netwo return fmt.Errorf("failed to delete network router: %w", err) } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID) + err = transaction.IncrementNetworkSerial(ctx, accountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -232,7 +232,7 @@ func (m *managerImpl) DeleteRouter(ctx context.Context, accountID, userID, netwo } func (m *managerImpl) DeleteRouterInTransaction(ctx context.Context, transaction store.Store, accountID, userID, networkID, routerID string) (func(), error) { - network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthShare, accountID, networkID) + network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthNone, accountID, networkID) if err != nil { return nil, fmt.Errorf("failed to get network: %w", err) } @@ -246,7 +246,7 @@ func (m *managerImpl) DeleteRouterInTransaction(ctx context.Context, transaction return nil, status.NewRouterNotPartOfNetworkError(routerID, networkID) } - err = transaction.DeleteNetworkRouter(ctx, store.LockingStrengthUpdate, accountID, routerID) + err = transaction.DeleteNetworkRouter(ctx, accountID, routerID) if err != nil { return nil, fmt.Errorf("failed to delete network router: %w", err) } diff --git a/management/server/peer.go b/management/server/peer.go index 6cd519f27..d72eac91a 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -17,28 +17,28 @@ import ( "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/idp" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" + "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/status" ) // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if // the current user is not an admin. func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) { - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID return nil, status.NewPermissionValidationError(err) } - accountPeers, err := am.Store.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, nameFilter, ipFilter) + accountPeers, err := am.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, nameFilter, ipFilter) if err != nil { return nil, err } @@ -58,7 +58,7 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID return accountPeers, nil } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("failed to get account settings: %w", err) } @@ -130,7 +130,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK } if peer.AddedWithSSOLogin() { - settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } @@ -173,7 +173,7 @@ func updatePeerStatusAndLocation(ctx context.Context, geo geolocation.Geolocatio peer.Location.CountryCode = location.Country.ISOCode peer.Location.CityName = location.City.Names.En peer.Location.GeoNameID = location.City.GeonameID - err = transaction.SavePeerLocation(ctx, store.LockingStrengthUpdate, accountID, peer) + err = transaction.SavePeerLocation(ctx, accountID, peer) if err != nil { log.WithContext(ctx).Warnf("could not store location for peer %s: %s", peer.ID, err) } @@ -182,7 +182,7 @@ func updatePeerStatusAndLocation(ctx context.Context, geo geolocation.Geolocatio log.WithContext(ctx).Tracef("saving peer status for peer %s is connected: %t", peer.ID, connected) - err := transaction.SavePeerStatus(ctx, store.LockingStrengthUpdate, accountID, peer.ID, *newStatus) + err := transaction.SavePeerStatus(ctx, accountID, peer.ID, *newStatus) if err != nil { return false, err } @@ -219,7 +219,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user return err } - settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } @@ -281,7 +281,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user inactivityExpirationChanged = true } - return transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer) + return transaction.SavePeer(ctx, accountID, peer) }) if err != nil { return nil, err @@ -346,7 +346,7 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return status.NewPermissionDeniedError() } - peerAccountID, err := am.Store.GetAccountIDByPeerID(ctx, store.LockingStrengthShare, peerID) + peerAccountID, err := am.Store.GetAccountIDByPeerID(ctx, store.LockingStrengthNone, peerID) if err != nil { return err } @@ -383,7 +383,7 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return fmt.Errorf("failed to delete peer: %w", err) } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -617,7 +617,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s }() err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - err = transaction.AddPeerToAccount(ctx, store.LockingStrengthUpdate, newPeer) + err = transaction.AddPeerToAccount(ctx, newPeer) if err != nil { return err } @@ -658,7 +658,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s } } - err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID) + err = transaction.IncrementNetworkSerial(ctx, accountID) if err != nil { return fmt.Errorf("failed to increment network serial: %w", err) } @@ -734,7 +734,7 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy var err error var postureChecks []*posture.Checks - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, nil, nil, err } @@ -746,7 +746,7 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy } if peer.UserID != "" { - user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, peer.UserID) + user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthNone, peer.UserID) if err != nil { return err } @@ -774,7 +774,7 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy if updated { am.metrics.AccountManagerMetrics().CountPeerMetUpdate() log.WithContext(ctx).Tracef("peer %s metadata updated", peer.ID) - if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { + if err = transaction.SavePeer(ctx, accountID, peer); err != nil { return err } @@ -849,7 +849,7 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer var isPeerUpdated bool var postureChecks []*posture.Checks - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, nil, nil, err } @@ -911,7 +911,7 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer } if shouldStorePeer { - if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { + if err = transaction.SavePeer(ctx, accountID, peer); err != nil { return err } } @@ -934,7 +934,7 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer // getPeerPostureChecks returns the posture checks for the peer. func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountID, peerID string) ([]*posture.Checks, error) { - policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthShare, accountID) + policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -958,7 +958,7 @@ func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountI peerPostureChecksIDs = append(peerPostureChecksIDs, postureChecksIDs...) } - peerPostureChecks, err := transaction.GetPostureChecksByIDs(ctx, store.LockingStrengthShare, accountID, peerPostureChecksIDs) + peerPostureChecks, err := transaction.GetPostureChecksByIDs(ctx, store.LockingStrengthNone, accountID, peerPostureChecksIDs) if err != nil { return nil, err } @@ -973,7 +973,7 @@ func processPeerPostureChecks(ctx context.Context, transaction store.Store, poli continue } - sourceGroups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, rule.Sources) + sourceGroups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, rule.Sources) if err != nil { return nil, err } @@ -998,7 +998,7 @@ func processPeerPostureChecks(ctx context.Context, transaction store.Store, poli // with no JWT token and usually no setup-key. As the client can send up to two login request to check if it is expired // and before starting the engine, we do the checks without an account lock to avoid piling up requests. func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Context, accountID string, login types.PeerLogin) error { - peer, err := am.Store.GetPeerByPeerPubKey(ctx, store.LockingStrengthShare, login.WireGuardPubKey) + peer, err := am.Store.GetPeerByPeerPubKey(ctx, store.LockingStrengthNone, login.WireGuardPubKey) if err != nil { return err } @@ -1009,7 +1009,7 @@ func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Co return nil } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } @@ -1080,7 +1080,7 @@ func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, transact // If peer was expired before and if it reached this point, it is re-authenticated. // UserID is present, meaning that JWT validation passed successfully in the API layer. peer = peer.UpdateLastLogin() - err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, peer.AccountID, peer) + err = transaction.SavePeer(ctx, peer.AccountID, peer) if err != nil { return err } @@ -1090,7 +1090,7 @@ func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, transact log.WithContext(ctx).Debugf("failed to update user last login: %v", err) } - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, peer.AccountID) + settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthNone, peer.AccountID) if err != nil { return fmt.Errorf("failed to get account settings: %w", err) } @@ -1132,7 +1132,7 @@ func peerLoginExpired(ctx context.Context, peer *nbpeer.Peer, settings *types.Se // GetPeer for a given accountID, peerID and userID error if not found. func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) { - peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID) + peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID) if err != nil { return nil, err } @@ -1145,7 +1145,7 @@ func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, return peer, nil } - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return nil, err } @@ -1171,7 +1171,7 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun // it is also possible that user doesn't own the peer but some of his peers have access to it, // this is a valid case, show the peer as well. - userPeers, err := am.Store.GetUserPeers(ctx, store.LockingStrengthShare, accountID, userID) + userPeers, err := am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID) if err != nil { return nil, err } @@ -1394,7 +1394,7 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI // If there is no peer that expires this function returns false and a duration of 0. // This function only considers peers that haven't been expired yet and that are connected. func (am *DefaultAccountManager) getNextPeerExpiration(ctx context.Context, accountID string) (time.Duration, bool) { - peersWithExpiry, err := am.Store.GetAccountPeersWithExpiration(ctx, store.LockingStrengthShare, accountID) + peersWithExpiry, err := am.Store.GetAccountPeersWithExpiration(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("failed to get peers with expiration: %v", err) return peerSchedulerRetryInterval, true @@ -1404,7 +1404,7 @@ func (am *DefaultAccountManager) getNextPeerExpiration(ctx context.Context, acco return 0, false } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("failed to get account settings: %v", err) return peerSchedulerRetryInterval, true @@ -1438,7 +1438,7 @@ func (am *DefaultAccountManager) getNextPeerExpiration(ctx context.Context, acco // If there is no peer that expires this function returns false and a duration of 0. // This function only considers peers that haven't been expired yet and that are not connected. func (am *DefaultAccountManager) getNextInactivePeerExpiration(ctx context.Context, accountID string) (time.Duration, bool) { - peersWithInactivity, err := am.Store.GetAccountPeersWithInactivity(ctx, store.LockingStrengthShare, accountID) + peersWithInactivity, err := am.Store.GetAccountPeersWithInactivity(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("failed to get peers with inactivity: %v", err) return peerSchedulerRetryInterval, true @@ -1448,7 +1448,7 @@ func (am *DefaultAccountManager) getNextInactivePeerExpiration(ctx context.Conte return 0, false } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("failed to get account settings: %v", err) return peerSchedulerRetryInterval, true @@ -1479,12 +1479,12 @@ func (am *DefaultAccountManager) getNextInactivePeerExpiration(ctx context.Conte // getExpiredPeers returns peers that have been expired. func (am *DefaultAccountManager) getExpiredPeers(ctx context.Context, accountID string) ([]*nbpeer.Peer, error) { - peersWithExpiry, err := am.Store.GetAccountPeersWithExpiration(ctx, store.LockingStrengthShare, accountID) + peersWithExpiry, err := am.Store.GetAccountPeersWithExpiration(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -1502,12 +1502,12 @@ func (am *DefaultAccountManager) getExpiredPeers(ctx context.Context, accountID // getInactivePeers returns peers that have been expired by inactivity func (am *DefaultAccountManager) getInactivePeers(ctx context.Context, accountID string) ([]*nbpeer.Peer, error) { - peersWithInactivity, err := am.Store.GetAccountPeersWithInactivity(ctx, store.LockingStrengthShare, accountID) + peersWithInactivity, err := am.Store.GetAccountPeersWithInactivity(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -1530,7 +1530,7 @@ func (am *DefaultAccountManager) GetPeerGroups(ctx context.Context, accountID, p // getPeerGroupIDs returns the IDs of the groups that the peer is part of. func getPeerGroupIDs(ctx context.Context, transaction store.Store, accountID string, peerID string) ([]string, error) { - return transaction.GetPeerGroupIDs(ctx, store.LockingStrengthShare, accountID, peerID) + return transaction.GetPeerGroupIDs(ctx, store.LockingStrengthNone, accountID, peerID) } // IsPeerInActiveGroup checks if the given peer is part of a group that is used @@ -1548,7 +1548,7 @@ func isPeerInActiveGroup(ctx context.Context, transaction store.Store, accountID func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction store.Store, accountID, userID string, peers []*nbpeer.Peer) ([]func(), error) { var peerDeletedEvents []func() - settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -1568,7 +1568,7 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto return nil, err } - if err = transaction.DeletePeer(ctx, store.LockingStrengthUpdate, accountID, peer.ID); err != nil { + if err = transaction.DeletePeer(ctx, accountID, peer.ID); err != nil { return nil, err } @@ -1624,7 +1624,7 @@ func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, transac // isPeerLinkedToNetworkRouter checks if a peer is linked to any network router in the account. func isPeerLinkedToNetworkRouter(ctx context.Context, transaction store.Store, accountID string, peerID string) (bool, *routerTypes.NetworkRouter) { - routers, err := transaction.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthShare, accountID) + routers, err := transaction.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthNone, accountID) if err != nil { log.WithContext(ctx).Errorf("error retrieving network routers while checking peer linkage: %v", err) return false, nil diff --git a/management/server/peer_test.go b/management/server/peer_test.go index a78f220d3..5c52692f3 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -38,8 +38,6 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/shared/management/domain" - "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" @@ -47,6 +45,8 @@ import ( "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" nbroute "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" ) func TestPeer_LoginExpired(t *testing.T) { @@ -1307,7 +1307,7 @@ func Test_RegisterPeerByUser(t *testing.T) { require.NoError(t, err) assert.Equal(t, newPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels) - peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, addedPeer.Key) + peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthNone, addedPeer.Key) require.NoError(t, err) assert.Equal(t, peer.AccountID, existingAccountID) assert.Equal(t, peer.UserID, existingUserID) @@ -1442,7 +1442,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { assert.NotNil(t, addedPeer, "addedPeer should not be nil on success") assert.Equal(t, currentPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch") - peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, currentPeer.Key) + peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthNone, currentPeer.Key) require.NoError(t, err, "Failed to get peer by pub key: %s", currentPeer.Key) assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store") assert.Equal(t, currentPeer.ExtraDNSLabels, peerFromStore.ExtraDNSLabels, "ExtraDNSLabels mismatch for peer from store") @@ -1528,7 +1528,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) { _, _, _, err = am.AddPeer(context.Background(), faultyKey, "", newPeer) require.Error(t, err) - _, err = s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, newPeer.Key) + _, err = s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthNone, newPeer.Key) require.Error(t, err) account, err := s.GetAccount(context.Background(), existingAccountID) @@ -1699,7 +1699,7 @@ func Test_LoginPeer(t *testing.T) { assert.Equal(t, existingAccountID, loggedinPeer.AccountID, "AccountID mismatch for logged peer") - peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, loginInput.WireGuardPubKey) + peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthNone, loginInput.WireGuardPubKey) require.NoError(t, err, "Failed to get peer by pub key: %s", loginInput.WireGuardPubKey) assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store") assert.Equal(t, loggedinPeer.ID, peerFromStore.ID, "Peer ID mismatch between loggedinPeer and peerFromStore") @@ -2160,10 +2160,10 @@ func Test_IsUniqueConstraintError(t *testing.T) { } t.Cleanup(cleanup) - err = s.AddPeerToAccount(context.Background(), store.LockingStrengthUpdate, peer) + err = s.AddPeerToAccount(context.Background(), peer) assert.NoError(t, err) - err = s.AddPeerToAccount(context.Background(), store.LockingStrengthUpdate, peer) + err = s.AddPeerToAccount(context.Background(), peer) result := isUniqueConstraintError(err) assert.True(t, result) }) diff --git a/management/server/peers/manager.go b/management/server/peers/manager.go index 7e0262ef9..50e36a880 100644 --- a/management/server/peers/manager.go +++ b/management/server/peers/manager.go @@ -10,8 +10,8 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/shared/management/status" ) type Manager interface { @@ -42,7 +42,7 @@ func (m *managerImpl) GetPeer(ctx context.Context, accountID, userID, peerID str return nil, status.NewPermissionDeniedError() } - return m.store.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID) + return m.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID) } func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) { @@ -52,12 +52,12 @@ func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string) } if !allowed { - return m.store.GetUserPeers(ctx, store.LockingStrengthShare, accountID, userID) + return m.store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID) } - return m.store.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") + return m.store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "") } func (m *managerImpl) GetPeerAccountID(ctx context.Context, peerID string) (string, error) { - return m.store.GetAccountIDByPeerID(ctx, store.LockingStrengthShare, peerID) + return m.store.GetAccountIDByPeerID(ctx, store.LockingStrengthNone, peerID) } diff --git a/management/server/permissions/manager.go b/management/server/permissions/manager.go index 4b2d91c3b..0ab244243 100644 --- a/management/server/permissions/manager.go +++ b/management/server/permissions/manager.go @@ -11,9 +11,9 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/permissions/roles" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/status" ) type Manager interface { @@ -45,7 +45,7 @@ func (m *managerImpl) ValidateUserPermissions( return true, nil } - user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return false, err } diff --git a/management/server/policy.go b/management/server/policy.go index b19f99960..d5c66e9f8 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -6,11 +6,11 @@ import ( "github.com/rs/xid" - "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/posture" @@ -27,7 +27,7 @@ func (am *DefaultAccountManager) GetPolicy(ctx context.Context, accountID, polic return nil, status.NewPermissionDeniedError() } - return am.Store.GetPolicyByID(ctx, store.LockingStrengthShare, accountID, policyID) + return am.Store.GetPolicyByID(ctx, store.LockingStrengthNone, accountID, policyID) } // SavePolicy in the store @@ -61,7 +61,7 @@ func (am *DefaultAccountManager) SavePolicy(ctx context.Context, accountID, user return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } @@ -71,7 +71,7 @@ func (am *DefaultAccountManager) SavePolicy(ctx context.Context, accountID, user saveFunc = transaction.SavePolicy } - return saveFunc(ctx, store.LockingStrengthUpdate, policy) + return saveFunc(ctx, policy) }) if err != nil { return nil, err @@ -113,11 +113,11 @@ func (am *DefaultAccountManager) DeletePolicy(ctx context.Context, accountID, po return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.DeletePolicy(ctx, store.LockingStrengthUpdate, accountID, policyID) + return transaction.DeletePolicy(ctx, accountID, policyID) }) if err != nil { return err @@ -142,13 +142,13 @@ func (am *DefaultAccountManager) ListPolicies(ctx context.Context, accountID, us return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountPolicies(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) } // arePolicyChangesAffectPeers checks if changes to a policy will affect any associated peers. func arePolicyChangesAffectPeers(ctx context.Context, transaction store.Store, accountID string, policy *types.Policy, isUpdate bool) (bool, error) { if isUpdate { - existingPolicy, err := transaction.GetPolicyByID(ctx, store.LockingStrengthShare, accountID, policy.ID) + existingPolicy, err := transaction.GetPolicyByID(ctx, store.LockingStrengthNone, accountID, policy.ID) if err != nil { return false, err } @@ -173,7 +173,7 @@ func arePolicyChangesAffectPeers(ctx context.Context, transaction store.Store, a // validatePolicy validates the policy and its rules. func validatePolicy(ctx context.Context, transaction store.Store, accountID string, policy *types.Policy) error { if policy.ID != "" { - _, err := transaction.GetPolicyByID(ctx, store.LockingStrengthShare, accountID, policy.ID) + _, err := transaction.GetPolicyByID(ctx, store.LockingStrengthNone, accountID, policy.ID) if err != nil { return err } @@ -182,12 +182,12 @@ func validatePolicy(ctx context.Context, transaction store.Store, accountID stri policy.AccountID = accountID } - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, policy.RuleGroups()) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, policy.RuleGroups()) if err != nil { return err } - postureChecks, err := transaction.GetPostureChecksByIDs(ctx, store.LockingStrengthShare, accountID, policy.SourcePostureChecks) + postureChecks, err := transaction.GetPostureChecksByIDs(ctx, store.LockingStrengthNone, accountID, policy.SourcePostureChecks) if err != nil { return err } diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index 50f7e4776..9414b8065 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -13,9 +13,9 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/status" ) func (am *DefaultAccountManager) GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error) { @@ -27,7 +27,7 @@ func (am *DefaultAccountManager) GetPostureChecks(ctx context.Context, accountID return nil, status.NewPermissionDeniedError() } - return am.Store.GetPostureChecksByID(ctx, store.LockingStrengthShare, accountID, postureChecksID) + return am.Store.GetPostureChecksByID(ctx, store.LockingStrengthNone, accountID, postureChecksID) } // SavePostureChecks saves a posture check. @@ -62,7 +62,7 @@ func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountI return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } @@ -70,7 +70,7 @@ func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountI } postureChecks.AccountID = accountID - return transaction.SavePostureChecks(ctx, store.LockingStrengthUpdate, postureChecks) + return transaction.SavePostureChecks(ctx, postureChecks) }) if err != nil { return nil, err @@ -101,7 +101,7 @@ func (am *DefaultAccountManager) DeletePostureChecks(ctx context.Context, accoun var postureChecks *posture.Checks err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - postureChecks, err = transaction.GetPostureChecksByID(ctx, store.LockingStrengthShare, accountID, postureChecksID) + postureChecks, err = transaction.GetPostureChecksByID(ctx, store.LockingStrengthNone, accountID, postureChecksID) if err != nil { return err } @@ -110,11 +110,11 @@ func (am *DefaultAccountManager) DeletePostureChecks(ctx context.Context, accoun return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.DeletePostureChecks(ctx, store.LockingStrengthUpdate, accountID, postureChecksID) + return transaction.DeletePostureChecks(ctx, accountID, postureChecksID) }) if err != nil { return err @@ -135,7 +135,7 @@ func (am *DefaultAccountManager) ListPostureChecks(ctx context.Context, accountI return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountPostureChecks(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountPostureChecks(ctx, store.LockingStrengthNone, accountID) } // getPeerPostureChecks returns the posture checks applied for a given peer. @@ -161,7 +161,7 @@ func (am *DefaultAccountManager) getPeerPostureChecks(account *types.Account, pe // arePostureCheckChangesAffectPeers checks if the changes in posture checks are affecting peers. func arePostureCheckChangesAffectPeers(ctx context.Context, transaction store.Store, accountID, postureCheckID string) (bool, error) { - policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthShare, accountID) + policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) if err != nil { return false, err } @@ -190,14 +190,14 @@ func validatePostureChecks(ctx context.Context, transaction store.Store, account // If the posture check already has an ID, verify its existence in the store. if postureChecks.ID != "" { - if _, err := transaction.GetPostureChecksByID(ctx, store.LockingStrengthShare, accountID, postureChecks.ID); err != nil { + if _, err := transaction.GetPostureChecksByID(ctx, store.LockingStrengthNone, accountID, postureChecks.ID); err != nil { return err } return nil } // For new posture checks, ensure no duplicates by name. - checks, err := transaction.GetAccountPostureChecks(ctx, store.LockingStrengthShare, accountID) + checks, err := transaction.GetAccountPostureChecks(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } @@ -259,7 +259,7 @@ func isPeerInPolicySourceGroups(account *types.Account, peerID string, policy *t // isPostureCheckLinkedToPolicy checks whether the posture check is linked to any account policy. func isPostureCheckLinkedToPolicy(ctx context.Context, transaction store.Store, postureChecksID, accountID string) error { - policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthShare, accountID) + policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } diff --git a/management/server/route.go b/management/server/route.go index 50c4bdc4a..6adff56b5 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -9,15 +9,15 @@ import ( "github.com/rs/xid" - "github.com/netbirdio/netbird/shared/management/domain" - "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/proto" + "github.com/netbirdio/netbird/shared/management/status" ) // GetRoute gets a route object from account and route IDs @@ -30,7 +30,7 @@ func (am *DefaultAccountManager) GetRoute(ctx context.Context, accountID string, return nil, status.NewPermissionDeniedError() } - return am.Store.GetRouteByID(ctx, store.LockingStrengthShare, accountID, string(routeID)) + return am.Store.GetRouteByID(ctx, store.LockingStrengthNone, accountID, string(routeID)) } // checkRoutePrefixOrDomainsExistForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups. @@ -59,7 +59,7 @@ func checkRoutePrefixOrDomainsExistForPeers(ctx context.Context, transaction sto seenPeers[string(prefixRoute.ID)] = true } - peerGroupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, prefixRoute.PeerGroups) + peerGroupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, prefixRoute.PeerGroups) if err != nil { return err } @@ -83,7 +83,7 @@ func checkRoutePrefixOrDomainsExistForPeers(ctx context.Context, transaction sto if peerID := checkRoute.Peer; peerID != "" { // check that peerID exists and is not in any route as single peer or part of the group - _, err = transaction.GetPeerByID(context.Background(), store.LockingStrengthShare, accountID, peerID) + _, err = transaction.GetPeerByID(context.Background(), store.LockingStrengthNone, accountID, peerID) if err != nil { return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) } @@ -104,7 +104,7 @@ func checkRoutePrefixOrDomainsExistForPeers(ctx context.Context, transaction sto } // check that the peers from peerGroupIDs groups are not the same peers we saw in routesWithPrefix - peersMap, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthShare, accountID, group.Peers) + peersMap, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthNone, accountID, group.Peers) if err != nil { return err } @@ -181,11 +181,11 @@ func (am *DefaultAccountManager) CreateRoute(ctx context.Context, accountID stri return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.SaveRoute(ctx, store.LockingStrengthUpdate, newRoute) + return transaction.SaveRoute(ctx, newRoute) }) if err != nil { return nil, err @@ -238,11 +238,11 @@ func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userI } routeToSave.AccountID = accountID - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.SaveRoute(ctx, store.LockingStrengthUpdate, routeToSave) + return transaction.SaveRoute(ctx, routeToSave) }) if err != nil { return err @@ -284,11 +284,11 @@ func (am *DefaultAccountManager) DeleteRoute(ctx context.Context, accountID stri return err } - if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil { return err } - return transaction.DeleteRoute(ctx, store.LockingStrengthUpdate, accountID, string(routeID)) + return transaction.DeleteRoute(ctx, accountID, string(routeID)) }) am.StoreEvent(ctx, userID, string(route.ID), accountID, activity.RouteRemoved, route.EventMeta()) @@ -310,7 +310,7 @@ func (am *DefaultAccountManager) ListRoutes(ctx context.Context, accountID, user return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountRoutes(ctx, store.LockingStrengthNone, accountID) } func validateRoute(ctx context.Context, transaction store.Store, accountID string, routeToSave *route.Route) error { @@ -353,7 +353,7 @@ func validateRoute(ctx context.Context, transaction store.Store, accountID strin // validateRouteGroups validates the route groups and returns the validated groups map. func validateRouteGroups(ctx context.Context, transaction store.Store, accountID string, routeToSave *route.Route) (map[string]*types.Group, error) { groupsToValidate := slices.Concat(routeToSave.Groups, routeToSave.PeerGroups, routeToSave.AccessControlGroups) - groupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, groupsToValidate) + groupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, groupsToValidate) if err != nil { return nil, err } @@ -494,7 +494,7 @@ func areRouteChangesAffectPeers(ctx context.Context, transaction store.Store, ro // GetRoutesByPrefixOrDomains return list of routes by account and route prefix func getRoutesByPrefixOrDomains(ctx context.Context, transaction store.Store, accountID string, prefix netip.Prefix, domains domain.List) ([]*route.Route, error) { - accountRoutes, err := transaction.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID) + accountRoutes, err := transaction.GetAccountRoutes(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } diff --git a/management/server/route_test.go b/management/server/route_test.go index ac51c25fd..c3eea35ea 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -27,6 +26,7 @@ import ( "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/domain" ) const ( @@ -1100,7 +1100,7 @@ func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) { require.NoError(t, err) assert.Len(t, peer4Routes.Routes, 1, "HA route should have 1 server route") - groups, err := am.Store.GetAccountGroups(context.Background(), store.LockingStrengthShare, account.Id) + groups, err := am.Store.GetAccountGroups(context.Background(), store.LockingStrengthNone, account.Id) require.NoError(t, err) var groupHA1, groupHA2 *types.Group for _, group := range groups { diff --git a/management/server/settings/manager.go b/management/server/settings/manager.go index 9cb142938..6d09f1786 100644 --- a/management/server/settings/manager.go +++ b/management/server/settings/manager.go @@ -11,10 +11,10 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" + "github.com/netbirdio/netbird/shared/management/status" ) type Manager interface { @@ -60,7 +60,7 @@ func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string) return nil, fmt.Errorf("get extra settings: %w", err) } - settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("get account settings: %w", err) } @@ -82,7 +82,7 @@ func (m *managerImpl) GetExtraSettings(ctx context.Context, accountID string) (* return nil, fmt.Errorf("get extra settings: %w", err) } - settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("get account settings: %w", err) } diff --git a/management/server/setupkey.go b/management/server/setupkey.go index e3647f6df..71915b4a2 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -10,10 +10,10 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/shared/management/status" ) const ( @@ -81,7 +81,7 @@ func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID s events := am.prepareSetupKeyEvents(ctx, transaction, accountID, userID, autoGroups, nil, setupKey) eventsToStore = append(eventsToStore, events...) - return transaction.SaveSetupKey(ctx, store.LockingStrengthUpdate, setupKey) + return transaction.SaveSetupKey(ctx, setupKey) }) if err != nil { return nil, err @@ -127,7 +127,7 @@ func (am *DefaultAccountManager) SaveSetupKey(ctx context.Context, accountID str return status.Errorf(status.InvalidArgument, "invalid auto groups: %v", err) } - oldKey, err = transaction.GetSetupKeyByID(ctx, store.LockingStrengthShare, accountID, keyToSave.Id) + oldKey, err = transaction.GetSetupKeyByID(ctx, store.LockingStrengthUpdate, accountID, keyToSave.Id) if err != nil { return err } @@ -148,7 +148,7 @@ func (am *DefaultAccountManager) SaveSetupKey(ctx context.Context, accountID str events := am.prepareSetupKeyEvents(ctx, transaction, accountID, userID, addedGroups, removedGroups, oldKey) eventsToStore = append(eventsToStore, events...) - return transaction.SaveSetupKey(ctx, store.LockingStrengthUpdate, newKey) + return transaction.SaveSetupKey(ctx, newKey) }) if err != nil { return nil, err @@ -175,7 +175,7 @@ func (am *DefaultAccountManager) ListSetupKeys(ctx context.Context, accountID, u return nil, status.NewPermissionDeniedError() } - return am.Store.GetAccountSetupKeys(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountSetupKeys(ctx, store.LockingStrengthNone, accountID) } // GetSetupKey looks up a SetupKey by KeyID, returns NotFound error if not found. @@ -188,7 +188,7 @@ func (am *DefaultAccountManager) GetSetupKey(ctx context.Context, accountID, use return nil, status.NewPermissionDeniedError() } - setupKey, err := am.Store.GetSetupKeyByID(ctx, store.LockingStrengthShare, accountID, keyID) + setupKey, err := am.Store.GetSetupKeyByID(ctx, store.LockingStrengthNone, accountID, keyID) if err != nil { return nil, err } @@ -214,12 +214,12 @@ func (am *DefaultAccountManager) DeleteSetupKey(ctx context.Context, accountID, var deletedSetupKey *types.SetupKey err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - deletedSetupKey, err = transaction.GetSetupKeyByID(ctx, store.LockingStrengthShare, accountID, keyID) + deletedSetupKey, err = transaction.GetSetupKeyByID(ctx, store.LockingStrengthUpdate, accountID, keyID) if err != nil { return err } - return transaction.DeleteSetupKey(ctx, store.LockingStrengthUpdate, accountID, keyID) + return transaction.DeleteSetupKey(ctx, accountID, keyID) }) if err != nil { return err @@ -231,7 +231,7 @@ func (am *DefaultAccountManager) DeleteSetupKey(ctx context.Context, accountID, } func validateSetupKeyAutoGroups(ctx context.Context, transaction store.Store, accountID string, autoGroupIDs []string) error { - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, autoGroupIDs) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, autoGroupIDs) if err != nil { return err } @@ -255,7 +255,7 @@ func (am *DefaultAccountManager) prepareSetupKeyEvents(ctx context.Context, tran var eventsToStore []func() modifiedGroups := slices.Concat(addedGroups, removedGroups) - groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, modifiedGroups) + groups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthNone, accountID, modifiedGroups) if err != nil { log.WithContext(ctx).Debugf("failed to get groups for setup key events: %v", err) return nil diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index c0f683124..5986214d9 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -29,11 +29,11 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/status" ) const ( @@ -270,7 +270,7 @@ func generateAccountSQLTypes(account *types.Account) { func (s *SqlStore) checkAccountDomainBeforeSave(ctx context.Context, accountID, newDomain string) { var acc types.Account var domain string - result := s.db.Model(&acc).Select("domain").Where(idQueryCondition, accountID).First(&domain) + result := s.db.Model(&acc).Select("domain").Where(idQueryCondition, accountID).Take(&domain) if result.Error != nil { if !errors.Is(result.Error, gorm.ErrRecordNotFound) { log.WithContext(ctx).Errorf("error when getting account %s from the store to check domain: %s", accountID, result.Error) @@ -323,23 +323,26 @@ func (s *SqlStore) SaveInstallationID(_ context.Context, ID string) error { func (s *SqlStore) GetInstallationID() string { var installation installation - if result := s.db.First(&installation, idQueryCondition, s.installationPK); result.Error != nil { + if result := s.db.Take(&installation, idQueryCondition, s.installationPK); result.Error != nil { return "" } return installation.InstallationIDValue } -func (s *SqlStore) SavePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peer *nbpeer.Peer) error { +func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error { // To maintain data integrity, we create a copy of the peer's to prevent unintended updates to other fields. peerCopy := peer.Copy() peerCopy.AccountID = accountID - err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Transaction(func(tx *gorm.DB) error { + err := s.db.Transaction(func(tx *gorm.DB) error { // check if peer exists before saving var peerID string - result := tx.Model(&nbpeer.Peer{}).Select("id").Find(&peerID, accountAndIDQueryCondition, accountID, peer.ID) + result := tx.Model(&nbpeer.Peer{}).Select("id").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID) if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID) + } return result.Error } @@ -385,7 +388,7 @@ func (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID return nil } -func (s *SqlStore) SavePeerStatus(ctx context.Context, lockStrength LockingStrength, accountID, peerID string, peerStatus nbpeer.PeerStatus) error { +func (s *SqlStore) SavePeerStatus(ctx context.Context, accountID, peerID string, peerStatus nbpeer.PeerStatus) error { var peerCopy nbpeer.Peer peerCopy.Status = &peerStatus @@ -393,7 +396,7 @@ func (s *SqlStore) SavePeerStatus(ctx context.Context, lockStrength LockingStren "peer_status_last_seen", "peer_status_connected", "peer_status_login_expired", "peer_status_required_approval", } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). + result := s.db.Model(&nbpeer.Peer{}). Select(fieldsToUpdate). Where(accountAndIDQueryCondition, accountID, peerID). Updates(&peerCopy) @@ -408,14 +411,14 @@ func (s *SqlStore) SavePeerStatus(ctx context.Context, lockStrength LockingStren return nil } -func (s *SqlStore) SavePeerLocation(ctx context.Context, lockStrength LockingStrength, accountID string, peerWithLocation *nbpeer.Peer) error { +func (s *SqlStore) SavePeerLocation(ctx context.Context, accountID string, peerWithLocation *nbpeer.Peer) error { // To maintain data integrity, we create a copy of the peer's location to prevent unintended updates to other fields. var peerCopy nbpeer.Peer // Since the location field has been migrated to JSON serialization, // updating the struct ensures the correct data format is inserted into the database. peerCopy.Location = peerWithLocation.Location - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). + result := s.db.Model(&nbpeer.Peer{}). Where(accountAndIDQueryCondition, accountID, peerWithLocation.ID). Updates(peerCopy) @@ -431,12 +434,12 @@ func (s *SqlStore) SavePeerLocation(ctx context.Context, lockStrength LockingStr } // SaveUsers saves the given list of users to the database. -func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, users []*types.User) error { +func (s *SqlStore) SaveUsers(ctx context.Context, users []*types.User) error { if len(users) == 0 { return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&users) + result := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&users) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save users to store") @@ -445,8 +448,8 @@ func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, } // SaveUser saves the given user to the database. -func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, user *types.User) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(user) +func (s *SqlStore) SaveUser(ctx context.Context, user *types.User) error { + result := s.db.Save(user) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save user to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save user to store") @@ -455,7 +458,7 @@ func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, u } // CreateGroups creates the given list of groups to the database. -func (s *SqlStore) CreateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error { +func (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error { if len(groups) == 0 { return nil } @@ -463,7 +466,6 @@ func (s *SqlStore) CreateGroups(ctx context.Context, lockStrength LockingStrengt return s.db.Transaction(func(tx *gorm.DB) error { result := tx. Clauses( - clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{ Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}}, UpdateAll: true, @@ -481,7 +483,7 @@ func (s *SqlStore) CreateGroups(ctx context.Context, lockStrength LockingStrengt } // UpdateGroups updates the given list of groups to the database. -func (s *SqlStore) UpdateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error { +func (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error { if len(groups) == 0 { return nil } @@ -489,7 +491,6 @@ func (s *SqlStore) UpdateGroups(ctx context.Context, lockStrength LockingStrengt return s.db.Transaction(func(tx *gorm.DB) error { result := tx. Clauses( - clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{ Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}}, UpdateAll: true, @@ -517,7 +518,7 @@ func (s *SqlStore) DeleteTokenID2UserIDIndex(tokenID string) error { } func (s *SqlStore) GetAccountByPrivateDomain(ctx context.Context, domain string) (*types.Account, error) { - accountID, err := s.GetAccountIDByPrivateDomain(ctx, LockingStrengthShare, domain) + accountID, err := s.GetAccountIDByPrivateDomain(ctx, LockingStrengthNone, domain) if err != nil { return nil, err } @@ -536,7 +537,7 @@ func (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength result := tx.Model(&types.Account{}).Select("id"). Where("domain = ? and is_domain_primary_account = ? and domain_category = ?", strings.ToLower(domain), true, types.PrivateCategory, - ).First(&accountID) + ).Take(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.Errorf(status.NotFound, "account not found: provided domain is not registered or is not private") @@ -550,7 +551,7 @@ func (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength func (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (*types.Account, error) { var key types.SetupKey - result := s.db.Select("account_id").First(&key, GetKeyQueryCondition(s), setupKey) + result := s.db.Select("account_id").Take(&key, GetKeyQueryCondition(s), setupKey) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewSetupKeyNotFoundError(setupKey) @@ -568,7 +569,7 @@ func (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (* func (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken string) (string, error) { var token types.PersonalAccessToken - result := s.db.First(&token, "hashed_token = ?", hashedToken) + result := s.db.Take(&token, "hashed_token = ?", hashedToken) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -589,7 +590,7 @@ func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStren var user types.User result := tx. Joins("JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id"). - Where("personal_access_tokens.id = ?", patID).First(&user) + Where("personal_access_tokens.id = ?", patID).Take(&user) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPATNotFoundError(patID) @@ -608,7 +609,7 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre } var user types.User - result := tx.First(&user, idQueryCondition, userID) + result := tx.Take(&user, idQueryCondition, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewUserNotFoundError(userID) @@ -619,16 +620,14 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre return &user, nil } -func (s *SqlStore) DeleteUser(ctx context.Context, lockStrength LockingStrength, accountID, userID string) error { +func (s *SqlStore) DeleteUser(ctx context.Context, accountID, userID string) error { err := s.db.Transaction(func(tx *gorm.DB) error { - result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&types.PersonalAccessToken{}, "user_id = ?", userID) + result := tx.Delete(&types.PersonalAccessToken{}, "user_id = ?", userID) if result.Error != nil { return result.Error } - return tx.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error + return tx.Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error }) if err != nil { log.WithContext(ctx).Errorf("failed to delete user from the store: %s", err) @@ -664,7 +663,7 @@ func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStre } var user types.User - result := tx.First(&user, "account_id = ? AND role = ?", accountID, types.UserRoleOwner) + result := tx.Take(&user, "account_id = ? AND role = ?", accountID, types.UserRoleOwner) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "account owner not found: index lookup failed") @@ -761,7 +760,7 @@ func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStren var accountMeta types.AccountMeta result := tx.Model(&types.Account{}). - First(&accountMeta, idQueryCondition, accountID) + Take(&accountMeta, idQueryCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("error when getting account meta %s from the store: %s", accountID, result.Error) if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -776,7 +775,7 @@ func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStren // GetAccountOnboarding retrieves the onboarding information for a specific account. func (s *SqlStore) GetAccountOnboarding(ctx context.Context, accountID string) (*types.AccountOnboarding, error) { var accountOnboarding types.AccountOnboarding - result := s.db.Model(&accountOnboarding).First(&accountOnboarding, accountIDCondition, accountID) + result := s.db.Model(&accountOnboarding).Take(&accountOnboarding, accountIDCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewAccountOnboardingNotFoundError(accountID) @@ -813,7 +812,7 @@ func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Acc Omit("GroupsG"). Preload("UsersG.PATsG"). // have to be specifies as this is nester reference Preload(clause.Associations). - First(&account, idQueryCondition, accountID) + Take(&account, idQueryCondition, accountID) if result.Error != nil { log.WithContext(ctx).Errorf("error when getting account %s from the store: %s", accountID, result.Error) if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -888,7 +887,7 @@ func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Acc func (s *SqlStore) GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) { var user types.User - result := s.db.Select("account_id").First(&user, idQueryCondition, userID) + result := s.db.Select("account_id").Take(&user, idQueryCondition, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -905,7 +904,7 @@ func (s *SqlStore) GetAccountByUser(ctx context.Context, userID string) (*types. func (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) { var peer nbpeer.Peer - result := s.db.Select("account_id").First(&peer, idQueryCondition, peerID) + result := s.db.Select("account_id").Take(&peer, idQueryCondition, peerID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -922,7 +921,7 @@ func (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*type func (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) { var peer nbpeer.Peer - result := s.db.Select("account_id").First(&peer, GetKeyQueryCondition(s), peerKey) + result := s.db.Select("account_id").Take(&peer, GetKeyQueryCondition(s), peerKey) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -954,7 +953,7 @@ func (s *SqlStore) GetAnyAccountID(ctx context.Context) (string, error) { func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) { var peer nbpeer.Peer var accountID string - result := s.db.Model(&peer).Select("account_id").Where(GetKeyQueryCondition(s), peerKey).First(&accountID) + result := s.db.Model(&peer).Select("account_id").Where(GetKeyQueryCondition(s), peerKey).Take(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -973,7 +972,7 @@ func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength Lockin var accountID string result := tx.Model(&types.User{}). - Select("account_id").Where(idQueryCondition, userID).First(&accountID) + Select("account_id").Where(idQueryCondition, userID).Take(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -992,7 +991,7 @@ func (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength Lockin var accountID string result := tx.Model(&nbpeer.Peer{}). - Select("account_id").Where(idQueryCondition, peerID).First(&accountID) + Select("account_id").Where(idQueryCondition, peerID).Take(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.Errorf(status.NotFound, "peer %s account not found", peerID) @@ -1005,7 +1004,7 @@ func (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength Lockin func (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) (string, error) { var accountID string - result := s.db.Model(&types.SetupKey{}).Select("account_id").Where(GetKeyQueryCondition(s), setupKey).First(&accountID) + result := s.db.Model(&types.SetupKey{}).Select("account_id").Where(GetKeyQueryCondition(s), setupKey).Take(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.NewSetupKeyNotFoundError(setupKey) @@ -1082,7 +1081,7 @@ func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingSt } var accountNetwork types.AccountNetwork - if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountNetwork).Error; err != nil { + if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountNetwork).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewAccountNotFoundError(accountID) } @@ -1098,7 +1097,7 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking } var peer nbpeer.Peer - result := tx.First(&peer, GetKeyQueryCondition(s), peerKey) + result := tx.Take(&peer, GetKeyQueryCondition(s), peerKey) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1117,7 +1116,7 @@ func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingS } var accountSettings types.AccountSettings - if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountSettings).Error; err != nil { + if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountSettings).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.Errorf(status.NotFound, "settings not found") } @@ -1134,7 +1133,7 @@ func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength Locking var createdBy string result := tx.Model(&types.Account{}). - Select("created_by").First(&createdBy, idQueryCondition, accountID) + Select("created_by").Take(&createdBy, idQueryCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.NewAccountNotFoundError(accountID) @@ -1148,7 +1147,7 @@ func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength Locking // SaveUserLastLogin stores the last login time for a user in DB. func (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error { var user types.User - result := s.db.First(&user, accountAndIDQueryCondition, accountID, userID) + result := s.db.Take(&user, accountAndIDQueryCondition, accountID, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return status.NewUserNotFoundError(userID) @@ -1171,7 +1170,7 @@ func (s *SqlStore) GetPostureCheckByChecksDefinition(accountID string, checks *p } var postureCheck posture.Checks - err = s.db.Where("account_id = ? AND checks = ?", accountID, string(definitionJSON)).First(&postureCheck).Error + err = s.db.Where("account_id = ? AND checks = ?", accountID, string(definitionJSON)).Take(&postureCheck).Error if err != nil { return nil, err } @@ -1336,7 +1335,7 @@ func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength Locking var setupKey types.SetupKey result := tx. - First(&setupKey, GetKeyQueryCondition(s), key) + Take(&setupKey, GetKeyQueryCondition(s), key) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1446,7 +1445,7 @@ func (s *SqlStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) e // AddResourceToGroup adds a resource to a group. Method always needs to run n a transaction func (s *SqlStore) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error { var group types.Group - result := s.db.Where(accountAndIDQueryCondition, accountId, groupID).First(&group) + result := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return status.NewGroupNotFoundError(groupID) @@ -1473,7 +1472,7 @@ func (s *SqlStore) AddResourceToGroup(ctx context.Context, accountId string, gro // RemoveResourceFromGroup removes a resource from a group. Method always needs to run in a transaction func (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error { var group types.Group - result := s.db.Where(accountAndIDQueryCondition, accountId, groupID).First(&group) + result := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return status.NewGroupNotFoundError(groupID) @@ -1593,7 +1592,7 @@ func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrengt return peers, nil } -func (s *SqlStore) AddPeerToAccount(ctx context.Context, lockStrength LockingStrength, peer *nbpeer.Peer) error { +func (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error { if err := s.db.Create(peer).Error; err != nil { return status.Errorf(status.Internal, "issue adding peer to account: %s", err) } @@ -1610,7 +1609,7 @@ func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength var peer *nbpeer.Peer result := tx. - First(&peer, accountAndIDQueryCondition, accountID, peerID) + Take(&peer, accountAndIDQueryCondition, accountID, peerID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPeerNotFoundError(peerID) @@ -1705,9 +1704,8 @@ func (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength Lockin } // DeletePeer removes a peer from the store. -func (s *SqlStore) DeletePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID) +func (s *SqlStore) DeletePeer(ctx context.Context, accountID string, peerID string) error { + result := s.db.Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to delete peer from the store: %s", err) return status.Errorf(status.Internal, "failed to delete peer from store") @@ -1720,9 +1718,8 @@ func (s *SqlStore) DeletePeer(ctx context.Context, lockStrength LockingStrength, return nil } -func (s *SqlStore) IncrementNetworkSerial(ctx context.Context, lockStrength LockingStrength, accountId string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Model(&types.Account{}).Where(idQueryCondition, accountId).Update("network_serial", gorm.Expr("network_serial + 1")) +func (s *SqlStore) IncrementNetworkSerial(ctx context.Context, accountId string) error { + result := s.db.Model(&types.Account{}).Where(idQueryCondition, accountId).Update("network_serial", gorm.Expr("network_serial + 1")) if result.Error != nil { log.WithContext(ctx).Errorf("failed to increment network serial count in store: %v", result.Error) return status.Errorf(status.Internal, "failed to increment network serial count in store") @@ -1772,7 +1769,7 @@ func (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength Locki var accountDNSSettings types.AccountDNSSettings result := tx.Model(&types.Account{}). - First(&accountDNSSettings, idQueryCondition, accountID) + Take(&accountDNSSettings, idQueryCondition, accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewAccountNotFoundError(accountID) @@ -1792,7 +1789,7 @@ func (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStreng var accountID string result := tx.Model(&types.Account{}). - Select("id").First(&accountID, idQueryCondition, id) + Select("id").Take(&accountID, idQueryCondition, id) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return false, nil @@ -1812,7 +1809,7 @@ func (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength var account types.Account result := tx.Model(&types.Account{}).Select("domain", "domain_category"). - Where(idQueryCondition, accountID).First(&account) + Where(idQueryCondition, accountID).Take(&account) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", "", status.Errorf(status.NotFound, "account not found") @@ -1831,7 +1828,7 @@ func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrengt } var group *types.Group - result := tx.Preload(clause.Associations).First(&group, accountAndIDQueryCondition, accountID, groupID) + result := tx.Preload(clause.Associations).Take(&group, accountAndIDQueryCondition, accountID, groupID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewGroupNotFoundError(groupID) @@ -1900,7 +1897,7 @@ func (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStren } // CreateGroup creates a group in the store. -func (s *SqlStore) CreateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error { +func (s *SqlStore) CreateGroup(ctx context.Context, group *types.Group) error { if group == nil { return status.Errorf(status.InvalidArgument, "group is nil") } @@ -1914,7 +1911,7 @@ func (s *SqlStore) CreateGroup(ctx context.Context, lockStrength LockingStrength } // UpdateGroup updates a group in the store. -func (s *SqlStore) UpdateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error { +func (s *SqlStore) UpdateGroup(ctx context.Context, group *types.Group) error { if group == nil { return status.Errorf(status.InvalidArgument, "group is nil") } @@ -1928,9 +1925,8 @@ func (s *SqlStore) UpdateGroup(ctx context.Context, lockStrength LockingStrength } // DeleteGroup deletes a group from the database. -func (s *SqlStore) DeleteGroup(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Select(clause.Associations). +func (s *SqlStore) DeleteGroup(ctx context.Context, accountID, groupID string) error { + result := s.db.Select(clause.Associations). Delete(&types.Group{}, accountAndIDQueryCondition, accountID, groupID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to delete group from store: %s", result.Error) @@ -1945,9 +1941,8 @@ func (s *SqlStore) DeleteGroup(ctx context.Context, lockStrength LockingStrength } // DeleteGroups deletes groups from the database. -func (s *SqlStore) DeleteGroups(ctx context.Context, strength LockingStrength, accountID string, groupIDs []string) error { - result := s.db.Clauses(clause.Locking{Strength: string(strength)}). - Select(clause.Associations). +func (s *SqlStore) DeleteGroups(ctx context.Context, accountID string, groupIDs []string) error { + result := s.db.Select(clause.Associations). Delete(&types.Group{}, accountAndIDsQueryCondition, accountID, groupIDs) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete groups from store: %v", result.Error) @@ -1985,7 +1980,7 @@ func (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStreng var policy *types.Policy result := tx.Preload(clause.Associations). - First(&policy, accountAndIDQueryCondition, accountID, policyID) + Take(&policy, accountAndIDQueryCondition, accountID, policyID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewPolicyNotFoundError(policyID) @@ -1997,8 +1992,8 @@ func (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStreng return policy, nil } -func (s *SqlStore) CreatePolicy(ctx context.Context, lockStrength LockingStrength, policy *types.Policy) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Create(policy) +func (s *SqlStore) CreatePolicy(ctx context.Context, policy *types.Policy) error { + result := s.db.Create(policy) if result.Error != nil { log.WithContext(ctx).Errorf("failed to create policy in store: %s", result.Error) return status.Errorf(status.Internal, "failed to create policy in store") @@ -2008,9 +2003,8 @@ func (s *SqlStore) CreatePolicy(ctx context.Context, lockStrength LockingStrengt } // SavePolicy saves a policy to the database. -func (s *SqlStore) SavePolicy(ctx context.Context, lockStrength LockingStrength, policy *types.Policy) error { - result := s.db.Session(&gorm.Session{FullSaveAssociations: true}). - Clauses(clause.Locking{Strength: string(lockStrength)}).Save(policy) +func (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error { + result := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(policy) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to save policy to the store: %s", err) return status.Errorf(status.Internal, "failed to save policy to store") @@ -2018,13 +2012,13 @@ func (s *SqlStore) SavePolicy(ctx context.Context, lockStrength LockingStrength, return nil } -func (s *SqlStore) DeletePolicy(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) error { +func (s *SqlStore) DeletePolicy(ctx context.Context, accountID, policyID string) error { return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("policy_id = ?", policyID).Delete(&types.PolicyRule{}).Error; err != nil { return fmt.Errorf("delete policy rules: %w", err) } - result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). + result := tx. Where(accountAndIDQueryCondition, accountID, policyID). Delete(&types.Policy{}) @@ -2067,7 +2061,7 @@ func (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength Lockin var postureCheck *posture.Checks result := tx. - First(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID) + Take(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPostureChecksNotFoundError(postureChecksID) @@ -2102,8 +2096,8 @@ func (s *SqlStore) GetPostureChecksByIDs(ctx context.Context, lockStrength Locki } // SavePostureChecks saves a posture checks to the database. -func (s *SqlStore) SavePostureChecks(ctx context.Context, lockStrength LockingStrength, postureCheck *posture.Checks) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(postureCheck) +func (s *SqlStore) SavePostureChecks(ctx context.Context, postureCheck *posture.Checks) error { + result := s.db.Save(postureCheck) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save posture checks to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save posture checks to store") @@ -2113,9 +2107,8 @@ func (s *SqlStore) SavePostureChecks(ctx context.Context, lockStrength LockingSt } // DeletePostureChecks deletes a posture checks from the database. -func (s *SqlStore) DeletePostureChecks(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&posture.Checks{}, accountAndIDQueryCondition, accountID, postureChecksID) +func (s *SqlStore) DeletePostureChecks(ctx context.Context, accountID, postureChecksID string) error { + result := s.db.Delete(&posture.Checks{}, accountAndIDQueryCondition, accountID, postureChecksID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete posture checks from store: %s", result.Error) return status.Errorf(status.Internal, "failed to delete posture checks from store") @@ -2130,9 +2123,13 @@ func (s *SqlStore) DeletePostureChecks(ctx context.Context, lockStrength Locking // GetAccountRoutes retrieves network routes for an account. func (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var routes []*route.Route - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Find(&routes, accountIDCondition, accountID) + result := tx.Find(&routes, accountIDCondition, accountID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get routes from the store: %s", err) return nil, status.Errorf(status.Internal, "failed to get routes from store") @@ -2143,9 +2140,13 @@ func (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStr // GetRouteByID retrieves a route by its ID and account ID. func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID string, routeID string) (*route.Route, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + var route *route.Route - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - First(&route, accountAndIDQueryCondition, accountID, routeID) + result := tx.Take(&route, accountAndIDQueryCondition, accountID, routeID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewRouteNotFoundError(routeID) @@ -2158,8 +2159,8 @@ func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrengt } // SaveRoute saves a route to the database. -func (s *SqlStore) SaveRoute(ctx context.Context, lockStrength LockingStrength, route *route.Route) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(route) +func (s *SqlStore) SaveRoute(ctx context.Context, route *route.Route) error { + result := s.db.Save(route) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to save route to the store: %s", err) return status.Errorf(status.Internal, "failed to save route to store") @@ -2169,9 +2170,8 @@ func (s *SqlStore) SaveRoute(ctx context.Context, lockStrength LockingStrength, } // DeleteRoute deletes a route from the database. -func (s *SqlStore) DeleteRoute(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&route.Route{}, accountAndIDQueryCondition, accountID, routeID) +func (s *SqlStore) DeleteRoute(ctx context.Context, accountID, routeID string) error { + result := s.db.Delete(&route.Route{}, accountAndIDQueryCondition, accountID, routeID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to delete route from the store: %s", err) return status.Errorf(status.Internal, "failed to delete route from store") @@ -2210,8 +2210,7 @@ func (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStre } var setupKey *types.SetupKey - result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). - First(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID) + result := tx.Take(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewSetupKeyNotFoundError(setupKeyID) @@ -2224,8 +2223,8 @@ func (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStre } // SaveSetupKey saves a setup key to the database. -func (s *SqlStore) SaveSetupKey(ctx context.Context, lockStrength LockingStrength, setupKey *types.SetupKey) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(setupKey) +func (s *SqlStore) SaveSetupKey(ctx context.Context, setupKey *types.SetupKey) error { + result := s.db.Save(setupKey) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save setup key to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save setup key to store") @@ -2235,8 +2234,8 @@ func (s *SqlStore) SaveSetupKey(ctx context.Context, lockStrength LockingStrengt } // DeleteSetupKey deletes a setup key from the database. -func (s *SqlStore) DeleteSetupKey(ctx context.Context, lockStrength LockingStrength, accountID, keyID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Delete(&types.SetupKey{}, accountAndIDQueryCondition, accountID, keyID) +func (s *SqlStore) DeleteSetupKey(ctx context.Context, accountID, keyID string) error { + result := s.db.Delete(&types.SetupKey{}, accountAndIDQueryCondition, accountID, keyID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete setup key from store: %s", result.Error) return status.Errorf(status.Internal, "failed to delete setup key from store") @@ -2275,7 +2274,7 @@ func (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength Lock var nsGroup *nbdns.NameServerGroup result := tx. - First(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID) + Take(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewNameServerGroupNotFoundError(nsGroupID) @@ -2288,8 +2287,8 @@ func (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength Lock } // SaveNameServerGroup saves a name server group to the database. -func (s *SqlStore) SaveNameServerGroup(ctx context.Context, lockStrength LockingStrength, nameServerGroup *nbdns.NameServerGroup) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(nameServerGroup) +func (s *SqlStore) SaveNameServerGroup(ctx context.Context, nameServerGroup *nbdns.NameServerGroup) error { + result := s.db.Save(nameServerGroup) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to save name server group to the store: %s", err) return status.Errorf(status.Internal, "failed to save name server group to store") @@ -2298,8 +2297,8 @@ func (s *SqlStore) SaveNameServerGroup(ctx context.Context, lockStrength Locking } // DeleteNameServerGroup deletes a name server group from the database. -func (s *SqlStore) DeleteNameServerGroup(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Delete(&nbdns.NameServerGroup{}, accountAndIDQueryCondition, accountID, nsGroupID) +func (s *SqlStore) DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID string) error { + result := s.db.Delete(&nbdns.NameServerGroup{}, accountAndIDQueryCondition, accountID, nsGroupID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to delete name server group from the store: %s", err) return status.Errorf(status.Internal, "failed to delete name server group from store") @@ -2313,8 +2312,8 @@ func (s *SqlStore) DeleteNameServerGroup(ctx context.Context, lockStrength Locki } // SaveDNSSettings saves the DNS settings to the store. -func (s *SqlStore) SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). +func (s *SqlStore) SaveDNSSettings(ctx context.Context, accountID string, settings *types.DNSSettings) error { + result := s.db.Model(&types.Account{}). Where(idQueryCondition, accountID).Updates(&types.AccountDNSSettings{DNSSettings: *settings}) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save dns settings to store: %v", result.Error) @@ -2329,8 +2328,8 @@ func (s *SqlStore) SaveDNSSettings(ctx context.Context, lockStrength LockingStre } // SaveAccountSettings stores the account settings in DB. -func (s *SqlStore) SaveAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.Settings) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). +func (s *SqlStore) SaveAccountSettings(ctx context.Context, accountID string, settings *types.Settings) error { + result := s.db.Model(&types.Account{}). Select("*").Where(idQueryCondition, accountID).Updates(&types.AccountSettings{Settings: settings}) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save account settings to store: %v", result.Error) @@ -2367,8 +2366,7 @@ func (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStren } var network *networkTypes.Network - result := tx. - First(&network, accountAndIDQueryCondition, accountID, networkID) + result := tx.Take(&network, accountAndIDQueryCondition, accountID, networkID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewNetworkNotFoundError(networkID) @@ -2381,8 +2379,8 @@ func (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStren return network, nil } -func (s *SqlStore) SaveNetwork(ctx context.Context, lockStrength LockingStrength, network *networkTypes.Network) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(network) +func (s *SqlStore) SaveNetwork(ctx context.Context, network *networkTypes.Network) error { + result := s.db.Save(network) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save network to store: %v", result.Error) return status.Errorf(status.Internal, "failed to save network to store") @@ -2391,9 +2389,8 @@ func (s *SqlStore) SaveNetwork(ctx context.Context, lockStrength LockingStrength return nil } -func (s *SqlStore) DeleteNetwork(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&networkTypes.Network{}, accountAndIDQueryCondition, accountID, networkID) +func (s *SqlStore) DeleteNetwork(ctx context.Context, accountID, networkID string) error { + result := s.db.Delete(&networkTypes.Network{}, accountAndIDQueryCondition, accountID, networkID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete network from store: %v", result.Error) return status.Errorf(status.Internal, "failed to delete network from store") @@ -2448,7 +2445,7 @@ func (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength Lockin var netRouter *routerTypes.NetworkRouter result := tx. - First(&netRouter, accountAndIDQueryCondition, accountID, routerID) + Take(&netRouter, accountAndIDQueryCondition, accountID, routerID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewNetworkRouterNotFoundError(routerID) @@ -2460,8 +2457,8 @@ func (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength Lockin return netRouter, nil } -func (s *SqlStore) SaveNetworkRouter(ctx context.Context, lockStrength LockingStrength, router *routerTypes.NetworkRouter) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(router) +func (s *SqlStore) SaveNetworkRouter(ctx context.Context, router *routerTypes.NetworkRouter) error { + result := s.db.Save(router) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save network router to store: %v", result.Error) return status.Errorf(status.Internal, "failed to save network router to store") @@ -2470,9 +2467,8 @@ func (s *SqlStore) SaveNetworkRouter(ctx context.Context, lockStrength LockingSt return nil } -func (s *SqlStore) DeleteNetworkRouter(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&routerTypes.NetworkRouter{}, accountAndIDQueryCondition, accountID, routerID) +func (s *SqlStore) DeleteNetworkRouter(ctx context.Context, accountID, routerID string) error { + result := s.db.Delete(&routerTypes.NetworkRouter{}, accountAndIDQueryCondition, accountID, routerID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete network router from store: %v", result.Error) return status.Errorf(status.Internal, "failed to delete network router from store") @@ -2527,7 +2523,7 @@ func (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength Lock var netResources *resourceTypes.NetworkResource result := tx. - First(&netResources, accountAndIDQueryCondition, accountID, resourceID) + Take(&netResources, accountAndIDQueryCondition, accountID, resourceID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewNetworkResourceNotFoundError(resourceID) @@ -2547,7 +2543,7 @@ func (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength Lo var netResources *resourceTypes.NetworkResource result := tx. - First(&netResources, "account_id = ? AND name = ?", accountID, resourceName) + Take(&netResources, "account_id = ? AND name = ?", accountID, resourceName) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewNetworkResourceNotFoundError(resourceName) @@ -2559,8 +2555,8 @@ func (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength Lo return netResources, nil } -func (s *SqlStore) SaveNetworkResource(ctx context.Context, lockStrength LockingStrength, resource *resourceTypes.NetworkResource) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(resource) +func (s *SqlStore) SaveNetworkResource(ctx context.Context, resource *resourceTypes.NetworkResource) error { + result := s.db.Save(resource) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save network resource to store: %v", result.Error) return status.Errorf(status.Internal, "failed to save network resource to store") @@ -2569,9 +2565,8 @@ func (s *SqlStore) SaveNetworkResource(ctx context.Context, lockStrength Locking return nil } -func (s *SqlStore) DeleteNetworkResource(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&resourceTypes.NetworkResource{}, accountAndIDQueryCondition, accountID, resourceID) +func (s *SqlStore) DeleteNetworkResource(ctx context.Context, accountID, resourceID string) error { + result := s.db.Delete(&resourceTypes.NetworkResource{}, accountAndIDQueryCondition, accountID, resourceID) if result.Error != nil { log.WithContext(ctx).Errorf("failed to delete network resource from store: %v", result.Error) return status.Errorf(status.Internal, "failed to delete network resource from store") @@ -2592,7 +2587,7 @@ func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength Locking } var pat types.PersonalAccessToken - result := tx.First(&pat, "hashed_token = ?", hashedToken) + result := tx.Take(&pat, "hashed_token = ?", hashedToken) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPATNotFoundError(hashedToken) @@ -2613,7 +2608,7 @@ func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, var pat types.PersonalAccessToken result := tx. - First(&pat, "id = ? AND user_id = ?", patID, userID) + Take(&pat, "id = ? AND user_id = ?", patID, userID) if err := result.Error; err != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPATNotFoundError(patID) @@ -2643,13 +2638,13 @@ func (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength } // MarkPATUsed marks a personal access token as used. -func (s *SqlStore) MarkPATUsed(ctx context.Context, lockStrength LockingStrength, patID string) error { +func (s *SqlStore) MarkPATUsed(ctx context.Context, patID string) error { patCopy := types.PersonalAccessToken{ LastUsed: util.ToPtr(time.Now().UTC()), } fieldsToUpdate := []string{"last_used"} - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Select(fieldsToUpdate). + result := s.db.Select(fieldsToUpdate). Where(idQueryCondition, patID).Updates(&patCopy) if result.Error != nil { log.WithContext(ctx).Errorf("failed to mark pat as used: %s", result.Error) @@ -2664,8 +2659,8 @@ func (s *SqlStore) MarkPATUsed(ctx context.Context, lockStrength LockingStrength } // SavePAT saves a personal access token to the database. -func (s *SqlStore) SavePAT(ctx context.Context, lockStrength LockingStrength, pat *types.PersonalAccessToken) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(pat) +func (s *SqlStore) SavePAT(ctx context.Context, pat *types.PersonalAccessToken) error { + result := s.db.Save(pat) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to save pat to the store: %s", err) return status.Errorf(status.Internal, "failed to save pat to store") @@ -2675,9 +2670,8 @@ func (s *SqlStore) SavePAT(ctx context.Context, lockStrength LockingStrength, pa } // DeletePAT deletes a personal access token from the database. -func (s *SqlStore) DeletePAT(ctx context.Context, lockStrength LockingStrength, userID, patID string) error { - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Delete(&types.PersonalAccessToken{}, "user_id = ? AND id = ?", userID, patID) +func (s *SqlStore) DeletePAT(ctx context.Context, userID, patID string) error { + result := s.db.Delete(&types.PersonalAccessToken{}, "user_id = ? AND id = ?", userID, patID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to delete pat from the store: %s", err) return status.Errorf(status.Internal, "failed to delete pat from store") @@ -2700,7 +2694,7 @@ func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength var peer nbpeer.Peer result := tx. - First(&peer, "account_id = ? AND ip = ?", accountID, jsonValue) + Take(&peer, "account_id = ? AND ip = ?", accountID, jsonValue) if result.Error != nil { // no logging here return nil, status.Errorf(status.Internal, "failed to get peer from store") diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index fdce9d735..935b0a595 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -27,11 +27,11 @@ import ( networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" nbroute "github.com/netbirdio/netbird/route" route2 "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/status" ) func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { @@ -401,11 +401,11 @@ func TestSqlite_DeleteAccount(t *testing.T) { } for _, network := range account.Networks { - routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) + routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthNone, account.Id, network.ID) require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network routers") require.Len(t, routers, 0, "expecting no network routers to be found after DeleteAccount") - resources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) + resources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthNone, account.Id, network.ID) require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network resources") require.Len(t, resources, 0, "expecting no network resources to be found after DeleteAccount") } @@ -459,7 +459,7 @@ func TestSqlStore_SavePeer(t *testing.T) { CreatedAt: time.Now().UTC(), } ctx := context.Background() - err = store.SavePeer(ctx, LockingStrengthUpdate, account.Id, peer) + err = store.SavePeer(ctx, account.Id, peer) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) @@ -475,7 +475,7 @@ func TestSqlStore_SavePeer(t *testing.T) { updatedPeer.Status.Connected = false updatedPeer.Meta.Hostname = "updatedpeer" - err = store.SavePeer(ctx, LockingStrengthUpdate, account.Id, updatedPeer) + err = store.SavePeer(ctx, account.Id, updatedPeer) require.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) @@ -499,7 +499,7 @@ func TestSqlStore_SavePeerStatus(t *testing.T) { // save status of non-existing peer newStatus := nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()} - err = store.SavePeerStatus(context.Background(), LockingStrengthUpdate, account.Id, "non-existing-peer", newStatus) + err = store.SavePeerStatus(context.Background(), account.Id, "non-existing-peer", newStatus) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) @@ -518,7 +518,7 @@ func TestSqlStore_SavePeerStatus(t *testing.T) { err = store.SaveAccount(context.Background(), account) require.NoError(t, err) - err = store.SavePeerStatus(context.Background(), LockingStrengthUpdate, account.Id, "testpeer", newStatus) + err = store.SavePeerStatus(context.Background(), account.Id, "testpeer", newStatus) require.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) @@ -532,7 +532,7 @@ func TestSqlStore_SavePeerStatus(t *testing.T) { newStatus.Connected = true - err = store.SavePeerStatus(context.Background(), LockingStrengthUpdate, account.Id, "testpeer", newStatus) + err = store.SavePeerStatus(context.Background(), account.Id, "testpeer", newStatus) require.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) @@ -566,7 +566,7 @@ func TestSqlStore_SavePeerLocation(t *testing.T) { Meta: nbpeer.PeerSystemMeta{}, } // error is expected as peer is not in store yet - err = store.SavePeerLocation(context.Background(), LockingStrengthUpdate, account.Id, peer) + err = store.SavePeerLocation(context.Background(), account.Id, peer) assert.Error(t, err) account.Peers[peer.ID] = peer @@ -578,7 +578,7 @@ func TestSqlStore_SavePeerLocation(t *testing.T) { peer.Location.CityName = "Berlin" peer.Location.GeoNameID = 2950159 - err = store.SavePeerLocation(context.Background(), LockingStrengthUpdate, account.Id, account.Peers[peer.ID]) + err = store.SavePeerLocation(context.Background(), account.Id, account.Peers[peer.ID]) assert.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) @@ -588,7 +588,7 @@ func TestSqlStore_SavePeerLocation(t *testing.T) { assert.Equal(t, peer.Location, actual) peer.ID = "non-existing-peer" - err = store.SavePeerLocation(context.Background(), LockingStrengthUpdate, account.Id, peer) + err = store.SavePeerLocation(context.Background(), account.Id, peer) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) @@ -961,7 +961,7 @@ func TestSqlite_GetTakenIPs(t *testing.T) { _, err = store.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) - takenIPs, err := store.GetTakenIPs(context.Background(), LockingStrengthShare, existingAccountID) + takenIPs, err := store.GetTakenIPs(context.Background(), LockingStrengthNone, existingAccountID) require.NoError(t, err) assert.Equal(t, []net.IP{}, takenIPs) @@ -971,10 +971,10 @@ func TestSqlite_GetTakenIPs(t *testing.T) { DNSLabel: "peer1", IP: net.IP{1, 1, 1, 1}, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + err = store.AddPeerToAccount(context.Background(), peer1) require.NoError(t, err) - takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthShare, existingAccountID) + takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthNone, existingAccountID) require.NoError(t, err) ip1 := net.IP{1, 1, 1, 1}.To16() assert.Equal(t, []net.IP{ip1}, takenIPs) @@ -985,10 +985,10 @@ func TestSqlite_GetTakenIPs(t *testing.T) { DNSLabel: "peer1-1", IP: net.IP{2, 2, 2, 2}, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + err = store.AddPeerToAccount(context.Background(), peer2) require.NoError(t, err) - takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthShare, existingAccountID) + takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthNone, existingAccountID) require.NoError(t, err) ip2 := net.IP{2, 2, 2, 2}.To16() assert.Equal(t, []net.IP{ip1, ip2}, takenIPs) @@ -1002,7 +1002,7 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) { _, err := store.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) - labels, err := store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID, peerHostname) + labels, err := store.GetPeerLabelsInAccount(context.Background(), LockingStrengthNone, existingAccountID, peerHostname) require.NoError(t, err) assert.Equal(t, []string{}, labels) @@ -1012,10 +1012,10 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) { DNSLabel: "peer1", IP: net.IP{1, 1, 1, 1}, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + err = store.AddPeerToAccount(context.Background(), peer1) require.NoError(t, err) - labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID, peerHostname) + labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthNone, existingAccountID, peerHostname) require.NoError(t, err) assert.Equal(t, []string{"peer1"}, labels) @@ -1025,10 +1025,10 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) { DNSLabel: "peer1-1", IP: net.IP{2, 2, 2, 2}, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + err = store.AddPeerToAccount(context.Background(), peer2) require.NoError(t, err) - labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID, peerHostname) + labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthNone, existingAccountID, peerHostname) require.NoError(t, err) expected := []string{"peer1", "peer1-1"} @@ -1050,7 +1050,7 @@ func Test_AddPeerWithSameDnsLabel(t *testing.T) { AccountID: existingAccountID, DNSLabel: "peer1.domain.test", } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + err = store.AddPeerToAccount(context.Background(), peer1) require.NoError(t, err) peer2 := &nbpeer.Peer{ @@ -1058,7 +1058,7 @@ func Test_AddPeerWithSameDnsLabel(t *testing.T) { AccountID: existingAccountID, DNSLabel: "peer1.domain.test", } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + err = store.AddPeerToAccount(context.Background(), peer2) require.Error(t, err) }) } @@ -1075,7 +1075,7 @@ func Test_AddPeerWithSameIP(t *testing.T) { AccountID: existingAccountID, IP: net.IP{1, 1, 1, 1}, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) + err = store.AddPeerToAccount(context.Background(), peer1) require.NoError(t, err) peer2 := &nbpeer.Peer{ @@ -1083,7 +1083,7 @@ func Test_AddPeerWithSameIP(t *testing.T) { AccountID: existingAccountID, IP: net.IP{1, 1, 1, 1}, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) + err = store.AddPeerToAccount(context.Background(), peer2) require.Error(t, err) }) } @@ -1101,7 +1101,7 @@ func TestSqlite_GetAccountNetwork(t *testing.T) { _, err = store.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) - network, err := store.GetAccountNetwork(context.Background(), LockingStrengthShare, existingAccountID) + network, err := store.GetAccountNetwork(context.Background(), LockingStrengthNone, existingAccountID) require.NoError(t, err) ip := net.IP{100, 64, 0, 0}.To16() assert.Equal(t, ip, network.Net.IP) @@ -1128,7 +1128,7 @@ func TestSqlite_GetSetupKeyBySecret(t *testing.T) { _, err = store.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) - setupKey, err := store.GetSetupKeyBySecret(context.Background(), LockingStrengthShare, encodedHashedKey) + setupKey, err := store.GetSetupKeyBySecret(context.Background(), LockingStrengthNone, encodedHashedKey) require.NoError(t, err) assert.Equal(t, encodedHashedKey, setupKey.Key) assert.Equal(t, types.HiddenKey(plainKey, 4), setupKey.KeySecret) @@ -1153,21 +1153,21 @@ func TestSqlite_incrementSetupKeyUsage(t *testing.T) { _, err = store.GetAccount(context.Background(), existingAccountID) require.NoError(t, err) - setupKey, err := store.GetSetupKeyBySecret(context.Background(), LockingStrengthShare, encodedHashedKey) + setupKey, err := store.GetSetupKeyBySecret(context.Background(), LockingStrengthNone, encodedHashedKey) require.NoError(t, err) assert.Equal(t, 0, setupKey.UsedTimes) err = store.IncrementSetupKeyUsage(context.Background(), setupKey.Id) require.NoError(t, err) - setupKey, err = store.GetSetupKeyBySecret(context.Background(), LockingStrengthShare, encodedHashedKey) + setupKey, err = store.GetSetupKeyBySecret(context.Background(), LockingStrengthNone, encodedHashedKey) require.NoError(t, err) assert.Equal(t, 1, setupKey.UsedTimes) err = store.IncrementSetupKeyUsage(context.Background(), setupKey.Id) require.NoError(t, err) - setupKey, err = store.GetSetupKeyBySecret(context.Background(), LockingStrengthShare, encodedHashedKey) + setupKey, err = store.GetSetupKeyBySecret(context.Background(), LockingStrengthNone, encodedHashedKey) require.NoError(t, err) assert.Equal(t, 2, setupKey.UsedTimes) } @@ -1188,7 +1188,7 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) { Peers: nil, } err = store.ExecuteInTransaction(context.Background(), func(transaction Store) error { - err := transaction.CreateGroup(context.Background(), LockingStrengthUpdate, group) + err := transaction.CreateGroup(context.Background(), group) if err != nil { t.Fatal("failed to save group") return err @@ -1213,7 +1213,7 @@ func TestSqlStore_GetAccountUsers(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" account, err := store.GetAccount(context.Background(), accountID) require.NoError(t, err) - users, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + users, err := store.GetAccountUsers(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Len(t, users, len(account.Users)) } @@ -1272,7 +1272,7 @@ func TestSqlite_GetGroupByName(t *testing.T) { } accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - group, err := store.GetGroupByName(context.Background(), LockingStrengthShare, accountID, "All") + group, err := store.GetGroupByName(context.Background(), LockingStrengthNone, accountID, "All") require.NoError(t, err) require.True(t, group.IsGroupAll()) } @@ -1286,10 +1286,10 @@ func Test_DeleteSetupKeySuccessfully(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" setupKeyID := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" - err = store.DeleteSetupKey(context.Background(), LockingStrengthUpdate, accountID, setupKeyID) + err = store.DeleteSetupKey(context.Background(), accountID, setupKeyID) require.NoError(t, err) - _, err = store.GetSetupKeyByID(context.Background(), LockingStrengthShare, setupKeyID, accountID) + _, err = store.GetSetupKeyByID(context.Background(), LockingStrengthNone, setupKeyID, accountID) require.Error(t, err) } @@ -1302,7 +1302,7 @@ func Test_DeleteSetupKeyFailsForNonExistingKey(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" nonExistingKeyID := "non-existing-key-id" - err = store.DeleteSetupKey(context.Background(), LockingStrengthUpdate, accountID, nonExistingKeyID) + err = store.DeleteSetupKey(context.Background(), accountID, nonExistingKeyID) require.Error(t, err) } @@ -1342,7 +1342,7 @@ func TestSqlStore_GetGroupsByIDs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - groups, err := store.GetGroupsByIDs(context.Background(), LockingStrengthShare, accountID, tt.groupIDs) + groups, err := store.GetGroupsByIDs(context.Background(), LockingStrengthNone, accountID, tt.groupIDs) require.NoError(t, err) require.Len(t, groups, tt.expectedCount) }) @@ -1365,10 +1365,10 @@ func TestSqlStore_CreateGroup(t *testing.T) { Resources: []types.Resource{}, GroupPeers: []types.GroupPeer{}, } - err = store.CreateGroup(context.Background(), LockingStrengthUpdate, group) + err = store.CreateGroup(context.Background(), group) require.NoError(t, err) - savedGroup, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, "group-id") + savedGroup, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, "group-id") require.NoError(t, err) require.Equal(t, savedGroup, group) } @@ -1398,14 +1398,14 @@ func TestSqlStore_CreateUpdateGroups(t *testing.T) { GroupPeers: []types.GroupPeer{}, }, } - err = store.CreateGroups(context.Background(), LockingStrengthUpdate, accountID, groups) + err = store.CreateGroups(context.Background(), accountID, groups) require.NoError(t, err) groups[1].Peers = []string{} - err = store.UpdateGroups(context.Background(), LockingStrengthUpdate, accountID, groups) + err = store.UpdateGroups(context.Background(), accountID, groups) require.NoError(t, err) - group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID) + group, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groups[1].ID) require.NoError(t, err) require.Equal(t, groups[1], group) } @@ -1441,7 +1441,7 @@ func TestSqlStore_DeleteGroup(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := store.DeleteGroup(context.Background(), LockingStrengthUpdate, accountID, tt.groupID) + err := store.DeleteGroup(context.Background(), accountID, tt.groupID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -1450,7 +1450,7 @@ func TestSqlStore_DeleteGroup(t *testing.T) { } else { require.NoError(t, err) - group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, tt.groupID) + group, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, tt.groupID) require.Error(t, err) require.Nil(t, group) } @@ -1489,14 +1489,14 @@ func TestSqlStore_DeleteGroups(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := store.DeleteGroups(context.Background(), LockingStrengthUpdate, accountID, tt.groupIDs) + err := store.DeleteGroups(context.Background(), accountID, tt.groupIDs) if tt.expectError { require.Error(t, err) } else { require.NoError(t, err) for _, groupID := range tt.groupIDs { - group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.Error(t, err) require.Nil(t, group) } @@ -1535,7 +1535,7 @@ func TestSqlStore_GetPeerByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peer, err := store.GetPeerByID(context.Background(), LockingStrengthShare, accountID, tt.peerID) + peer, err := store.GetPeerByID(context.Background(), LockingStrengthNone, accountID, tt.peerID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -1586,7 +1586,7 @@ func TestSqlStore_GetPeersByIDs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peers, err := store.GetPeersByIDs(context.Background(), LockingStrengthShare, accountID, tt.peerIDs) + peers, err := store.GetPeersByIDs(context.Background(), LockingStrengthNone, accountID, tt.peerIDs) require.NoError(t, err) require.Len(t, peers, tt.expectedCount) }) @@ -1623,7 +1623,7 @@ func TestSqlStore_GetPostureChecksByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - postureChecks, err := store.GetPostureChecksByID(context.Background(), LockingStrengthShare, accountID, tt.postureChecksID) + postureChecks, err := store.GetPostureChecksByID(context.Background(), LockingStrengthNone, accountID, tt.postureChecksID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -1675,7 +1675,7 @@ func TestSqlStore_GetPostureChecksByIDs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - groups, err := store.GetPostureChecksByIDs(context.Background(), LockingStrengthShare, accountID, tt.postureCheckIDs) + groups, err := store.GetPostureChecksByIDs(context.Background(), LockingStrengthNone, accountID, tt.postureCheckIDs) require.NoError(t, err) require.Len(t, groups, tt.expectedCount) }) @@ -1715,10 +1715,10 @@ func TestSqlStore_SavePostureChecks(t *testing.T) { }, }, } - err = store.SavePostureChecks(context.Background(), LockingStrengthUpdate, postureChecks) + err = store.SavePostureChecks(context.Background(), postureChecks) require.NoError(t, err) - savePostureChecks, err := store.GetPostureChecksByID(context.Background(), LockingStrengthShare, accountID, "posture-checks-id") + savePostureChecks, err := store.GetPostureChecksByID(context.Background(), LockingStrengthNone, accountID, "posture-checks-id") require.NoError(t, err) require.Equal(t, savePostureChecks, postureChecks) } @@ -1754,7 +1754,7 @@ func TestSqlStore_DeletePostureChecks(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err = store.DeletePostureChecks(context.Background(), LockingStrengthUpdate, accountID, tt.postureChecksID) + err = store.DeletePostureChecks(context.Background(), accountID, tt.postureChecksID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -1762,7 +1762,7 @@ func TestSqlStore_DeletePostureChecks(t *testing.T) { require.Equal(t, sErr.Type(), status.NotFound) } else { require.NoError(t, err) - group, err := store.GetPostureChecksByID(context.Background(), LockingStrengthShare, accountID, tt.postureChecksID) + group, err := store.GetPostureChecksByID(context.Background(), LockingStrengthNone, accountID, tt.postureChecksID) require.Error(t, err) require.Nil(t, group) } @@ -1800,7 +1800,7 @@ func TestSqlStore_GetPolicyByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - policy, err := store.GetPolicyByID(context.Background(), LockingStrengthShare, accountID, tt.policyID) + policy, err := store.GetPolicyByID(context.Background(), LockingStrengthNone, accountID, tt.policyID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -1837,10 +1837,10 @@ func TestSqlStore_CreatePolicy(t *testing.T) { }, }, } - err = store.CreatePolicy(context.Background(), LockingStrengthUpdate, policy) + err = store.CreatePolicy(context.Background(), policy) require.NoError(t, err) - savePolicy, err := store.GetPolicyByID(context.Background(), LockingStrengthShare, accountID, policy.ID) + savePolicy, err := store.GetPolicyByID(context.Background(), LockingStrengthNone, accountID, policy.ID) require.NoError(t, err) require.Equal(t, savePolicy, policy) @@ -1854,17 +1854,17 @@ func TestSqlStore_SavePolicy(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" policyID := "cs1tnh0hhcjnqoiuebf0" - policy, err := store.GetPolicyByID(context.Background(), LockingStrengthShare, accountID, policyID) + policy, err := store.GetPolicyByID(context.Background(), LockingStrengthNone, accountID, policyID) require.NoError(t, err) policy.Enabled = false policy.Description = "policy" policy.Rules[0].Sources = []string{"group"} policy.Rules[0].Ports = []string{"80", "443"} - err = store.SavePolicy(context.Background(), LockingStrengthUpdate, policy) + err = store.SavePolicy(context.Background(), policy) require.NoError(t, err) - savePolicy, err := store.GetPolicyByID(context.Background(), LockingStrengthShare, accountID, policy.ID) + savePolicy, err := store.GetPolicyByID(context.Background(), LockingStrengthNone, accountID, policy.ID) require.NoError(t, err) require.Equal(t, savePolicy, policy) } @@ -1877,10 +1877,10 @@ func TestSqlStore_DeletePolicy(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" policyID := "cs1tnh0hhcjnqoiuebf0" - err = store.DeletePolicy(context.Background(), LockingStrengthShare, accountID, policyID) + err = store.DeletePolicy(context.Background(), accountID, policyID) require.NoError(t, err) - policy, err := store.GetPolicyByID(context.Background(), LockingStrengthShare, accountID, policyID) + policy, err := store.GetPolicyByID(context.Background(), LockingStrengthNone, accountID, policyID) require.Error(t, err) require.Nil(t, policy) } @@ -1914,7 +1914,7 @@ func TestSqlStore_GetDNSSettings(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dnsSettings, err := store.GetAccountDNSSettings(context.Background(), LockingStrengthShare, tt.accountID) + dnsSettings, err := store.GetAccountDNSSettings(context.Background(), LockingStrengthNone, tt.accountID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -1936,14 +1936,14 @@ func TestSqlStore_SaveDNSSettings(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - dnsSettings, err := store.GetAccountDNSSettings(context.Background(), LockingStrengthShare, accountID) + dnsSettings, err := store.GetAccountDNSSettings(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) dnsSettings.DisabledManagementGroups = []string{"groupA", "groupB"} - err = store.SaveDNSSettings(context.Background(), LockingStrengthUpdate, accountID, dnsSettings) + err = store.SaveDNSSettings(context.Background(), accountID, dnsSettings) require.NoError(t, err) - saveDNSSettings, err := store.GetAccountDNSSettings(context.Background(), LockingStrengthShare, accountID) + saveDNSSettings, err := store.GetAccountDNSSettings(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Equal(t, saveDNSSettings, dnsSettings) } @@ -1977,7 +1977,7 @@ func TestSqlStore_GetAccountNameServerGroups(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peers, err := store.GetAccountNameServerGroups(context.Background(), LockingStrengthShare, tt.accountID) + peers, err := store.GetAccountNameServerGroups(context.Background(), LockingStrengthNone, tt.accountID) require.NoError(t, err) require.Len(t, peers, tt.expectedCount) }) @@ -2015,7 +2015,7 @@ func TestSqlStore_GetNameServerByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - nsGroup, err := store.GetNameServerGroupByID(context.Background(), LockingStrengthShare, accountID, tt.nsGroupID) + nsGroup, err := store.GetNameServerGroupByID(context.Background(), LockingStrengthNone, accountID, tt.nsGroupID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -2055,10 +2055,10 @@ func TestSqlStore_SaveNameServerGroup(t *testing.T) { SearchDomainsEnabled: false, } - err = store.SaveNameServerGroup(context.Background(), LockingStrengthUpdate, nsGroup) + err = store.SaveNameServerGroup(context.Background(), nsGroup) require.NoError(t, err) - saveNSGroup, err := store.GetNameServerGroupByID(context.Background(), LockingStrengthShare, accountID, nsGroup.ID) + saveNSGroup, err := store.GetNameServerGroupByID(context.Background(), LockingStrengthNone, accountID, nsGroup.ID) require.NoError(t, err) require.Equal(t, saveNSGroup, nsGroup) } @@ -2071,10 +2071,10 @@ func TestSqlStore_DeleteNameServerGroup(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" nsGroupID := "csqdelq7qv97ncu7d9t0" - err = store.DeleteNameServerGroup(context.Background(), LockingStrengthShare, accountID, nsGroupID) + err = store.DeleteNameServerGroup(context.Background(), accountID, nsGroupID) require.NoError(t, err) - nsGroup, err := store.GetNameServerGroupByID(context.Background(), LockingStrengthShare, accountID, nsGroupID) + nsGroup, err := store.GetNameServerGroupByID(context.Background(), LockingStrengthNone, accountID, nsGroupID) require.Error(t, err) require.Nil(t, nsGroup) } @@ -2154,7 +2154,7 @@ func TestSqlStore_GetAccountNetworks(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - networks, err := store.GetAccountNetworks(context.Background(), LockingStrengthShare, tt.accountID) + networks, err := store.GetAccountNetworks(context.Background(), LockingStrengthNone, tt.accountID) require.NoError(t, err) require.Len(t, networks, tt.expectedCount) }) @@ -2191,7 +2191,7 @@ func TestSqlStore_GetNetworkByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - network, err := store.GetNetworkByID(context.Background(), LockingStrengthShare, accountID, tt.networkID) + network, err := store.GetNetworkByID(context.Background(), LockingStrengthNone, accountID, tt.networkID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -2219,10 +2219,10 @@ func TestSqlStore_SaveNetwork(t *testing.T) { Name: "net", } - err = store.SaveNetwork(context.Background(), LockingStrengthUpdate, network) + err = store.SaveNetwork(context.Background(), network) require.NoError(t, err) - savedNet, err := store.GetNetworkByID(context.Background(), LockingStrengthShare, accountID, network.ID) + savedNet, err := store.GetNetworkByID(context.Background(), LockingStrengthNone, accountID, network.ID) require.NoError(t, err) require.Equal(t, network, savedNet) } @@ -2235,10 +2235,10 @@ func TestSqlStore_DeleteNetwork(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" networkID := "ct286bi7qv930dsrrug0" - err = store.DeleteNetwork(context.Background(), LockingStrengthUpdate, accountID, networkID) + err = store.DeleteNetwork(context.Background(), accountID, networkID) require.NoError(t, err) - network, err := store.GetNetworkByID(context.Background(), LockingStrengthShare, accountID, networkID) + network, err := store.GetNetworkByID(context.Background(), LockingStrengthNone, accountID, networkID) require.Error(t, err) sErr, ok := status.FromError(err) require.True(t, ok) @@ -2272,7 +2272,7 @@ func TestSqlStore_GetNetworkRoutersByNetID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthShare, accountID, tt.networkID) + routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthNone, accountID, tt.networkID) require.NoError(t, err) require.Len(t, routers, tt.expectedCount) }) @@ -2309,7 +2309,7 @@ func TestSqlStore_GetNetworkRouterByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - networkRouter, err := store.GetNetworkRouterByID(context.Background(), LockingStrengthShare, accountID, tt.networkRouterID) + networkRouter, err := store.GetNetworkRouterByID(context.Background(), LockingStrengthNone, accountID, tt.networkRouterID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -2336,10 +2336,10 @@ func TestSqlStore_SaveNetworkRouter(t *testing.T) { netRouter, err := routerTypes.NewNetworkRouter(accountID, networkID, "", []string{"net-router-grp"}, true, 0, true) require.NoError(t, err) - err = store.SaveNetworkRouter(context.Background(), LockingStrengthUpdate, netRouter) + err = store.SaveNetworkRouter(context.Background(), netRouter) require.NoError(t, err) - savedNetRouter, err := store.GetNetworkRouterByID(context.Background(), LockingStrengthShare, accountID, netRouter.ID) + savedNetRouter, err := store.GetNetworkRouterByID(context.Background(), LockingStrengthNone, accountID, netRouter.ID) require.NoError(t, err) require.Equal(t, netRouter, savedNetRouter) } @@ -2352,10 +2352,10 @@ func TestSqlStore_DeleteNetworkRouter(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" netRouterID := "ctc20ji7qv9ck2sebc80" - err = store.DeleteNetworkRouter(context.Background(), LockingStrengthUpdate, accountID, netRouterID) + err = store.DeleteNetworkRouter(context.Background(), accountID, netRouterID) require.NoError(t, err) - netRouter, err := store.GetNetworkByID(context.Background(), LockingStrengthShare, accountID, netRouterID) + netRouter, err := store.GetNetworkByID(context.Background(), LockingStrengthNone, accountID, netRouterID) require.Error(t, err) sErr, ok := status.FromError(err) require.True(t, ok) @@ -2389,7 +2389,7 @@ func TestSqlStore_GetNetworkResourcesByNetID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - netResources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthShare, accountID, tt.networkID) + netResources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthNone, accountID, tt.networkID) require.NoError(t, err) require.Len(t, netResources, tt.expectedCount) }) @@ -2426,7 +2426,7 @@ func TestSqlStore_GetNetworkResourceByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - netResource, err := store.GetNetworkResourceByID(context.Background(), LockingStrengthShare, accountID, tt.netResourceID) + netResource, err := store.GetNetworkResourceByID(context.Background(), LockingStrengthNone, accountID, tt.netResourceID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -2453,10 +2453,10 @@ func TestSqlStore_SaveNetworkResource(t *testing.T) { netResource, err := resourceTypes.NewNetworkResource(accountID, networkID, "resource-name", "", "example.com", []string{}, true) require.NoError(t, err) - err = store.SaveNetworkResource(context.Background(), LockingStrengthUpdate, netResource) + err = store.SaveNetworkResource(context.Background(), netResource) require.NoError(t, err) - savedNetResource, err := store.GetNetworkResourceByID(context.Background(), LockingStrengthShare, accountID, netResource.ID) + savedNetResource, err := store.GetNetworkResourceByID(context.Background(), LockingStrengthNone, accountID, netResource.ID) require.NoError(t, err) require.Equal(t, netResource.ID, savedNetResource.ID) require.Equal(t, netResource.Name, savedNetResource.Name) @@ -2475,10 +2475,10 @@ func TestSqlStore_DeleteNetworkResource(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" netResourceID := "ctc4nci7qv9061u6ilfg" - err = store.DeleteNetworkResource(context.Background(), LockingStrengthUpdate, accountID, netResourceID) + err = store.DeleteNetworkResource(context.Background(), accountID, netResourceID) require.NoError(t, err) - netResource, err := store.GetNetworkByID(context.Background(), LockingStrengthShare, accountID, netResourceID) + netResource, err := store.GetNetworkByID(context.Background(), LockingStrengthNone, accountID, netResourceID) require.Error(t, err) sErr, ok := status.FromError(err) require.True(t, ok) @@ -2502,18 +2502,18 @@ func TestSqlStore_AddAndRemoveResourceFromGroup(t *testing.T) { err = store.AddResourceToGroup(context.Background(), accountID, groupID, res) require.NoError(t, err) - group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.NoError(t, err) require.Contains(t, group.Resources, *res) - groups, err := store.GetResourceGroups(context.Background(), LockingStrengthShare, accountID, resourceId) + groups, err := store.GetResourceGroups(context.Background(), LockingStrengthNone, accountID, resourceId) require.NoError(t, err) require.Len(t, groups, 1) err = store.RemoveResourceFromGroup(context.Background(), accountID, groupID, res.ID) require.NoError(t, err) - group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err = store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.NoError(t, err) require.NotContains(t, group.Resources, *res) } @@ -2527,14 +2527,14 @@ func TestSqlStore_AddPeerToGroup(t *testing.T) { peerID := "cfefqs706sqkneg59g4g" groupID := "cfefqs706sqkneg59g4h" - group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.NoError(t, err, "failed to get group") require.Len(t, group.Peers, 0, "group should have 0 peers") err = store.AddPeerToGroup(context.Background(), accountID, peerID, groupID) require.NoError(t, err, "failed to add peer to group") - group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err = store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.NoError(t, err, "failed to get group") require.Len(t, group.Peers, 1, "group should have 1 peers") require.Contains(t, group.Peers, peerID) @@ -2554,18 +2554,18 @@ func TestSqlStore_AddPeerToAllGroup(t *testing.T) { DNSLabel: "peer1.domain.test", } - group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err := store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.NoError(t, err, "failed to get group") require.Len(t, group.Peers, 2, "group should have 2 peers") require.NotContains(t, group.Peers, peer.ID) - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) + err = store.AddPeerToAccount(context.Background(), peer) require.NoError(t, err, "failed to add peer to account") err = store.AddPeerToAllGroup(context.Background(), accountID, peer.ID) require.NoError(t, err, "failed to add peer to all group") - group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + group, err = store.GetGroupByID(context.Background(), LockingStrengthNone, accountID, groupID) require.NoError(t, err, "failed to get group") require.Len(t, group.Peers, 3, "group should have peers") require.Contains(t, group.Peers, peer.ID) @@ -2609,10 +2609,10 @@ func TestSqlStore_AddPeerToAccount(t *testing.T) { CreatedAt: time.Now().UTC(), Ephemeral: true, } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) + err = store.AddPeerToAccount(context.Background(), peer) require.NoError(t, err, "failed to add peer to account") - storedPeer, err := store.GetPeerByID(context.Background(), LockingStrengthShare, accountID, peer.ID) + storedPeer, err := store.GetPeerByID(context.Background(), LockingStrengthNone, accountID, peer.ID) require.NoError(t, err, "failed to get peer") assert.Equal(t, peer.ID, storedPeer.ID) @@ -2643,7 +2643,7 @@ func TestSqlStore_GetPeerGroups(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" peerID := "cfefqs706sqkneg59g4g" - groups, err := store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peerID) + groups, err := store.GetPeerGroups(context.Background(), LockingStrengthNone, accountID, peerID) require.NoError(t, err) assert.Len(t, groups, 1) assert.Equal(t, groups[0].Name, "All") @@ -2651,7 +2651,7 @@ func TestSqlStore_GetPeerGroups(t *testing.T) { err = store.AddPeerToGroup(context.Background(), accountID, peerID, "cfefqs706sqkneg59g4h") require.NoError(t, err) - groups, err = store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peerID) + groups, err = store.GetPeerGroups(context.Background(), LockingStrengthNone, accountID, peerID) require.NoError(t, err) assert.Len(t, groups, 2) } @@ -2705,7 +2705,7 @@ func TestSqlStore_GetAccountPeers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peers, err := store.GetAccountPeers(context.Background(), LockingStrengthShare, tt.accountID, tt.nameFilter, tt.ipFilter) + peers, err := store.GetAccountPeers(context.Background(), LockingStrengthNone, tt.accountID, tt.nameFilter, tt.ipFilter) require.NoError(t, err) require.Len(t, peers, tt.expectedCount) }) @@ -2742,7 +2742,7 @@ func TestSqlStore_GetAccountPeersWithExpiration(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peers, err := store.GetAccountPeersWithExpiration(context.Background(), LockingStrengthShare, tt.accountID) + peers, err := store.GetAccountPeersWithExpiration(context.Background(), LockingStrengthNone, tt.accountID) require.NoError(t, err) require.Len(t, peers, tt.expectedCount) }) @@ -2778,7 +2778,7 @@ func TestSqlStore_GetAccountPeersWithInactivity(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peers, err := store.GetAccountPeersWithInactivity(context.Background(), LockingStrengthShare, tt.accountID) + peers, err := store.GetAccountPeersWithInactivity(context.Background(), LockingStrengthNone, tt.accountID) require.NoError(t, err) require.Len(t, peers, tt.expectedCount) }) @@ -2790,7 +2790,7 @@ func TestSqlStore_GetAllEphemeralPeers(t *testing.T) { t.Cleanup(cleanup) require.NoError(t, err) - peers, err := store.GetAllEphemeralPeers(context.Background(), LockingStrengthShare) + peers, err := store.GetAllEphemeralPeers(context.Background(), LockingStrengthNone) require.NoError(t, err) require.Len(t, peers, 1) require.True(t, peers[0].Ephemeral) @@ -2841,7 +2841,7 @@ func TestSqlStore_GetUserPeers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - peers, err := store.GetUserPeers(context.Background(), LockingStrengthShare, tt.accountID, tt.userID) + peers, err := store.GetUserPeers(context.Background(), LockingStrengthNone, tt.accountID, tt.userID) require.NoError(t, err) require.Len(t, peers, tt.expectedCount) }) @@ -2856,10 +2856,10 @@ func TestSqlStore_DeletePeer(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" peerID := "csrnkiq7qv9d8aitqd50" - err = store.DeletePeer(context.Background(), LockingStrengthUpdate, accountID, peerID) + err = store.DeletePeer(context.Background(), accountID, peerID) require.NoError(t, err) - peer, err := store.GetPeerByID(context.Background(), LockingStrengthShare, accountID, peerID) + peer, err := store.GetPeerByID(context.Background(), LockingStrengthNone, accountID, peerID) require.Error(t, err) require.Nil(t, peer) } @@ -2888,7 +2888,7 @@ func TestSqlStore_DatabaseBlocking(t *testing.T) { <-start err := store.ExecuteInTransaction(context.Background(), func(tx Store) error { - _, err := tx.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog") + _, err := tx.GetAccountIDByPeerID(context.Background(), LockingStrengthNone, "cfvprsrlo1hqoo49ohog") return err }) if err != nil { @@ -2906,7 +2906,7 @@ func TestSqlStore_DatabaseBlocking(t *testing.T) { t.Logf("Entered routine 2-%d", i) <-start - _, err := store.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog") + _, err := store.GetAccountIDByPeerID(context.Background(), LockingStrengthNone, "cfvprsrlo1hqoo49ohog") if err != nil { t.Errorf("Failed, got error: %v", err) return @@ -2965,7 +2965,7 @@ func TestSqlStore_GetAccountCreatedBy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID) + createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthNone, tt.accountID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -3011,7 +3011,7 @@ func TestSqlStore_GetUserByUserID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID) + user, err := store.GetUserByUserID(context.Background(), LockingStrengthNone, tt.userID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -3034,7 +3034,7 @@ func TestSqlStore_GetUserByPATID(t *testing.T) { id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id) + user, err := store.GetUserByPATID(context.Background(), LockingStrengthNone, id) require.NoError(t, err) require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id) } @@ -3057,10 +3057,10 @@ func TestSqlStore_SaveUser(t *testing.T) { CreatedAt: time.Now().UTC().Add(-time.Hour), Issued: types.UserIssuedIntegration, } - err = store.SaveUser(context.Background(), LockingStrengthUpdate, user) + err = store.SaveUser(context.Background(), user) require.NoError(t, err) - saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id) + saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthNone, user.Id) require.NoError(t, err) require.Equal(t, user.Id, saveUser.Id) require.Equal(t, user.AccountID, saveUser.AccountID) @@ -3080,7 +3080,7 @@ func TestSqlStore_SaveUsers(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Len(t, accountUsers, 2) @@ -3098,18 +3098,18 @@ func TestSqlStore_SaveUsers(t *testing.T) { AutoGroups: []string{"groupA"}, }, } - err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + err = store.SaveUsers(context.Background(), users) require.NoError(t, err) - accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Len(t, accountUsers, 4) users[1].AutoGroups = []string{"groupA", "groupC"} - err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + err = store.SaveUsers(context.Background(), users) require.NoError(t, err) - user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, users[1].Id) + user, err := store.GetUserByUserID(context.Background(), LockingStrengthNone, users[1].Id) require.NoError(t, err) require.Equal(t, users[1].AutoGroups, user.AutoGroups) } @@ -3122,14 +3122,14 @@ func TestSqlStore_DeleteUser(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" - err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID) + err = store.DeleteUser(context.Background(), accountID, userID) require.NoError(t, err) - user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID) + user, err := store.GetUserByUserID(context.Background(), LockingStrengthNone, userID) require.Error(t, err) require.Nil(t, user) - userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID) + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthNone, userID) require.NoError(t, err) require.Len(t, userPATs, 0) } @@ -3165,7 +3165,7 @@ func TestSqlStore_GetPATByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID) + pat, err := store.GetPATByID(context.Background(), LockingStrengthNone, userID, tt.patID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -3186,7 +3186,7 @@ func TestSqlStore_GetUserPATs(t *testing.T) { t.Cleanup(cleanup) require.NoError(t, err) - userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003") + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthNone, "f4f6d672-63fb-11ec-90d6-0242ac120003") require.NoError(t, err) require.Len(t, userPATs, 1) } @@ -3196,7 +3196,7 @@ func TestSqlStore_GetPATByHashedToken(t *testing.T) { t.Cleanup(cleanup) require.NoError(t, err) - pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN") + pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthNone, "SoMeHaShEdToKeN") require.NoError(t, err) require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID) } @@ -3209,10 +3209,10 @@ func TestSqlStore_MarkPATUsed(t *testing.T) { userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" - err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID) + err = store.MarkPATUsed(context.Background(), patID) require.NoError(t, err) - pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + pat, err := store.GetPATByID(context.Background(), LockingStrengthNone, userID, patID) require.NoError(t, err) now := time.Now().UTC() require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now") @@ -3235,10 +3235,10 @@ func TestSqlStore_SavePAT(t *testing.T) { CreatedAt: time.Now().UTC().Add(time.Hour), LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)), } - err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat) + err = store.SavePAT(context.Background(), pat) require.NoError(t, err) - savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID) + savePAT, err := store.GetPATByID(context.Background(), LockingStrengthNone, userID, pat.ID) require.NoError(t, err) require.Equal(t, pat.ID, savePAT.ID) require.Equal(t, pat.UserID, savePAT.UserID) @@ -3257,10 +3257,10 @@ func TestSqlStore_DeletePAT(t *testing.T) { userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" - err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID) + err = store.DeletePAT(context.Background(), userID, patID) require.NoError(t, err) - pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + pat, err := store.GetPATByID(context.Background(), LockingStrengthNone, userID, patID) require.Error(t, err) require.Nil(t, pat) } @@ -3272,7 +3272,7 @@ func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Len(t, accountUsers, 2) @@ -3286,10 +3286,10 @@ func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) { }) } - err = store.SaveUsers(context.Background(), LockingStrengthUpdate, usersToSave) + err = store.SaveUsers(context.Background(), usersToSave) require.NoError(t, err) - accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Equal(t, 8002, len(accountUsers)) } @@ -3301,7 +3301,7 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - accountGroups, err := store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + accountGroups, err := store.GetAccountGroups(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Len(t, accountGroups, 3) @@ -3315,10 +3315,10 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { }) } - err = store.CreateGroups(context.Background(), LockingStrengthUpdate, accountID, groupsToSave) + err = store.CreateGroups(context.Background(), accountID, groupsToSave) require.NoError(t, err) - accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.Equal(t, 8003, len(accountGroups)) } @@ -3351,7 +3351,7 @@ func TestSqlStore_GetAccountRoutes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - routes, err := store.GetAccountRoutes(context.Background(), LockingStrengthShare, tt.accountID) + routes, err := store.GetAccountRoutes(context.Background(), LockingStrengthNone, tt.accountID) require.NoError(t, err) require.Len(t, routes, tt.expectedCount) }) @@ -3388,7 +3388,7 @@ func TestSqlStore_GetRouteByID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - route, err := store.GetRouteByID(context.Background(), LockingStrengthShare, accountID, tt.routeID) + route, err := store.GetRouteByID(context.Background(), LockingStrengthNone, accountID, tt.routeID) if tt.expectError { require.Error(t, err) sErr, ok := status.FromError(err) @@ -3424,10 +3424,10 @@ func TestSqlStore_SaveRoute(t *testing.T) { Groups: []string{"groupA"}, AccessControlGroups: []string{}, } - err = store.SaveRoute(context.Background(), LockingStrengthUpdate, route) + err = store.SaveRoute(context.Background(), route) require.NoError(t, err) - saveRoute, err := store.GetRouteByID(context.Background(), LockingStrengthShare, accountID, string(route.ID)) + saveRoute, err := store.GetRouteByID(context.Background(), LockingStrengthNone, accountID, string(route.ID)) require.NoError(t, err) require.Equal(t, route, saveRoute) @@ -3441,10 +3441,10 @@ func TestSqlStore_DeleteRoute(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" routeID := "ct03t427qv97vmtmglog" - err = store.DeleteRoute(context.Background(), LockingStrengthUpdate, accountID, routeID) + err = store.DeleteRoute(context.Background(), accountID, routeID) require.NoError(t, err) - route, err := store.GetRouteByID(context.Background(), LockingStrengthShare, accountID, routeID) + route, err := store.GetRouteByID(context.Background(), LockingStrengthNone, accountID, routeID) require.Error(t, err) require.Nil(t, route) } @@ -3455,7 +3455,7 @@ func TestSqlStore_GetAccountMeta(t *testing.T) { require.NoError(t, err) accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - accountMeta, err := store.GetAccountMeta(context.Background(), LockingStrengthShare, accountID) + accountMeta, err := store.GetAccountMeta(context.Background(), LockingStrengthNone, accountID) require.NoError(t, err) require.NotNil(t, accountMeta) require.Equal(t, accountID, accountMeta.AccountID) @@ -3567,7 +3567,7 @@ func BenchmarkGetAccountPeers(b *testing.B) { DNSLabel: fmt.Sprintf("peer%d.example.com", i), IP: intToIPv4(uint32(i)), } - err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) + err = store.AddPeerToAccount(context.Background(), peer) if err != nil { b.Fatalf("Failed to add peer: %v", err) } @@ -3580,7 +3580,7 @@ func BenchmarkGetAccountPeers(b *testing.B) { ID: groupID, AccountID: accountID, } - err = store.CreateGroup(context.Background(), LockingStrengthUpdate, group) + err = store.CreateGroup(context.Background(), group) if err != nil { b.Fatalf("Failed to create group: %v", err) } @@ -3595,7 +3595,7 @@ func BenchmarkGetAccountPeers(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peers[i%numberOfPeers].ID) + _, err := store.GetPeerGroups(context.Background(), LockingStrengthNone, accountID, peers[i%numberOfPeers].ID) if err != nil { b.Fatal(err) } diff --git a/management/server/store/store.go b/management/server/store/store.go index 912939bc2..da4459256 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -72,8 +72,8 @@ type Store interface { SaveAccount(ctx context.Context, account *types.Account) error DeleteAccount(ctx context.Context, account *types.Account) error UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error - SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error - SaveAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.Settings) error + SaveDNSSettings(ctx context.Context, accountID string, settings *types.DNSSettings) error + SaveAccountSettings(ctx context.Context, accountID string, settings *types.Settings) error CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) SaveAccountOnboarding(ctx context.Context, onboarding *types.AccountOnboarding) error @@ -81,10 +81,10 @@ type Store interface { GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) GetAccountOwner(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.User, error) - SaveUsers(ctx context.Context, lockStrength LockingStrength, users []*types.User) error - SaveUser(ctx context.Context, lockStrength LockingStrength, user *types.User) error + SaveUsers(ctx context.Context, users []*types.User) error + SaveUser(ctx context.Context, user *types.User) error SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error - DeleteUser(ctx context.Context, lockStrength LockingStrength, accountID, userID string) error + DeleteUser(ctx context.Context, accountID, userID string) error GetTokenIDByHashedToken(ctx context.Context, secret string) (string, error) DeleteHashedPAT2TokenIDIndex(hashedToken string) error DeleteTokenID2UserIDIndex(tokenID string) error @@ -92,34 +92,34 @@ type Store interface { GetPATByID(ctx context.Context, lockStrength LockingStrength, userID, patID string) (*types.PersonalAccessToken, error) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) - MarkPATUsed(ctx context.Context, lockStrength LockingStrength, patID string) error - SavePAT(ctx context.Context, strength LockingStrength, pat *types.PersonalAccessToken) error - DeletePAT(ctx context.Context, strength LockingStrength, userID, patID string) error + MarkPATUsed(ctx context.Context, patID string) error + SavePAT(ctx context.Context, pat *types.PersonalAccessToken) error + DeletePAT(ctx context.Context, userID, patID string) error GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types.Group, error) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) - CreateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error - UpdateGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error - CreateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error - UpdateGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error - DeleteGroup(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) error - DeleteGroups(ctx context.Context, strength LockingStrength, accountID string, groupIDs []string) error + CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error + UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error + CreateGroup(ctx context.Context, group *types.Group) error + UpdateGroup(ctx context.Context, group *types.Group) error + DeleteGroup(ctx context.Context, accountID, groupID string) error + DeleteGroups(ctx context.Context, accountID string, groupIDs []string) error GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) - CreatePolicy(ctx context.Context, lockStrength LockingStrength, policy *types.Policy) error - SavePolicy(ctx context.Context, lockStrength LockingStrength, policy *types.Policy) error - DeletePolicy(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) error + CreatePolicy(ctx context.Context, policy *types.Policy) error + SavePolicy(ctx context.Context, policy *types.Policy) error + DeletePolicy(ctx context.Context, accountID, policyID string) error GetPostureCheckByChecksDefinition(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) GetAccountPostureChecks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*posture.Checks, error) GetPostureChecksByID(ctx context.Context, lockStrength LockingStrength, accountID, postureCheckID string) (*posture.Checks, error) GetPostureChecksByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, postureChecksIDs []string) (map[string]*posture.Checks, error) - SavePostureChecks(ctx context.Context, lockStrength LockingStrength, postureCheck *posture.Checks) error - DeletePostureChecks(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) error + SavePostureChecks(ctx context.Context, postureCheck *posture.Checks) error + DeletePostureChecks(ctx context.Context, accountID, postureChecksID string) error GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountId string, hostname string) ([]string, error) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error @@ -130,7 +130,7 @@ type Store interface { GetPeerGroupIDs(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]string, error) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error - AddPeerToAccount(ctx context.Context, lockStrength LockingStrength, peer *nbpeer.Peer) error + AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) @@ -139,30 +139,30 @@ type Store interface { GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) - SavePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peer *nbpeer.Peer) error - SavePeerStatus(ctx context.Context, lockStrength LockingStrength, accountID, peerID string, status nbpeer.PeerStatus) error - SavePeerLocation(ctx context.Context, lockStrength LockingStrength, accountID string, peer *nbpeer.Peer) error - DeletePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error + SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error + SavePeerStatus(ctx context.Context, accountID, peerID string, status nbpeer.PeerStatus) error + SavePeerLocation(ctx context.Context, accountID string, peer *nbpeer.Peer) error + DeletePeer(ctx context.Context, accountID string, peerID string) error GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string) error GetAccountSetupKeys(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.SetupKey, error) GetSetupKeyByID(ctx context.Context, lockStrength LockingStrength, accountID, setupKeyID string) (*types.SetupKey, error) - SaveSetupKey(ctx context.Context, lockStrength LockingStrength, setupKey *types.SetupKey) error - DeleteSetupKey(ctx context.Context, lockStrength LockingStrength, accountID, keyID string) error + SaveSetupKey(ctx context.Context, setupKey *types.SetupKey) error + DeleteSetupKey(ctx context.Context, accountID, keyID string) error GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) (*route.Route, error) - SaveRoute(ctx context.Context, lockStrength LockingStrength, route *route.Route) error - DeleteRoute(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) error + SaveRoute(ctx context.Context, route *route.Route) error + DeleteRoute(ctx context.Context, accountID, routeID string) error GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*dns.NameServerGroup, error) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, nameServerGroupID string, accountID string) (*dns.NameServerGroup, error) - SaveNameServerGroup(ctx context.Context, lockStrength LockingStrength, nameServerGroup *dns.NameServerGroup) error - DeleteNameServerGroup(ctx context.Context, lockStrength LockingStrength, accountID, nameServerGroupID string) error + SaveNameServerGroup(ctx context.Context, nameServerGroup *dns.NameServerGroup) error + DeleteNameServerGroup(ctx context.Context, accountID, nameServerGroupID string) error GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountId string) ([]net.IP, error) - IncrementNetworkSerial(ctx context.Context, lockStrength LockingStrength, accountId string) error + IncrementNetworkSerial(ctx context.Context, accountId string) error GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountId string) (*types.Network, error) GetInstallationID() string @@ -184,21 +184,21 @@ type Store interface { GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) GetNetworkByID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) (*networkTypes.Network, error) - SaveNetwork(ctx context.Context, lockStrength LockingStrength, network *networkTypes.Network) error - DeleteNetwork(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) error + SaveNetwork(ctx context.Context, network *networkTypes.Network) error + DeleteNetwork(ctx context.Context, accountID, networkID string) error GetNetworkRoutersByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*routerTypes.NetworkRouter, error) GetNetworkRoutersByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*routerTypes.NetworkRouter, error) GetNetworkRouterByID(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) (*routerTypes.NetworkRouter, error) - SaveNetworkRouter(ctx context.Context, lockStrength LockingStrength, router *routerTypes.NetworkRouter) error - DeleteNetworkRouter(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) error + SaveNetworkRouter(ctx context.Context, router *routerTypes.NetworkRouter) error + DeleteNetworkRouter(ctx context.Context, accountID, routerID string) error GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*resourceTypes.NetworkResource, error) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) - SaveNetworkResource(ctx context.Context, lockStrength LockingStrength, resource *resourceTypes.NetworkResource) error - DeleteNetworkResource(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) error + SaveNetworkResource(ctx context.Context, resource *resourceTypes.NetworkResource) error + DeleteNetworkResource(ctx context.Context, accountID, resourceID string) error GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) GetAccountGroupPeers(ctx context.Context, lockStrength LockingStrength, accountID string) (map[string]map[string]struct{}, error) diff --git a/management/server/user.go b/management/server/user.go index e33f89dfd..ba1835f22 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -17,11 +17,11 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/shared/management/status" ) // createServiceUser creates a new service user under the given account. @@ -46,7 +46,7 @@ func (am *DefaultAccountManager) createServiceUser(ctx context.Context, accountI newUser.AccountID = accountID log.WithContext(ctx).Debugf("New User: %v", newUser) - if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { + if err = am.Store.SaveUser(ctx, newUser); err != nil { return nil, err } @@ -95,14 +95,14 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u return nil, status.NewPermissionDeniedError() } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return nil, err } inviterID := userID if initiatorUser.IsServiceUser { - createdBy, err := am.Store.GetAccountCreatedBy(ctx, store.LockingStrengthShare, accountID) + createdBy, err := am.Store.GetAccountCreatedBy(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -124,7 +124,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u CreatedAt: time.Now().UTC(), } - if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { + if err = am.Store.SaveUser(ctx, newUser); err != nil { return nil, err } @@ -178,13 +178,13 @@ func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID } func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) { - return am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id) + return am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, id) } // GetUser looks up a user by provided nbContext.UserAuths. // Expects account to have been created already. func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbContext.UserAuth) (*types.User, error) { - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userAuth.UserId) if err != nil { return nil, err } @@ -209,11 +209,11 @@ func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAu // ListUsers returns lists of all users under the account. // It doesn't populate user information such as email or name. func (am *DefaultAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) { - return am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + return am.Store.GetAccountUsers(ctx, store.LockingStrengthNone, accountID) } func (am *DefaultAccountManager) deleteServiceUser(ctx context.Context, accountID string, initiatorUserID string, targetUser *types.User) error { - if err := am.Store.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUser.Id); err != nil { + if err := am.Store.DeleteUser(ctx, accountID, targetUser.Id); err != nil { return err } meta := map[string]any{"name": targetUser.ServiceUserName, "created_at": targetUser.CreatedAt} @@ -230,7 +230,7 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return err } @@ -243,7 +243,7 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init return status.NewPermissionDeniedError() } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID) if err != nil { return err } @@ -347,12 +347,12 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string return nil, status.NewPermissionDeniedError() } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID) if err != nil { return nil, err } @@ -367,7 +367,7 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string return nil, status.Errorf(status.Internal, "failed to create PAT: %v", err) } - if err = am.Store.SavePAT(ctx, store.LockingStrengthUpdate, &pat.PersonalAccessToken); err != nil { + if err = am.Store.SavePAT(ctx, &pat.PersonalAccessToken); err != nil { return nil, err } @@ -390,12 +390,12 @@ func (am *DefaultAccountManager) DeletePAT(ctx context.Context, accountID string return status.NewPermissionDeniedError() } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID) if err != nil { return err } @@ -404,12 +404,12 @@ func (am *DefaultAccountManager) DeletePAT(ctx context.Context, accountID string return status.NewAdminPermissionError() } - pat, err := am.Store.GetPATByID(ctx, store.LockingStrengthShare, targetUserID, tokenID) + pat, err := am.Store.GetPATByID(ctx, store.LockingStrengthNone, targetUserID, tokenID) if err != nil { return err } - if err = am.Store.DeletePAT(ctx, store.LockingStrengthUpdate, targetUserID, tokenID); err != nil { + if err = am.Store.DeletePAT(ctx, targetUserID, tokenID); err != nil { return err } @@ -429,12 +429,12 @@ func (am *DefaultAccountManager) GetPAT(ctx context.Context, accountID string, i return nil, status.NewPermissionDeniedError() } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID) if err != nil { return nil, err } @@ -443,7 +443,7 @@ func (am *DefaultAccountManager) GetPAT(ctx context.Context, accountID string, i return nil, status.NewAdminPermissionError() } - return am.Store.GetPATByID(ctx, store.LockingStrengthShare, targetUserID, tokenID) + return am.Store.GetPATByID(ctx, store.LockingStrengthNone, targetUserID, tokenID) } // GetAllPATs returns all PATs for a user @@ -456,12 +456,12 @@ func (am *DefaultAccountManager) GetAllPATs(ctx context.Context, accountID strin return nil, status.NewPermissionDeniedError() } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID) if err != nil { return nil, err } @@ -470,7 +470,7 @@ func (am *DefaultAccountManager) GetAllPATs(ctx context.Context, accountID strin return nil, status.NewAdminPermissionError() } - return am.Store.GetUserPATs(ctx, store.LockingStrengthShare, targetUserID) + return am.Store.GetUserPATs(ctx, store.LockingStrengthNone, targetUserID) } // SaveUser saves updates to the given user. If the user doesn't exist, it will throw status.NotFound error. @@ -511,7 +511,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, if !allowed { return nil, status.NewPermissionDeniedError() } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -521,7 +521,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, var addUserEvents []func() var usersToSave = make([]*types.User, 0, len(updates)) - groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, fmt.Errorf("error getting account groups: %w", err) } @@ -533,7 +533,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, var initiatorUser *types.User if initiatorUserID != activity.SystemInitiator { - result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return nil, err } @@ -560,7 +560,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, updateAccountPeers = true } } - return transaction.SaveUsers(ctx, store.LockingStrengthUpdate, usersToSave) + return transaction.SaveUsers(ctx, usersToSave) }) if err != nil { return nil, err @@ -593,7 +593,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, } if settings.GroupsPropagationEnabled && updateAccountPeers { - if err = am.Store.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + if err = am.Store.IncrementNetworkSerial(ctx, accountID); err != nil { return nil, fmt.Errorf("failed to increment network serial: %w", err) } am.UpdateAccountPeers(ctx, accountID) @@ -700,7 +700,7 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact // getUserOrCreateIfNotExists retrieves the existing user or creates a new one if it doesn't exist. func getUserOrCreateIfNotExists(ctx context.Context, transaction store.Store, accountID string, update *types.User, addIfNotExists bool) (*types.User, error) { - existingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, update.Id) + existingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthNone, update.Id) if err != nil { if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound { if !addIfNotExists { @@ -724,7 +724,7 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi newInitiatorUser := initiatorUser.Copy() newInitiatorUser.Role = types.UserRoleAdmin - if err := transaction.SaveUser(ctx, store.LockingStrengthUpdate, newInitiatorUser); err != nil { + if err := transaction.SaveUser(ctx, newInitiatorUser); err != nil { return false, err } return true, nil @@ -835,7 +835,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun var user *types.User if initiatorUserID != activity.SystemInitiator { - result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } @@ -845,7 +845,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun accountUsers := []*types.User{} switch { case allowed: - accountUsers, err = am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + accountUsers, err = am.Store.GetAccountUsers(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -939,7 +939,7 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a // expireAndUpdatePeers expires all peers of the given user and updates them in the account func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accountID string, peers []*nbpeer.Peer) error { log.WithContext(ctx).Debugf("Expiring %d peers for account %s", len(peers), accountID) - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return err } @@ -956,7 +956,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou peerIDs = append(peerIDs, peer.ID) peer.MarkLoginExpired(true) - if err := am.Store.SavePeerStatus(ctx, store.LockingStrengthUpdate, accountID, peer.ID, *peer.Status); err != nil { + if err := am.Store.SavePeerStatus(ctx, accountID, peer.ID, *peer.Status); err != nil { return err } am.StoreEvent( @@ -1009,7 +1009,7 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account return status.NewPermissionDeniedError() } - initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID) if err != nil { return err } @@ -1023,7 +1023,7 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account continue } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID) if err != nil { allErrors = errors.Join(allErrors, err) continue @@ -1087,12 +1087,12 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountI var err error err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { - targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserInfo.ID) + targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, targetUserInfo.ID) if err != nil { return fmt.Errorf("failed to get user to delete: %w", err) } - userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserInfo.ID) + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthNone, accountID, targetUserInfo.ID) if err != nil { return fmt.Errorf("failed to get user peers: %w", err) } @@ -1105,7 +1105,7 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountI } } - if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserInfo.ID); err != nil { + if err = transaction.DeleteUser(ctx, accountID, targetUserInfo.ID); err != nil { return fmt.Errorf("failed to delete user: %s %w", targetUserInfo.ID, err) } @@ -1126,7 +1126,7 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountI // GetOwnerInfo retrieves the owner information for a given account ID. func (am *DefaultAccountManager) GetOwnerInfo(ctx context.Context, accountID string) (*types.UserInfo, error) { - owner, err := am.Store.GetAccountOwner(ctx, store.LockingStrengthShare, accountID) + owner, err := am.Store.GetAccountOwner(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } @@ -1176,7 +1176,7 @@ func validateUserInvite(invite *types.UserInfo) error { func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) { accountID, userID := userAuth.AccountId, userAuth.UserId - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return nil, err } @@ -1193,7 +1193,7 @@ func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAut return nil, err } - settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } diff --git a/management/server/user_test.go b/management/server/user_test.go index 7515439b8..8ab0c1565 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -15,9 +15,9 @@ import ( "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/roles" - "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/management/server/users" "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/shared/management/status" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" @@ -88,7 +88,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) { assert.Equal(t, pat.ID, tokenID) - user, err := am.Store.GetUserByPATID(context.Background(), store.LockingStrengthShare, tokenID) + user, err := am.Store.GetUserByPATID(context.Background(), store.LockingStrengthNone, tokenID) if err != nil { t.Fatalf("Error when getting user by token ID: %s", err) } @@ -1521,7 +1521,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) { _, err = am.SaveOrAddUser(context.Background(), "account2", "ownerAccount2", account1.Users[targetId], true) assert.Error(t, err, "update user to another account should fail") - user, err := s.GetUserByUserID(context.Background(), store.LockingStrengthShare, targetId) + user, err := s.GetUserByUserID(context.Background(), store.LockingStrengthNone, targetId) require.NoError(t, err) assert.Equal(t, account1.Users[targetId].Id, user.Id) assert.Equal(t, account1.Users[targetId].AccountID, user.AccountID) diff --git a/management/server/users/manager.go b/management/server/users/manager.go index 718eb6190..e07f28706 100644 --- a/management/server/users/manager.go +++ b/management/server/users/manager.go @@ -26,7 +26,7 @@ func NewManager(store store.Store) Manager { } func (m *managerImpl) GetUser(ctx context.Context, userID string) (*types.User, error) { - return m.store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + return m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) } func NewManagerMock() Manager { From 3d19468b6c3520296bfc462a6ce22cb122940384 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:30:19 +0200 Subject: [PATCH 202/213] [client] Add windows arm64 build (#4206) --- .github/workflows/release.yml | 14 +++++++++++++- .goreleaser.yaml | 2 -- .goreleaser_ui.yaml | 21 +++++++++++++++++++-- client/installer.nsis | 26 ++++++++++++++++++++++---- client/netbird.wxs | 10 ++++++---- 5 files changed, 60 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44e02f457..1ec18731a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.0.21" + SIGN_PIPE_VER: "v0.0.22" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" COPYRIGHT: "NetBird GmbH" @@ -79,6 +79,8 @@ jobs: run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - name: Generate windows syso amd64 run: goversioninfo -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_amd64.syso + - name: Generate windows syso arm64 + run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: @@ -154,10 +156,20 @@ jobs: - name: Install dependencies run: sudo apt update && sudo apt install -y -q libappindicator3-dev gir1.2-appindicator3-0.1 libxxf86vm-dev gcc-mingw-w64-x86-64 + + - name: Install LLVM-MinGW for ARM64 cross-compilation + run: | + cd /tmp + wget -q https://github.com/mstorsjo/llvm-mingw/releases/download/20250709/llvm-mingw-20250709-ucrt-ubuntu-22.04-x86_64.tar.xz + echo "60cafae6474c7411174cff1d4ba21a8e46cadbaeb05a1bace306add301628337 llvm-mingw-20250709-ucrt-ubuntu-22.04-x86_64.tar.xz" | sha256sum -c + tar -xf llvm-mingw-20250709-ucrt-ubuntu-22.04-x86_64.tar.xz + echo "/tmp/llvm-mingw-20250709-ucrt-ubuntu-22.04-x86_64/bin" >> $GITHUB_PATH - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - name: Generate windows syso amd64 run: goversioninfo -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso + - name: Generate windows syso arm64 + run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d4a97b447..59a95c89a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -16,8 +16,6 @@ builds: - arm64 - 386 ignore: - - goos: windows - goarch: arm64 - goos: windows goarch: arm - goos: windows diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 459f204d3..a243702ea 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -15,7 +15,7 @@ builds: - -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser mod_timestamp: "{{ .CommitTimestamp }}" - - id: netbird-ui-windows + - id: netbird-ui-windows-amd64 dir: client/ui binary: netbird-ui env: @@ -30,6 +30,22 @@ builds: - -H windowsgui mod_timestamp: "{{ .CommitTimestamp }}" + - id: netbird-ui-windows-arm64 + dir: client/ui + binary: netbird-ui + env: + - CGO_ENABLED=1 + - CC=aarch64-w64-mingw32-clang + - CXX=aarch64-w64-mingw32-clang++ + goos: + - windows + goarch: + - arm64 + ldflags: + - -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser + - -H windowsgui + mod_timestamp: "{{ .CommitTimestamp }}" + archives: - id: linux-arch name_template: "{{ .ProjectName }}-linux_{{ .Version }}_{{ .Os }}_{{ .Arch }}" @@ -38,7 +54,8 @@ archives: - id: windows-arch name_template: "{{ .ProjectName }}-windows_{{ .Version }}_{{ .Os }}_{{ .Arch }}" builds: - - netbird-ui-windows + - netbird-ui-windows-amd64 + - netbird-ui-windows-arm64 nfpms: - maintainer: Netbird diff --git a/client/installer.nsis b/client/installer.nsis index fabd727fc..41907030e 100644 --- a/client/installer.nsis +++ b/client/installer.nsis @@ -59,9 +59,15 @@ ShowInstDetails Show !define MUI_UNICON "${ICON}" !define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}" !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}" -!define MUI_FINISHPAGE_RUN -!define MUI_FINISHPAGE_RUN_TEXT "Start ${UI_APP_NAME}" -!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink" +!ifndef ARCH + !define ARCH "amd64" +!endif + +!if ${ARCH} == "amd64" + !define MUI_FINISHPAGE_RUN + !define MUI_FINISHPAGE_RUN_TEXT "Start ${UI_APP_NAME}" + !define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink" +!endif ###################################################################### !define MUI_ABORTWARNING @@ -213,7 +219,15 @@ Section -MainProgram ${INSTALL_TYPE} # SetOverwrite ifnewer SetOutPath "$INSTDIR" - File /r "..\\dist\\netbird_windows_amd64\\" + !ifndef ARCH + !define ARCH "amd64" + !endif + + !if ${ARCH} == "arm64" + File /r "..\\dist\\netbird_windows_arm64\\" + !else + File /r "..\\dist\\netbird_windows_amd64\\" + !endif SectionEnd ###################################################################### @@ -292,7 +306,9 @@ DetailPrint "Deleting application files..." Delete "$INSTDIR\${UI_APP_EXE}" Delete "$INSTDIR\${MAIN_APP_EXE}" Delete "$INSTDIR\wintun.dll" +!if ${ARCH} == "amd64" Delete "$INSTDIR\opengl32.dll" +!endif DetailPrint "Removing application directory..." RmDir /r "$INSTDIR" @@ -314,8 +330,10 @@ DetailPrint "Uninstallation finished." SectionEnd +!if ${ARCH} == "amd64" Function LaunchLink SetShellVarContext all SetOutPath $INSTDIR ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk" FunctionEnd +!endif diff --git a/client/netbird.wxs b/client/netbird.wxs index 91595539a..230521d3a 100644 --- a/client/netbird.wxs +++ b/client/netbird.wxs @@ -16,13 +16,15 @@ - - + + - - + + + + Date: Thu, 7 Aug 2025 11:49:37 +0200 Subject: [PATCH 203/213] [management] Mark SaveAccount deprecated (#4300) --- management/server/store/sql_store.go | 1 + 1 file changed, 1 insertion(+) diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 5986214d9..1bcae7048 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -172,6 +172,7 @@ func (s *SqlStore) AcquireReadLockByUID(ctx context.Context, uniqueID string) (u return unlock } +// Deprecated: Full account operations are no longer supported func (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) error { start := time.Now() defer func() { From 6da394355971de130262f88ea8f24519295139e4 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:08:30 +0300 Subject: [PATCH 204/213] [client] fix ssh command for non-default profile (#4298) [client] fix ssh command for non-default profile (#4298) --- client/cmd/ssh.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index 5a52b3795..035d06727 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -59,8 +59,8 @@ var sshCmd = &cobra.Command{ ctx := internal.CtxInitState(cmd.Context()) - pm := profilemanager.NewProfileManager() - activeProf, err := pm.GetActiveProfile() + sm := profilemanager.NewServiceManager(configPath) + activeProf, err := sm.GetActiveProfileState() if err != nil { return fmt.Errorf("get active profile: %v", err) } From 9e95841252c62b50ae93805c8dfd2b749ac95ea7 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:12:07 +0200 Subject: [PATCH 205/213] [management] during JSON migration filter duplicates on conflict (#4303) --- management/server/migration/migration.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go index 88af9a58f..78f4afbd5 100644 --- a/management/server/migration/migration.go +++ b/management/server/migration/migration.go @@ -15,6 +15,7 @@ import ( log "github.com/sirupsen/logrus" "gorm.io/gorm" + "gorm.io/gorm/clause" ) func GetColumnName(db *gorm.DB, column string) string { @@ -466,7 +467,7 @@ func MigrateJsonToTable[T any](ctx context.Context, db *gorm.DB, columnName stri } for _, value := range data { - if err := tx.Create( + if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create( mapperFunc(row["account_id"].(string), row["id"].(string), value), ).Error; err != nil { return fmt.Errorf("failed to insert id %v: %w", row["id"], err) From bef99d48f81cd356ab46ba421e01266803ccd29c Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:48:30 +0200 Subject: [PATCH 206/213] [client] Rename logout to deregister (#4307) --- client/cmd/logout.go | 9 +++++---- client/ui/profile.go | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/client/cmd/logout.go b/client/cmd/logout.go index 054571fbb..31fd13e49 100644 --- a/client/cmd/logout.go +++ b/client/cmd/logout.go @@ -12,8 +12,9 @@ import ( ) var logoutCmd = &cobra.Command{ - Use: "logout", - Short: "logout from the NetBird Management Service and delete peer", + Use: "deregister", + Aliases: []string{"logout"}, + Short: "deregister from the NetBird Management Service and delete peer", RunE: func(cmd *cobra.Command, args []string) error { SetFlagsFromEnvVars(rootCmd) @@ -44,10 +45,10 @@ var logoutCmd = &cobra.Command{ } if _, err := daemonClient.Logout(ctx, req); err != nil { - return fmt.Errorf("logout: %v", err) + return fmt.Errorf("deregister: %v", err) } - cmd.Println("Logged out successfully") + cmd.Println("Deregistered successfully") return nil }, } diff --git a/client/ui/profile.go b/client/ui/profile.go index b0502c1fb..f4505ab19 100644 --- a/client/ui/profile.go +++ b/client/ui/profile.go @@ -46,7 +46,7 @@ func (s *serviceClient) showProfilesUI() { widget.NewLabel(""), // profile name layout.NewSpacer(), widget.NewButton("Select", nil), - widget.NewButton("Logout", nil), + widget.NewButton("Deregister", nil), widget.NewButton("Remove", nil), ) }, @@ -128,7 +128,7 @@ func (s *serviceClient) showProfilesUI() { } logoutBtn.Show() - logoutBtn.SetText("Logout") + logoutBtn.SetText("Deregister") logoutBtn.OnTapped = func() { s.handleProfileLogout(profile.Name, refresh) } @@ -143,7 +143,7 @@ func (s *serviceClient) showProfilesUI() { if !confirm { return } - + err = s.removeProfile(profile.Name) if err != nil { log.Errorf("failed to remove profile: %v", err) @@ -334,27 +334,27 @@ func (s *serviceClient) getProfiles() ([]Profile, error) { func (s *serviceClient) handleProfileLogout(profileName string, refreshCallback func()) { dialog.ShowConfirm( - "Logout", - fmt.Sprintf("Are you sure you want to logout from '%s'?", profileName), + "Deregister", + fmt.Sprintf("Are you sure you want to deregister from '%s'?", profileName), func(confirm bool) { if !confirm { return } - + conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("failed to get service client: %v", err) dialog.ShowError(fmt.Errorf("failed to connect to service"), s.wProfiles) return } - + currUser, err := user.Current() if err != nil { log.Errorf("failed to get current user: %v", err) dialog.ShowError(fmt.Errorf("failed to get current user"), s.wProfiles) return } - + username := currUser.Username _, err = conn.Logout(s.ctx, &proto.LogoutRequest{ ProfileName: &profileName, @@ -362,16 +362,16 @@ func (s *serviceClient) handleProfileLogout(profileName string, refreshCallback }) if err != nil { log.Errorf("logout failed: %v", err) - dialog.ShowError(fmt.Errorf("logout failed"), s.wProfiles) + dialog.ShowError(fmt.Errorf("deregister failed"), s.wProfiles) return } - + dialog.ShowInformation( - "Logged Out", - fmt.Sprintf("Successfully logged out from '%s'", profileName), + "Deregistered", + fmt.Sprintf("Successfully deregistered from '%s'", profileName), s.wProfiles, ) - + refreshCallback() }, s.wProfiles, @@ -602,7 +602,7 @@ func (p *profileMenu) refresh() { // Add Logout menu item ctx2, cancel2 := context.WithCancel(context.Background()) - logoutItem := p.profileMenuItem.AddSubMenuItem("Logout", "") + logoutItem := p.profileMenuItem.AddSubMenuItem("Deregister", "") p.logoutSubItem = &subItem{logoutItem, ctx2, cancel2} go func() { @@ -616,9 +616,9 @@ func (p *profileMenu) refresh() { } if err := p.eventHandler.logout(p.ctx); err != nil { log.Errorf("logout failed: %v", err) - p.app.SendNotification(fyne.NewNotification("Error", "Failed to logout")) + p.app.SendNotification(fyne.NewNotification("Error", "Failed to deregister")) } else { - p.app.SendNotification(fyne.NewNotification("Success", "Logged out successfully")) + p.app.SendNotification(fyne.NewNotification("Success", "Deregistered successfully")) } } } From 0926400b8ac9fb872d61e763045813c6742d9350 Mon Sep 17 00:00:00 2001 From: "Krzysztof Nazarewski (kdn)" Date: Fri, 8 Aug 2025 17:44:25 +0200 Subject: [PATCH 207/213] fix: profilemanager panic when reading incomplete config (#4309) fix: profilemanager panic when reading incomplete config (#4309) --- client/cmd/service.go | 2 +- client/internal/profilemanager/config.go | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/client/cmd/service.go b/client/cmd/service.go index 0c566370b..997520f4c 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -47,7 +47,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") serviceEnvDesc := `Sets extra environment variables for the service. ` + `You can specify a comma-separated list of KEY=VALUE pairs. ` + - `E.g. --service-env LOG_LEVEL=debug,CUSTOM_VAR=value` + `E.g. --service-env NB_LOG_LEVEL=debug,CUSTOM_VAR=value` installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc) reconfigureCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc) diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 084a03d88..6bbdbd984 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -593,17 +593,9 @@ func update(input ConfigInput) (*Config, error) { return config, nil } +// GetConfig read config file and return with Config. Errors out if it does not exist func GetConfig(configPath string) (*Config, error) { - if !fileExists(configPath) { - return nil, fmt.Errorf("config file %s does not exist", configPath) - } - - config := &Config{} - if _, err := util.ReadJson(configPath, config); err != nil { - return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err) - } - - return config, nil + return readConfig(configPath, false) } // UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain. @@ -695,6 +687,11 @@ func CreateInMemoryConfig(input ConfigInput) (*Config, error) { // ReadConfig read config file and return with Config. If it is not exists create a new with default values func ReadConfig(configPath string) (*Config, error) { + return readConfig(configPath, true) +} + +// ReadConfig read config file and return with Config. If it is not exists create a new with default values +func readConfig(configPath string, createIfMissing bool) (*Config, error) { if fileExists(configPath) { err := util.EnforcePermission(configPath) if err != nil { @@ -715,6 +712,8 @@ func ReadConfig(configPath string) (*Config, error) { } return config, nil + } else if !createIfMissing { + return nil, fmt.Errorf("config file %s does not exist", configPath) } cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath}) From 0f52144894fef5f323b42c04b5593a45bd20fca6 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 8 Aug 2025 13:14:26 -0300 Subject: [PATCH 208/213] [misc] Add docs acknowledgement check (#4310) adds a GitHub Actions workflow to enforce documentation requirements for pull requests, ensuring contributors acknowledge whether their changes need documentation updates or provide a link to a corresponding docs PR. - Adds a new GitHub Actions workflow that validates documentation acknowledgement in PR descriptions - Updates the PR template to include mandatory documentation checkboxes and URL field - Implements validation logic to ensure exactly one documentation option is selected and verifies docs PR URLs when provided --- .github/pull_request_template.md | 12 +++- .github/workflows/docs-ack.yml | 94 ++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs-ack.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f41bc9e89..9d6bc96eb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,6 +12,16 @@ - [ ] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) -- [ ] Extended the README / documentation, if necessary > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). + +## Documentation +Select exactly one: + +- [ ] I added/updated documentation for this change +- [ ] Documentation is **not needed** for this change (explain why) + +### Docs PR URL (required if "docs added" is checked) +Paste the PR link from https://github.com/netbirdio/docs here: + +https://github.com/netbirdio/docs/pull/__ diff --git a/.github/workflows/docs-ack.yml b/.github/workflows/docs-ack.yml new file mode 100644 index 000000000..9116be8c7 --- /dev/null +++ b/.github/workflows/docs-ack.yml @@ -0,0 +1,94 @@ +name: Docs Acknowledgement + +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + contents: read + pull-requests: read + +jobs: + docs-ack: + name: Require docs PR URL or explicit "not needed" + runs-on: ubuntu-latest + + steps: + - name: Read PR body + id: body + run: | + BODY=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH") + echo "body<> $GITHUB_OUTPUT + echo "$BODY" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Validate checkbox selection + id: validate + run: | + body='${{ steps.body.outputs.body }}' + + added_checked=$(printf "%s" "$body" | grep -E '^- \[x\] I added/updated documentation' -i | wc -l | tr -d ' ') + noneed_checked=$(printf "%s" "$body" | grep -E '^- \[x\] Documentation is \*\*not needed\*\*' -i | wc -l | tr -d ' ') + + if [ "$added_checked" -eq 1 ] && [ "$noneed_checked" -eq 1 ]; then + echo "::error::Choose exactly one: either 'docs added' OR 'not needed'." + exit 1 + fi + + if [ "$added_checked" -eq 0 ] && [ "$noneed_checked" -eq 0 ]; then + echo "::error::You must check exactly one docs option in the PR template." + exit 1 + fi + + if [ "$added_checked" -eq 1 ]; then + echo "mode=added" >> $GITHUB_OUTPUT + else + echo "mode=noneed" >> $GITHUB_OUTPUT + fi + + - name: Extract docs PR URL (when 'docs added') + if: steps.validate.outputs.mode == 'added' + id: extract + run: | + body='${{ steps.body.outputs.body }}' + + # Strictly require HTTPS and that it's a PR in netbirdio/docs + # Examples accepted: + # https://github.com/netbirdio/docs/pull/1234 + url=$(printf "%s" "$body" | grep -Eo 'https://github\.com/netbirdio/docs/pull/[0-9]+' | head -n1 || true) + + if [ -z "$url" ]; then + echo "::error::You checked 'docs added' but didn't include a valid HTTPS PR link to netbirdio/docs (e.g., https://github.com/netbirdio/docs/pull/1234)." + exit 1 + fi + + pr_number=$(echo "$url" | sed -E 's#.*/pull/([0-9]+)$#\1#') + echo "url=$url" >> $GITHUB_OUTPUT + echo "pr_number=$pr_number" >> $GITHUB_OUTPUT + + - name: Verify docs PR exists (and is open or merged) + if: steps.validate.outputs.mode == 'added' + uses: actions/github-script@v7 + id: verify + with: + pr_number: ${{ steps.extract.outputs.pr_number }} + script: | + const prNumber = parseInt(core.getInput('pr_number'), 10); + const { data } = await github.rest.pulls.get({ + owner: 'netbirdio', + repo: 'docs', + pull_number: prNumber + }); + + // Allow open or merged PRs + const ok = data.state === 'open' || data.merged === true; + core.setOutput('state', data.state); + core.setOutput('merged', String(!!data.merged)); + if (!ok) { + core.setFailed(`Docs PR #${prNumber} exists but is neither open nor merged (state=${data.state}, merged=${data.merged}).`); + } + result-encoding: string + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: All good + run: echo "Documentation requirement satisfied ✅" From 82937ba1845fcdae8ee4d658472dd6cc3259a1be Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:16:48 +0200 Subject: [PATCH 209/213] [client] Increase logout timeout (#4311) --- client/cmd/logout.go | 2 +- shared/management/client/grpc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/cmd/logout.go b/client/cmd/logout.go index 31fd13e49..5e04a8c3a 100644 --- a/client/cmd/logout.go +++ b/client/cmd/logout.go @@ -20,7 +20,7 @@ var logoutCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) + ctx, cancel := context.WithTimeout(cmd.Context(), time.Second*15) defer cancel() conn, err := DialClientGRPCServer(ctx, daemonAddr) diff --git a/shared/management/client/grpc.go b/shared/management/client/grpc.go index 7e3004945..dc26253e9 100644 --- a/shared/management/client/grpc.go +++ b/shared/management/client/grpc.go @@ -503,7 +503,7 @@ func (c *GrpcClient) Logout() error { return fmt.Errorf("get server public key: %w", err) } - mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*5) + mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*15) defer cancel() message := &proto.Empty{} From 9acf7f926250ca61f90cb7be5a93b05a7caf250f Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 8 Aug 2025 16:18:58 -0300 Subject: [PATCH 210/213] [client] Update Windows installer description (#4306) * [client] Update Windows installer description * Update netbird.wxs --- client/installer.nsis | 2 +- client/netbird.wxs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/installer.nsis b/client/installer.nsis index 41907030e..96d60a785 100644 --- a/client/installer.nsis +++ b/client/installer.nsis @@ -3,7 +3,7 @@ !define WEB_SITE "Netbird.io" !define VERSION $%APPVER% !define COPYRIGHT "Netbird Authors, 2022" -!define DESCRIPTION "A WireGuard®-based mesh network that connects your devices into a single private network" +!define DESCRIPTION "Connect your devices into a secure WireGuard-based overlay network with SSO, MFA, and granular access controls." !define INSTALLER_NAME "netbird-installer.exe" !define MAIN_APP_EXE "Netbird" !define ICON "ui\\assets\\netbird.ico" diff --git a/client/netbird.wxs b/client/netbird.wxs index 230521d3a..ba827debf 100644 --- a/client/netbird.wxs +++ b/client/netbird.wxs @@ -30,7 +30,7 @@ Id="NetBirdService" Name="NetBird" DisplayName="NetBird" - Description="A WireGuard-based mesh network that connects your devices into a single private network." + Description="Connect your devices into a secure WireGuard-based overlay network with SSO, MFA and granular access controls." Start="auto" Type="ownProcess" ErrorControl="normal" Account="LocalSystem" From 375fcf27524d10ca354a0b7f0cc02567d6f4f624 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 8 Aug 2025 16:41:33 -0300 Subject: [PATCH 211/213] [misc] Post release to forum (#4312) --- .github/workflows/forum.yml | 18 ++++++++++++++++++ .github/workflows/release.yml | 14 -------------- 2 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/forum.yml diff --git a/.github/workflows/forum.yml b/.github/workflows/forum.yml new file mode 100644 index 000000000..a26a72586 --- /dev/null +++ b/.github/workflows/forum.yml @@ -0,0 +1,18 @@ +name: Post release topic on Discourse + +on: + release: + types: [published] + +jobs: + post: + runs-on: ubuntu-latest + steps: + - uses: roots/discourse-topic-github-release-action@main + with: + discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }} + discourse-base-url: https://forum.netbird.io + discourse-author-username: NetBird + discourse-category: 17 + discourse-tags: + releases diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ec18731a..7be52259b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -243,17 +243,3 @@ jobs: ref: ${{ env.SIGN_PIPE_VER }} token: ${{ secrets.SIGN_GITHUB_TOKEN }} inputs: '{ "tag": "${{ github.ref }}", "skipRelease": false }' - - post_on_forum: - runs-on: ubuntu-latest - continue-on-error: true - needs: [trigger_signer] - steps: - - uses: Codixer/discourse-topic-github-release-action@v2.0.1 - with: - discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }} - discourse-base-url: https://forum.netbird.io - discourse-author-username: NetBird - discourse-category: 17 - discourse-tags: - releases From 1022a5015c96508724b1d1155b95aa8ab928b56b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:57:21 +0200 Subject: [PATCH 212/213] [client] Eliminate upstream server strings in dns code (#4267) --- client/android/client.go | 7 +- client/android/dns_list.go | 27 ++++-- client/android/dns_list_test.go | 16 +++- client/internal/connect.go | 3 +- client/internal/dns/file_parser_unix.go | 38 ++------ client/internal/dns/file_parser_unix_test.go | 95 ++------------------ client/internal/dns/file_repair_unix.go | 2 +- client/internal/dns/file_unix.go | 25 ++---- client/internal/dns/host_darwin.go | 4 +- client/internal/dns/host_unix.go | 5 +- client/internal/dns/host_windows.go | 2 +- client/internal/dns/hosts_dns_holder.go | 37 ++------ client/internal/dns/mock_server.go | 2 +- client/internal/dns/network_manager_unix.go | 2 +- client/internal/dns/resolvconf_unix.go | 6 +- client/internal/dns/server.go | 43 +++------ client/internal/dns/server_test.go | 68 ++------------ client/internal/dns/service.go | 2 +- client/internal/dns/service_listener.go | 6 +- client/internal/dns/service_memory.go | 2 +- client/internal/dns/systemd_linux.go | 2 +- client/internal/dns/unclean_shutdown_unix.go | 2 +- client/internal/dns/upstream.go | 36 +++++--- client/internal/dns/upstream_android.go | 4 +- client/internal/dns/upstream_ios.go | 2 + client/internal/dns/upstream_test.go | 12 ++- client/internal/mobile_dependency.go | 4 +- client/internal/peer/status.go | 2 +- client/server/server.go | 8 +- dns/nameserver.go | 5 ++ 30 files changed, 161 insertions(+), 308 deletions(-) diff --git a/client/android/client.go b/client/android/client.go index 6924d333c..c05246569 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -4,6 +4,7 @@ package android import ( "context" + "slices" "sync" log "github.com/sirupsen/logrus" @@ -112,7 +113,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder) - return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) + return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener) } // RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot). @@ -138,7 +139,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder) - return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) + return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener) } // Stop the internal client and free the resources @@ -235,7 +236,7 @@ func (c *Client) OnUpdatedHostDNS(list *DNSList) error { return err } - dnsServer.OnUpdatedHostDNSServer(list.items) + dnsServer.OnUpdatedHostDNSServer(slices.Clone(list.items)) return nil } diff --git a/client/android/dns_list.go b/client/android/dns_list.go index 76b922220..4c3dff4cc 100644 --- a/client/android/dns_list.go +++ b/client/android/dns_list.go @@ -1,23 +1,34 @@ package android -import "fmt" +import ( + "fmt" + "net/netip" -// DNSList is a wrapper of []string + "github.com/netbirdio/netbird/client/internal/dns" +) + +// DNSList is a wrapper of []netip.AddrPort with default DNS port type DNSList struct { - items []string + items []netip.AddrPort } -// Add new DNS address to the collection -func (array *DNSList) Add(s string) { - array.items = append(array.items, s) +// Add new DNS address to the collection, returns error if invalid +func (array *DNSList) Add(s string) error { + addr, err := netip.ParseAddr(s) + if err != nil { + return fmt.Errorf("invalid DNS address: %s", s) + } + addrPort := netip.AddrPortFrom(addr.Unmap(), dns.DefaultPort) + array.items = append(array.items, addrPort) + return nil } -// Get return an element of the collection +// Get return an element of the collection as string func (array *DNSList) Get(i int) (string, error) { if i >= len(array.items) || i < 0 { return "", fmt.Errorf("out of range") } - return array.items[i], nil + return array.items[i].Addr().String(), nil } // Size return with the size of the collection diff --git a/client/android/dns_list_test.go b/client/android/dns_list_test.go index 93aea78a8..7cb7b33a1 100644 --- a/client/android/dns_list_test.go +++ b/client/android/dns_list_test.go @@ -3,20 +3,30 @@ package android import "testing" func TestDNSList_Get(t *testing.T) { - l := DNSList{ - items: make([]string, 1), + l := DNSList{} + + // Add a valid DNS address + err := l.Add("8.8.8.8") + if err != nil { + t.Errorf("unexpected error: %s", err) } - _, err := l.Get(0) + // Test getting valid index + addr, err := l.Get(0) if err != nil { t.Errorf("invalid error: %s", err) } + if addr != "8.8.8.8" { + t.Errorf("expected 8.8.8.8, got %s", addr) + } + // Test negative index _, err = l.Get(-1) if err == nil { t.Errorf("expected error but got nil") } + // Test out of bounds index _, err = l.Get(1) if err == nil { t.Errorf("expected error but got nil") diff --git a/client/internal/connect.go b/client/internal/connect.go index d6be285f9..523dcaf1f 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "net/netip" "runtime" "runtime/debug" "strings" @@ -70,7 +71,7 @@ func (c *ConnectClient) RunOnAndroid( tunAdapter device.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, networkChangeListener listener.NetworkChangeListener, - dnsAddresses []string, + dnsAddresses []netip.AddrPort, dnsReadyListener dns.ReadyListener, ) error { // in case of non Android os these variables will be nil diff --git a/client/internal/dns/file_parser_unix.go b/client/internal/dns/file_parser_unix.go index 6e123c94e..8dacb4e51 100644 --- a/client/internal/dns/file_parser_unix.go +++ b/client/internal/dns/file_parser_unix.go @@ -16,7 +16,7 @@ const ( ) type resolvConf struct { - nameServers []string + nameServers []netip.Addr searchDomains []string others []string } @@ -36,7 +36,7 @@ func parseBackupResolvConf() (*resolvConf, error) { func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) { rconf := &resolvConf{ searchDomains: make([]string, 0), - nameServers: make([]string, 0), + nameServers: make([]netip.Addr, 0), others: make([]string, 0), } @@ -94,7 +94,11 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) { if len(splitLines) != 2 { continue } - rconf.nameServers = append(rconf.nameServers, splitLines[1]) + if addr, err := netip.ParseAddr(splitLines[1]); err == nil { + rconf.nameServers = append(rconf.nameServers, addr.Unmap()) + } else { + log.Warnf("invalid nameserver address in resolv.conf: %s, skipping", splitLines[1]) + } continue } @@ -104,31 +108,3 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) { } return rconf, nil } - -// removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position -// and writes the file back to the original location -func removeFirstNbNameserver(filename string, nameserverIP netip.Addr) error { - resolvConf, err := parseResolvConfFile(filename) - if err != nil { - return fmt.Errorf("parse backup resolv.conf: %w", err) - } - content, err := os.ReadFile(filename) - if err != nil { - return fmt.Errorf("read %s: %w", filename, err) - } - - if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP.String() { - newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1) - - stat, err := os.Stat(filename) - if err != nil { - return fmt.Errorf("stat %s: %w", filename, err) - } - if err := os.WriteFile(filename, []byte(newContent), stat.Mode()); err != nil { - return fmt.Errorf("write %s: %w", filename, err) - } - - } - - return nil -} diff --git a/client/internal/dns/file_parser_unix_test.go b/client/internal/dns/file_parser_unix_test.go index 228a708f1..31e0dd5a0 100644 --- a/client/internal/dns/file_parser_unix_test.go +++ b/client/internal/dns/file_parser_unix_test.go @@ -3,13 +3,9 @@ package dns import ( - "net/netip" "os" "path/filepath" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_parseResolvConf(t *testing.T) { @@ -99,9 +95,13 @@ options debug t.Errorf("invalid parse result for search domains, expected: %v, got: %v", testCase.expectedSearch, cfg.searchDomains) } - ok = compareLists(cfg.nameServers, testCase.expectedNS) + nsStrings := make([]string, len(cfg.nameServers)) + for i, ns := range cfg.nameServers { + nsStrings[i] = ns.String() + } + ok = compareLists(nsStrings, testCase.expectedNS) if !ok { - t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, cfg.nameServers) + t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, nsStrings) } ok = compareLists(cfg.others, testCase.expectedOther) @@ -177,86 +177,3 @@ nameserver 192.168.0.1 } } -func TestRemoveFirstNbNameserver(t *testing.T) { - testCases := []struct { - name string - content string - ipToRemove string - expected string - }{ - { - name: "Unrelated nameservers with comments and options", - content: `# This is a comment -options rotate -nameserver 1.1.1.1 -# Another comment -nameserver 8.8.4.4 -search example.com`, - ipToRemove: "9.9.9.9", - expected: `# This is a comment -options rotate -nameserver 1.1.1.1 -# Another comment -nameserver 8.8.4.4 -search example.com`, - }, - { - name: "First nameserver matches", - content: `search example.com -nameserver 9.9.9.9 -# oof, a comment -nameserver 8.8.4.4 -options attempts:5`, - ipToRemove: "9.9.9.9", - expected: `search example.com -# oof, a comment -nameserver 8.8.4.4 -options attempts:5`, - }, - { - name: "Target IP not the first nameserver", - // nolint:dupword - content: `# Comment about the first nameserver -nameserver 8.8.4.4 -# Comment before our target -nameserver 9.9.9.9 -options timeout:2`, - ipToRemove: "9.9.9.9", - // nolint:dupword - expected: `# Comment about the first nameserver -nameserver 8.8.4.4 -# Comment before our target -nameserver 9.9.9.9 -options timeout:2`, - }, - { - name: "Only nameserver matches", - content: `options debug -nameserver 9.9.9.9 -search localdomain`, - ipToRemove: "9.9.9.9", - expected: `options debug -nameserver 9.9.9.9 -search localdomain`, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - tempDir := t.TempDir() - tempFile := filepath.Join(tempDir, "resolv.conf") - err := os.WriteFile(tempFile, []byte(tc.content), 0644) - assert.NoError(t, err) - - ip, err := netip.ParseAddr(tc.ipToRemove) - require.NoError(t, err, "Failed to parse IP address") - err = removeFirstNbNameserver(tempFile, ip) - assert.NoError(t, err) - - content, err := os.ReadFile(tempFile) - assert.NoError(t, err) - - assert.Equal(t, tc.expected, string(content), "The resulting content should match the expected output.") - }) - } -} diff --git a/client/internal/dns/file_repair_unix.go b/client/internal/dns/file_repair_unix.go index 75af411df..0846dbf38 100644 --- a/client/internal/dns/file_repair_unix.go +++ b/client/internal/dns/file_repair_unix.go @@ -146,7 +146,7 @@ func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP netip.Addr, rCon return true } - if rConf.nameServers[0] != nbNameserverIP.String() { + if rConf.nameServers[0] != nbNameserverIP { return true } diff --git a/client/internal/dns/file_unix.go b/client/internal/dns/file_unix.go index 423989f72..45e621443 100644 --- a/client/internal/dns/file_unix.go +++ b/client/internal/dns/file_unix.go @@ -29,7 +29,7 @@ type fileConfigurator struct { repair *repair originalPerms os.FileMode nbNameserverIP netip.Addr - originalNameservers []string + originalNameservers []netip.Addr } func newFileConfigurator() (*fileConfigurator, error) { @@ -70,7 +70,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st } // getOriginalNameservers returns the nameservers that were found in the original resolv.conf -func (f *fileConfigurator) getOriginalNameservers() []string { +func (f *fileConfigurator) getOriginalNameservers() []netip.Addr { return f.originalNameservers } @@ -128,20 +128,14 @@ func (f *fileConfigurator) backup() error { } func (f *fileConfigurator) restore() error { - err := removeFirstNbNameserver(fileDefaultResolvConfBackupLocation, f.nbNameserverIP) - if err != nil { - log.Errorf("Failed to remove netbird nameserver from %s on backup restore: %s", fileDefaultResolvConfBackupLocation, err) - } - - err = copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath) - if err != nil { + if err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath); err != nil { return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err) } return os.RemoveAll(fileDefaultResolvConfBackupLocation) } -func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error { +func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress netip.Addr) error { resolvConf, err := parseDefaultResolvConf() if err != nil { return fmt.Errorf("parse current resolv.conf: %w", err) @@ -152,16 +146,9 @@ func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Add return restoreResolvConfFile() } - currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0]) - // not a valid first nameserver -> restore - if err != nil { - log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[0], err) - return restoreResolvConfFile() - } - // current address is still netbird's non-available dns address -> restore - // comparing parsed addresses only, to remove ambiguity - if currentDNSAddress.String() == storedDNSAddress.String() { + currentDNSAddress := resolvConf.nameServers[0] + if currentDNSAddress == storedDNSAddress { return restoreResolvConfFile() } diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index 820cf9029..852dfef48 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -239,7 +239,7 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) { } else if inServerAddressesArray { address := strings.Split(line, " : ")[1] if ip, err := netip.ParseAddr(address); err == nil && ip.Is4() { - dnsSettings.ServerIP = ip + dnsSettings.ServerIP = ip.Unmap() inServerAddressesArray = false // Stop reading after finding the first IPv4 address } } @@ -250,7 +250,7 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) { } // default to 53 port - dnsSettings.ServerPort = defaultPort + dnsSettings.ServerPort = DefaultPort return dnsSettings, nil } diff --git a/client/internal/dns/host_unix.go b/client/internal/dns/host_unix.go index 297d50822..422fed4e5 100644 --- a/client/internal/dns/host_unix.go +++ b/client/internal/dns/host_unix.go @@ -42,7 +42,7 @@ func (t osManagerType) String() string { type restoreHostManager interface { hostManager - restoreUncleanShutdownDNS(*netip.Addr) error + restoreUncleanShutdownDNS(netip.Addr) error } func newHostManager(wgInterface string) (hostManager, error) { @@ -130,8 +130,9 @@ func checkStub() bool { return true } + systemdResolvedAddr := netip.AddrFrom4([4]byte{127, 0, 0, 53}) // 127.0.0.53 for _, ns := range rConf.nameServers { - if ns == "127.0.0.53" { + if ns == systemdResolvedAddr { return true } } diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 648a58207..829d83a04 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -216,7 +216,7 @@ func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error { return fmt.Errorf("adding dns setup for all failed: %w", err) } r.routingAll = true - log.Infof("configured %s:53 as main DNS forwarder for this peer", ip) + log.Infof("configured %s:%d as main DNS forwarder for this peer", ip, DefaultPort) return nil } diff --git a/client/internal/dns/hosts_dns_holder.go b/client/internal/dns/hosts_dns_holder.go index 2601af9c8..980d917a7 100644 --- a/client/internal/dns/hosts_dns_holder.go +++ b/client/internal/dns/hosts_dns_holder.go @@ -1,38 +1,31 @@ package dns import ( - "fmt" "net/netip" "sync" - - log "github.com/sirupsen/logrus" ) type hostsDNSHolder struct { - unprotectedDNSList map[string]struct{} + unprotectedDNSList map[netip.AddrPort]struct{} mutex sync.RWMutex } func newHostsDNSHolder() *hostsDNSHolder { return &hostsDNSHolder{ - unprotectedDNSList: make(map[string]struct{}), + unprotectedDNSList: make(map[netip.AddrPort]struct{}), } } -func (h *hostsDNSHolder) set(list []string) { +func (h *hostsDNSHolder) set(list []netip.AddrPort) { h.mutex.Lock() - h.unprotectedDNSList = make(map[string]struct{}) - for _, dns := range list { - dnsAddr, err := h.normalizeAddress(dns) - if err != nil { - continue - } - h.unprotectedDNSList[dnsAddr] = struct{}{} + h.unprotectedDNSList = make(map[netip.AddrPort]struct{}) + for _, addrPort := range list { + h.unprotectedDNSList[addrPort] = struct{}{} } h.mutex.Unlock() } -func (h *hostsDNSHolder) get() map[string]struct{} { +func (h *hostsDNSHolder) get() map[netip.AddrPort]struct{} { h.mutex.RLock() l := h.unprotectedDNSList h.mutex.RUnlock() @@ -40,24 +33,10 @@ func (h *hostsDNSHolder) get() map[string]struct{} { } //nolint:unused -func (h *hostsDNSHolder) isContain(upstream string) bool { +func (h *hostsDNSHolder) contains(upstream netip.AddrPort) bool { h.mutex.RLock() defer h.mutex.RUnlock() _, ok := h.unprotectedDNSList[upstream] return ok } - -func (h *hostsDNSHolder) normalizeAddress(addr string) (string, error) { - a, err := netip.ParseAddr(addr) - if err != nil { - log.Errorf("invalid upstream IP address: %s, error: %s", addr, err) - return "", err - } - - if a.Is4() { - return fmt.Sprintf("%s:53", addr), nil - } else { - return fmt.Sprintf("[%s]:53", addr), nil - } -} diff --git a/client/internal/dns/mock_server.go b/client/internal/dns/mock_server.go index 9e0d06399..d160fa99a 100644 --- a/client/internal/dns/mock_server.go +++ b/client/internal/dns/mock_server.go @@ -50,7 +50,7 @@ func (m *MockServer) DnsIP() netip.Addr { return netip.MustParseAddr("100.10.254.255") } -func (m *MockServer) OnUpdatedHostDNSServer(strings []string) { +func (m *MockServer) OnUpdatedHostDNSServer(addrs []netip.AddrPort) { // TODO implement me panic("implement me") } diff --git a/client/internal/dns/network_manager_unix.go b/client/internal/dns/network_manager_unix.go index 5459bc2d7..e4ccc8cbd 100644 --- a/client/internal/dns/network_manager_unix.go +++ b/client/internal/dns/network_manager_unix.go @@ -245,7 +245,7 @@ func (n *networkManagerDbusConfigurator) deleteConnectionSettings() error { return nil } -func (n *networkManagerDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error { +func (n *networkManagerDbusConfigurator) restoreUncleanShutdownDNS(netip.Addr) error { if err := n.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns via network-manager: %w", err) } diff --git a/client/internal/dns/resolvconf_unix.go b/client/internal/dns/resolvconf_unix.go index 6080c1d2c..8cdea562b 100644 --- a/client/internal/dns/resolvconf_unix.go +++ b/client/internal/dns/resolvconf_unix.go @@ -40,7 +40,7 @@ type resolvconf struct { implType resolvconfType originalSearchDomains []string - originalNameServers []string + originalNameServers []netip.Addr othersConfigs []string } @@ -110,7 +110,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman return nil } -func (r *resolvconf) getOriginalNameservers() []string { +func (r *resolvconf) getOriginalNameservers() []netip.Addr { return r.originalNameServers } @@ -158,7 +158,7 @@ func (r *resolvconf) applyConfig(content bytes.Buffer) error { return nil } -func (r *resolvconf) restoreUncleanShutdownDNS(*netip.Addr) error { +func (r *resolvconf) restoreUncleanShutdownDNS(netip.Addr) error { if err := r.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns for interface %s: %w", r.ifaceName, err) } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index cd0757dc1..ce75369c8 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -42,7 +42,7 @@ type Server interface { Stop() DnsIP() netip.Addr UpdateDNSServer(serial uint64, update nbdns.Config) error - OnUpdatedHostDNSServer(strings []string) + OnUpdatedHostDNSServer(addrs []netip.AddrPort) SearchDomains() []string ProbeAvailability() } @@ -55,7 +55,7 @@ type nsGroupsByDomain struct { // hostManagerWithOriginalNS extends the basic hostManager interface type hostManagerWithOriginalNS interface { hostManager - getOriginalNameservers() []string + getOriginalNameservers() []netip.Addr } // DefaultServer dns server object @@ -136,7 +136,7 @@ func NewDefaultServer( func NewDefaultServerPermanentUpstream( ctx context.Context, wgInterface WGIface, - hostsDnsList []string, + hostsDnsList []netip.AddrPort, config nbdns.Config, listener listener.NetworkChangeListener, statusRecorder *peer.Status, @@ -144,6 +144,7 @@ func NewDefaultServerPermanentUpstream( ) *DefaultServer { log.Debugf("host dns address list is: %v", hostsDnsList) ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder, nil, disableSys) + ds.hostsDNSHolder.set(hostsDnsList) ds.permanent = true ds.addHostRootZone() @@ -340,7 +341,7 @@ func (s *DefaultServer) disableDNS() error { // OnUpdatedHostDNSServer update the DNS servers addresses for root zones // It will be applied if the mgm server do not enforce DNS settings for root zone -func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { +func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []netip.AddrPort) { s.hostsDNSHolder.set(hostsDnsList) // Check if there's any root handler @@ -461,7 +462,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort()) - if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() { + if s.service.RuntimePort() != DefaultPort && !s.hostManager.supportCustomPort() { log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " + "Learn more at: https://docs.netbird.io/how-to/manage-dns-in-your-network#local-resolver") s.currentConfig.RouteAll = false @@ -581,14 +582,13 @@ func (s *DefaultServer) registerFallback(config HostDNSConfig) { } for _, ns := range originalNameservers { - if ns == config.ServerIP.String() { + if ns == config.ServerIP { log.Debugf("skipping original nameserver %s as it is the same as the server IP %s", ns, config.ServerIP) continue } - ns = formatAddr(ns, defaultPort) - - handler.upstreamServers = append(handler.upstreamServers, ns) + addrPort := netip.AddrPortFrom(ns, DefaultPort) + handler.upstreamServers = append(handler.upstreamServers, addrPort) } handler.deactivate = func(error) { /* always active */ } handler.reactivate = func() { /* always active */ } @@ -695,7 +695,7 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String()) continue } - handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns)) + handler.upstreamServers = append(handler.upstreamServers, ns.AddrPort()) } if len(handler.upstreamServers) == 0 { @@ -770,18 +770,6 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) { s.dnsMuxMap = muxUpdateMap } -func getNSHostPort(ns nbdns.NameServer) string { - return formatAddr(ns.IP.String(), ns.Port) -} - -// formatAddr formats a nameserver address with port, handling IPv6 addresses properly -func formatAddr(address string, port int) string { - if ip, err := netip.ParseAddr(address); err == nil && ip.Is6() { - return fmt.Sprintf("[%s]:%d", address, port) - } - return fmt.Sprintf("%s:%d", address, port) -} - // upstreamCallbacks returns two functions, the first one is used to deactivate // the upstream resolver from the configuration, the second one is used to // reactivate it. Not allowed to call reactivate before deactivate. @@ -879,10 +867,7 @@ func (s *DefaultServer) addHostRootZone() { return } - handler.upstreamServers = make([]string, 0) - for k := range hostDNSServers { - handler.upstreamServers = append(handler.upstreamServers, k) - } + handler.upstreamServers = maps.Keys(hostDNSServers) handler.deactivate = func(error) {} handler.reactivate = func() {} @@ -893,9 +878,9 @@ func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) { var states []peer.NSGroupState for _, group := range groups { - var servers []string + var servers []netip.AddrPort for _, ns := range group.NameServers { - servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port)) + servers = append(servers, ns.AddrPort()) } state := peer.NSGroupState{ @@ -927,7 +912,7 @@ func (s *DefaultServer) updateNSState(nsGroup *nbdns.NameServerGroup, err error, func generateGroupKey(nsGroup *nbdns.NameServerGroup) string { var servers []string for _, ns := range nsGroup.NameServers { - servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port)) + servers = append(servers, ns.AddrPort().String()) } return fmt.Sprintf("%v_%v", servers, nsGroup.Domains) } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 3ac1b6eb1..91543da8f 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -97,9 +97,9 @@ func init() { } func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase { - var srvs []string + var srvs []netip.AddrPort for _, srv := range servers { - srvs = append(srvs, getNSHostPort(srv)) + srvs = append(srvs, srv.AddrPort()) } return &upstreamResolverBase{ domain: domain, @@ -705,7 +705,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { } defer wgIFace.Close() - var dnsList []string + var dnsList []netip.AddrPort dnsConfig := nbdns.Config{} dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() @@ -715,7 +715,8 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { } defer dnsServer.Stop() - dnsServer.OnUpdatedHostDNSServer([]string{"8.8.8.8"}) + addrPort := netip.MustParseAddrPort("8.8.8.8:53") + dnsServer.OnUpdatedHostDNSServer([]netip.AddrPort{addrPort}) resolver := newDnsResolver(dnsServer.service.RuntimeIP(), dnsServer.service.RuntimePort()) _, err = resolver.LookupHost(context.Background(), "netbird.io") @@ -731,7 +732,8 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) + addrPort := netip.MustParseAddrPort("8.8.8.8:53") + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []netip.AddrPort{addrPort}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -823,7 +825,8 @@ func TestDNSPermanent_matchOnly(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) + addrPort := netip.MustParseAddrPort("8.8.8.8:53") + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []netip.AddrPort{addrPort}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -2053,56 +2056,3 @@ func TestLocalResolverPriorityConstants(t *testing.T) { assert.Equal(t, PriorityLocal, localMuxUpdates[0].priority, "Local handler should use PriorityLocal") assert.Equal(t, "local.example.com", localMuxUpdates[0].domain) } - -func TestFormatAddr(t *testing.T) { - tests := []struct { - name string - address string - port int - expected string - }{ - { - name: "IPv4 address", - address: "8.8.8.8", - port: 53, - expected: "8.8.8.8:53", - }, - { - name: "IPv4 address with custom port", - address: "1.1.1.1", - port: 5353, - expected: "1.1.1.1:5353", - }, - { - name: "IPv6 address", - address: "fd78:94bf:7df8::1", - port: 53, - expected: "[fd78:94bf:7df8::1]:53", - }, - { - name: "IPv6 address with custom port", - address: "2001:db8::1", - port: 5353, - expected: "[2001:db8::1]:5353", - }, - { - name: "IPv6 localhost", - address: "::1", - port: 53, - expected: "[::1]:53", - }, - { - name: "Invalid address treated as hostname", - address: "dns.example.com", - port: 53, - expected: "dns.example.com:53", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := formatAddr(tt.address, tt.port) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/client/internal/dns/service.go b/client/internal/dns/service.go index ab8238a61..6a76c53e3 100644 --- a/client/internal/dns/service.go +++ b/client/internal/dns/service.go @@ -7,7 +7,7 @@ import ( ) const ( - defaultPort = 53 + DefaultPort = 53 ) type service interface { diff --git a/client/internal/dns/service_listener.go b/client/internal/dns/service_listener.go index abd2f4f05..806559444 100644 --- a/client/internal/dns/service_listener.go +++ b/client/internal/dns/service_listener.go @@ -122,7 +122,7 @@ func (s *serviceViaListener) RuntimePort() int { defer s.listenerFlagLock.Unlock() if s.ebpfService != nil { - return defaultPort + return DefaultPort } else { return int(s.listenPort) } @@ -148,9 +148,9 @@ func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) { return s.customAddr.Addr(), s.customAddr.Port(), nil } - ip, ok := s.testFreePort(defaultPort) + ip, ok := s.testFreePort(DefaultPort) if ok { - return ip, defaultPort, nil + return ip, DefaultPort, nil } ebpfSrv, port, ok := s.tryToUseeBPF() diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 9f55838bf..89d637686 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -33,7 +33,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory { dnsMux: dns.NewServeMux(), runtimeIP: lastIP, - runtimePort: defaultPort, + runtimePort: DefaultPort, } return s } diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index a58747d5b..0e8a53a63 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -235,7 +235,7 @@ func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error return nil } -func (s *systemdDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error { +func (s *systemdDbusConfigurator) restoreUncleanShutdownDNS(netip.Addr) error { if err := s.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns via systemd: %w", err) } diff --git a/client/internal/dns/unclean_shutdown_unix.go b/client/internal/dns/unclean_shutdown_unix.go index 2e786f484..dc44aefaf 100644 --- a/client/internal/dns/unclean_shutdown_unix.go +++ b/client/internal/dns/unclean_shutdown_unix.go @@ -27,7 +27,7 @@ func (s *ShutdownState) Cleanup() error { return fmt.Errorf("create previous host manager: %w", err) } - if err := manager.restoreUncleanShutdownDNS(&s.DNSAddress); err != nil { + if err := manager.restoreUncleanShutdownDNS(s.DNSAddress); err != nil { return fmt.Errorf("restore unclean shutdown dns: %w", err) } diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index c44d36599..f5d0e775f 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net" + "net/netip" "slices" "strings" "sync" @@ -48,7 +49,7 @@ type upstreamResolverBase struct { ctx context.Context cancel context.CancelFunc upstreamClient upstreamClient - upstreamServers []string + upstreamServers []netip.AddrPort domain string disabled bool failsCount atomic.Int32 @@ -79,17 +80,20 @@ func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, d // String returns a string representation of the upstream resolver func (u *upstreamResolverBase) String() string { - return fmt.Sprintf("upstream %v", u.upstreamServers) + return fmt.Sprintf("upstream %s", u.upstreamServers) } // ID returns the unique handler ID func (u *upstreamResolverBase) ID() types.HandlerID { servers := slices.Clone(u.upstreamServers) - slices.Sort(servers) + slices.SortFunc(servers, func(a, b netip.AddrPort) int { return a.Compare(b) }) hash := sha256.New() hash.Write([]byte(u.domain + ":")) - hash.Write([]byte(strings.Join(servers, ","))) + for _, s := range servers { + hash.Write([]byte(s.String())) + hash.Write([]byte("|")) + } return types.HandlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8])) } @@ -130,7 +134,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { func() { ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout) defer cancel() - rm, t, err = u.upstreamClient.exchange(ctx, upstream, r) + rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), r) }() if err != nil { @@ -197,7 +201,7 @@ func (u *upstreamResolverBase) checkUpstreamFails(err error) { proto.SystemEvent_DNS, "All upstream servers failed (fail count exceeded)", "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", - map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, + map[string]string{"upstreams": u.upstreamServersString()}, // TODO add domain meta ) } @@ -258,7 +262,7 @@ func (u *upstreamResolverBase) ProbeAvailability() { proto.SystemEvent_DNS, "All upstream servers failed (probe failed)", "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", - map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, + map[string]string{"upstreams": u.upstreamServersString()}, ) } } @@ -278,7 +282,7 @@ func (u *upstreamResolverBase) waitUntilResponse() { operation := func() error { select { case <-u.ctx.Done(): - return backoff.Permanent(fmt.Errorf("exiting upstream retry loop for upstreams %s: parent context has been canceled", u.upstreamServers)) + return backoff.Permanent(fmt.Errorf("exiting upstream retry loop for upstreams %s: parent context has been canceled", u.upstreamServersString())) default: } @@ -291,7 +295,7 @@ func (u *upstreamResolverBase) waitUntilResponse() { } } - log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServers, exponentialBackOff.NextBackOff()) + log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServersString(), exponentialBackOff.NextBackOff()) return fmt.Errorf("upstream check call error") } @@ -301,7 +305,7 @@ func (u *upstreamResolverBase) waitUntilResponse() { return } - log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServers) + log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServersString()) u.failsCount.Store(0) u.successCount.Add(1) u.reactivate() @@ -331,13 +335,21 @@ func (u *upstreamResolverBase) disable(err error) { go u.waitUntilResponse() } -func (u *upstreamResolverBase) testNameserver(server string, timeout time.Duration) error { +func (u *upstreamResolverBase) upstreamServersString() string { + var servers []string + for _, server := range u.upstreamServers { + servers = append(servers, server.String()) + } + return strings.Join(servers, ", ") +} + +func (u *upstreamResolverBase) testNameserver(server netip.AddrPort, timeout time.Duration) error { ctx, cancel := context.WithTimeout(u.ctx, timeout) defer cancel() r := new(dns.Msg).SetQuestion(testRecord, dns.TypeSOA) - _, _, err := u.upstreamClient.exchange(ctx, server, r) + _, _, err := u.upstreamClient.exchange(ctx, server.String(), r) return err } diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go index e7db581b1..ddbf84ae4 100644 --- a/client/internal/dns/upstream_android.go +++ b/client/internal/dns/upstream_android.go @@ -79,8 +79,8 @@ func (u *upstreamResolver) exchangeWithoutVPN(ctx context.Context, upstream stri } func (u *upstreamResolver) isLocalResolver(upstream string) bool { - if u.hostsDNSHolder.isContain(upstream) { - return true + if addrPort, err := netip.ParseAddrPort(upstream); err == nil { + return u.hostsDNSHolder.contains(addrPort) } return false } diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index 648cab176..96b8bbb0f 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -62,6 +62,8 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r * upstreamIP, err := netip.ParseAddr(upstreamHost) if err != nil { log.Warnf("failed to parse upstream host %s: %s", upstreamHost, err) + } else { + upstreamIP = upstreamIP.Unmap() } if u.lNet.Contains(upstreamIP) || upstreamIP.IsPrivate() { log.Debugf("using private client to query upstream: %s", upstream) diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go index e440995d9..51d870e2a 100644 --- a/client/internal/dns/upstream_test.go +++ b/client/internal/dns/upstream_test.go @@ -59,7 +59,14 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) resolver, _ := newUpstreamResolver(ctx, "", netip.Addr{}, netip.Prefix{}, nil, nil, ".") - resolver.upstreamServers = testCase.InputServers + // Convert test servers to netip.AddrPort + var servers []netip.AddrPort + for _, server := range testCase.InputServers { + if addrPort, err := netip.ParseAddrPort(server); err == nil { + servers = append(servers, netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())) + } + } + resolver.upstreamServers = servers resolver.upstreamTimeout = testCase.timeout if testCase.cancelCTX { cancel() @@ -128,7 +135,8 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) { reactivatePeriod: reactivatePeriod, failsTillDeact: failsTillDeact, } - resolver.upstreamServers = []string{"0.0.0.0:-1"} + addrPort, _ := netip.ParseAddrPort("0.0.0.0:1") // Use valid port for parsing, test will still fail on connection + resolver.upstreamServers = []netip.AddrPort{netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())} resolver.failsTillDeact = 0 resolver.reactivatePeriod = time.Microsecond * 100 diff --git a/client/internal/mobile_dependency.go b/client/internal/mobile_dependency.go index 4ac0fc141..7c95e2b99 100644 --- a/client/internal/mobile_dependency.go +++ b/client/internal/mobile_dependency.go @@ -1,6 +1,8 @@ package internal import ( + "net/netip" + "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" @@ -13,7 +15,7 @@ type MobileDependency struct { TunAdapter device.TunAdapter IFaceDiscover stdnet.ExternalIFaceDiscover NetworkChangeListener listener.NetworkChangeListener - HostDNSAddresses []string + HostDNSAddresses []netip.AddrPort DnsReadyListener dns.ReadyListener // iOS only diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index bd4e63357..239cce7e0 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -140,7 +140,7 @@ type RosenpassState struct { // whether it's enabled, and the last error message encountered during probing. type NSGroupState struct { ID string - Servers []string + Servers []netip.AddrPort Domains []string Enabled bool Error error diff --git a/client/server/server.go b/client/server/server.go index fadf2e7fc..daef7d02b 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -1197,8 +1197,14 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { if dnsState.Error != nil { err = dnsState.Error.Error() } + + var servers []string + for _, server := range dnsState.Servers { + servers = append(servers, server.String()) + } + pbDnsState := &proto.NSGroupState{ - Servers: dnsState.Servers, + Servers: servers, Domains: dnsState.Domains, Enabled: dnsState.Enabled, Error: err, diff --git a/dns/nameserver.go b/dns/nameserver.go index bb904b165..81c616c50 100644 --- a/dns/nameserver.go +++ b/dns/nameserver.go @@ -102,6 +102,11 @@ func (n *NameServer) IsEqual(other *NameServer) bool { other.Port == n.Port } +// AddrPort returns the nameserver as a netip.AddrPort +func (n *NameServer) AddrPort() netip.AddrPort { + return netip.AddrPortFrom(n.IP, uint16(n.Port)) +} + // ParseNameServerURL parses a nameserver url in the format ://:, e.g., udp://1.1.1.1:53 func ParseNameServerURL(nsURL string) (NameServer, error) { parsedURL, err := url.Parse(nsURL) From a942e4add57d307061aca4683ea8fd88a3c9545a Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:21:26 +0200 Subject: [PATCH 213/213] [management] use readlock on add peer (#4308) --- management/server/grpcserver.go | 3 +- management/server/peer.go | 2 +- management/server/peer_test.go | 5 +- management/server/store/sql_store.go | 100 +++++++++++++++++++++++---- 4 files changed, 91 insertions(+), 19 deletions(-) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 8893ad2e2..782e46948 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -913,6 +913,7 @@ func (s *GRPCServer) SyncMeta(ctx context.Context, req *proto.EncryptedMessage) func (s *GRPCServer) Logout(ctx context.Context, req *proto.EncryptedMessage) (*proto.Empty, error) { log.WithContext(ctx).Debugf("Logout request from peer [%s]", req.WgPubKey) + start := time.Now() empty := &proto.Empty{} peerKey, err := s.parseRequest(ctx, req, empty) @@ -944,7 +945,7 @@ func (s *GRPCServer) Logout(ctx context.Context, req *proto.EncryptedMessage) (* s.accountManager.BufferUpdateAccountPeers(ctx, peer.AccountID) - log.WithContext(ctx).Infof("peer %s logged out successfully", peerKey.String()) + log.WithContext(ctx).Debugf("peer %s logged out successfully after %s", peerKey.String(), time.Since(start)) return &proto.Empty{}, nil } diff --git a/management/server/peer.go b/management/server/peer.go index d72eac91a..a1f669f4f 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -609,7 +609,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s newPeer.DNSLabel = freeLabel newPeer.IP = freeIP - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) + unlock := am.Store.AcquireReadLockByUID(ctx, accountID) defer func() { if unlock != nil { unlock() diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 5c52692f3..d974e7c21 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1476,8 +1476,9 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { func Test_RegisterPeerRollbackOnFailure(t *testing.T) { engine := os.Getenv("NETBIRD_STORE_ENGINE") - if engine == "sqlite" || engine == "" { - t.Skip("Skipping test because sqlite test store is not respecting foreign keys") + if engine == "sqlite" || engine == "mysql" || engine == "" { + // we intentionally disabled foreign keys in mysql + t.Skip("Skipping test because store is not respecting foreign keys") } if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 1bcae7048..8aa56f7b0 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -24,6 +24,7 @@ import ( "gorm.io/gorm/logger" nbdns "github.com/netbirdio/netbird/dns" + nbcontext "github.com/netbirdio/netbird/management/server/context" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -76,7 +77,12 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met conns = runtime.NumCPU() } - if storeEngine == types.SqliteStoreEngine { + switch storeEngine { + case types.MysqlStoreEngine: + if err := db.Exec("SET GLOBAL FOREIGN_KEY_CHECKS = 0").Error; err != nil { + return nil, err + } + case types.SqliteStoreEngine: if err == nil { log.WithContext(ctx).Warnf("setting NB_SQL_MAX_OPEN_CONNS is not supported for sqlite, using default value 1") } @@ -142,14 +148,16 @@ func (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) { func (s *SqlStore) AcquireWriteLockByUID(ctx context.Context, uniqueID string) (unlock func()) { log.WithContext(ctx).Tracef("acquiring write lock for ID %s", uniqueID) - start := time.Now() + startWait := time.Now() value, _ := s.resourceLocks.LoadOrStore(uniqueID, &sync.RWMutex{}) mtx := value.(*sync.RWMutex) mtx.Lock() + log.WithContext(ctx).Tracef("waiting to acquire write lock for ID %s in %v", uniqueID, time.Since(startWait)) + startHold := time.Now() unlock = func() { mtx.Unlock() - log.WithContext(ctx).Tracef("released write lock for ID %s in %v", uniqueID, time.Since(start)) + log.WithContext(ctx).Tracef("released write lock for ID %s in %v", uniqueID, time.Since(startHold)) } return unlock @@ -159,14 +167,16 @@ func (s *SqlStore) AcquireWriteLockByUID(ctx context.Context, uniqueID string) ( func (s *SqlStore) AcquireReadLockByUID(ctx context.Context, uniqueID string) (unlock func()) { log.WithContext(ctx).Tracef("acquiring read lock for ID %s", uniqueID) - start := time.Now() + startWait := time.Now() value, _ := s.resourceLocks.LoadOrStore(uniqueID, &sync.RWMutex{}) mtx := value.(*sync.RWMutex) mtx.RLock() + log.WithContext(ctx).Tracef("waiting to acquire read lock for ID %s in %v", uniqueID, time.Since(startWait)) + startHold := time.Now() unlock = func() { mtx.RUnlock() - log.WithContext(ctx).Tracef("released read lock for ID %s in %v", uniqueID, time.Since(start)) + log.WithContext(ctx).Tracef("released read lock for ID %s in %v", uniqueID, time.Since(startHold)) } return unlock @@ -604,13 +614,16 @@ func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStren } func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + tx := s.db if lockStrength != LockingStrengthNone { tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) } var user types.User - result := tx.Take(&user, idQueryCondition, userID) + result := tx.WithContext(ctx).Take(&user, idQueryCondition, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewUserNotFoundError(userID) @@ -1076,13 +1089,16 @@ func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength Lock } func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Network, error) { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + tx := s.db if lockStrength != LockingStrengthNone { tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) } var accountNetwork types.AccountNetwork - if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountNetwork).Error; err != nil { + if err := tx.WithContext(ctx).Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountNetwork).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.NewAccountNotFoundError(accountID) } @@ -1092,13 +1108,16 @@ func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingSt } func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + tx := s.db if lockStrength != LockingStrengthNone { tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) } var peer nbpeer.Peer - result := tx.Take(&peer, GetKeyQueryCondition(s), peerKey) + result := tx.WithContext(ctx).Take(&peer, GetKeyQueryCondition(s), peerKey) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -1147,8 +1166,11 @@ func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength Locking // SaveUserLastLogin stores the last login time for a user in DB. func (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + var user types.User - result := s.db.Take(&user, accountAndIDQueryCondition, accountID, userID) + result := s.db.WithContext(ctx).Take(&user, accountAndIDQueryCondition, accountID, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return status.NewUserNotFoundError(userID) @@ -1329,13 +1351,16 @@ func NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn s } func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + tx := s.db if lockStrength != LockingStrengthNone { tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) } var setupKey types.SetupKey - result := tx. + result := tx.WithContext(ctx). Take(&setupKey, GetKeyQueryCondition(s), key) if result.Error != nil { @@ -1349,7 +1374,10 @@ func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength Locking } func (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string) error { - result := s.db.Model(&types.SetupKey{}). + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + + result := s.db.WithContext(ctx).Model(&types.SetupKey{}). Where(idQueryCondition, setupKeyID). Updates(map[string]interface{}{ "used_times": gorm.Expr("used_times + 1"), @@ -1369,8 +1397,11 @@ func (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string // AddPeerToAllGroup adds a peer to the 'All' group. Method always needs to run in a transaction func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + var groupID string - _ = s.db.Model(types.Group{}). + _ = s.db.WithContext(ctx).Model(types.Group{}). Select("id"). Where("account_id = ? AND name = ?", accountID, "All"). Limit(1). @@ -1398,13 +1429,16 @@ func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peer // AddPeerToGroup adds a peer to a group func (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupID string) error { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + peer := &types.GroupPeer{ AccountID: accountID, GroupID: groupID, PeerID: peerID, } - err := s.db.Clauses(clause.OnConflict{ + err := s.db.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "group_id"}, {Name: "peer_id"}}, DoNothing: true, }).Create(peer).Error @@ -1594,7 +1628,10 @@ func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrengt } func (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error { - if err := s.db.Create(peer).Error; err != nil { + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + + if err := s.db.WithContext(ctx).Create(peer).Error; err != nil { return status.Errorf(status.Internal, "issue adding peer to account: %s", err) } @@ -1720,7 +1757,10 @@ func (s *SqlStore) DeletePeer(ctx context.Context, accountID string, peerID stri } func (s *SqlStore) IncrementNetworkSerial(ctx context.Context, accountId string) error { - result := s.db.Model(&types.Account{}).Where(idQueryCondition, accountId).Update("network_serial", gorm.Expr("network_serial + 1")) + ctx, cancel := getDebuggingCtx(ctx) + defer cancel() + + result := s.db.WithContext(ctx).Model(&types.Account{}).Where(idQueryCondition, accountId).Update("network_serial", gorm.Expr("network_serial + 1")) if result.Error != nil { log.WithContext(ctx).Errorf("failed to increment network serial count in store: %v", result.Error) return status.Errorf(status.Internal, "failed to increment network serial count in store") @@ -2762,3 +2802,33 @@ func (s *SqlStore) GetAccountGroupPeers(ctx context.Context, lockStrength Lockin return groupPeers, nil } + +func getDebuggingCtx(grpcCtx context.Context) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + userID, ok := grpcCtx.Value(nbcontext.UserIDKey).(string) + if ok { + //nolint + ctx = context.WithValue(ctx, nbcontext.UserIDKey, userID) + } + + requestID, ok := grpcCtx.Value(nbcontext.RequestIDKey).(string) + if ok { + //nolint + ctx = context.WithValue(ctx, nbcontext.RequestIDKey, requestID) + } + + accountID, ok := grpcCtx.Value(nbcontext.AccountIDKey).(string) + if ok { + //nolint + ctx = context.WithValue(ctx, nbcontext.AccountIDKey, accountID) + } + + go func() { + select { + case <-ctx.Done(): + case <-grpcCtx.Done(): + log.WithContext(grpcCtx).Warnf("grpc context ended early, error: %v", grpcCtx.Err()) + } + }() + return ctx, cancel +}