2022-08-18 18:22:15 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2024-06-20 13:52:32 +02:00
|
|
|
"fmt"
|
2023-09-04 17:03:44 +02:00
|
|
|
"net/netip"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
2023-11-08 11:35:37 +01:00
|
|
|
"github.com/rs/xid"
|
|
|
|
|
2024-06-13 13:24:24 +02:00
|
|
|
"github.com/netbirdio/netbird/management/domain"
|
2022-08-18 18:22:15 +02:00
|
|
|
"github.com/netbirdio/netbird/management/proto"
|
2023-01-25 16:29:59 +01:00
|
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
2022-11-11 20:36:45 +01:00
|
|
|
"github.com/netbirdio/netbird/management/server/status"
|
2022-08-18 18:22:15 +02:00
|
|
|
"github.com/netbirdio/netbird/route"
|
|
|
|
)
|
|
|
|
|
|
|
|
// GetRoute gets a route object from account and route IDs
|
2024-05-06 14:47:49 +02:00
|
|
|
func (am *DefaultAccountManager) GetRoute(accountID string, routeID route.ID, userID string) (*route.Route, error) {
|
2024-05-07 14:30:03 +02:00
|
|
|
unlock := am.Store.AcquireAccountWriteLock(accountID)
|
2022-11-07 17:52:23 +01:00
|
|
|
defer unlock()
|
2022-08-18 18:22:15 +02:00
|
|
|
|
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
|
|
if err != nil {
|
2022-11-11 20:36:45 +01:00
|
|
|
return nil, err
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2022-11-05 10:24:50 +01:00
|
|
|
user, err := account.FindUser(userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-25 09:50:27 +01:00
|
|
|
if !(user.HasAdminPower() || user.IsServiceUser) {
|
2023-12-01 17:24:57 +01:00
|
|
|
return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view Network Routes")
|
2022-11-05 10:24:50 +01:00
|
|
|
}
|
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
wantedRoute, found := account.Routes[routeID]
|
|
|
|
if found {
|
|
|
|
return wantedRoute, nil
|
|
|
|
}
|
|
|
|
|
2022-11-11 20:36:45 +01:00
|
|
|
return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID)
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2024-06-13 13:24:24 +02:00
|
|
|
// checkRoutePrefixOrDomainsExistForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups.
|
|
|
|
func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account *Account, peerID string, routeID route.ID, peerGroupIDs []string, prefix netip.Prefix, domains domain.List) error {
|
2023-09-28 14:32:36 +02:00
|
|
|
// routes can have both peer and peer_groups
|
2024-06-13 13:24:24 +02:00
|
|
|
routesWithPrefix := account.GetRoutesByPrefixOrDomains(prefix, domains)
|
2023-09-28 14:32:36 +02:00
|
|
|
|
|
|
|
// lets remember all the peers and the peer groups from routesWithPrefix
|
|
|
|
seenPeers := make(map[string]bool)
|
|
|
|
seenPeerGroups := make(map[string]bool)
|
|
|
|
|
|
|
|
for _, prefixRoute := range routesWithPrefix {
|
|
|
|
// we skip route(s) with the same network ID as we want to allow updating of the existing route
|
2024-06-20 13:52:32 +02:00
|
|
|
// when creating a new route routeID is newly generated so nothing will be skipped
|
2023-09-28 14:32:36 +02:00
|
|
|
if routeID == prefixRoute.ID {
|
|
|
|
continue
|
|
|
|
}
|
2022-08-23 11:09:56 +02:00
|
|
|
|
2023-09-28 14:32:36 +02:00
|
|
|
if prefixRoute.Peer != "" {
|
2024-05-06 14:47:49 +02:00
|
|
|
seenPeers[string(prefixRoute.ID)] = true
|
2023-09-28 14:32:36 +02:00
|
|
|
}
|
|
|
|
for _, groupID := range prefixRoute.PeerGroups {
|
|
|
|
seenPeerGroups[groupID] = true
|
|
|
|
|
|
|
|
group := account.GetGroup(groupID)
|
|
|
|
if group == nil {
|
|
|
|
return status.Errorf(
|
2024-06-20 13:52:32 +02:00
|
|
|
status.InvalidArgument, "failed to add route with %s - peer group %s doesn't exist",
|
|
|
|
getRouteDescriptor(prefix, domains), groupID,
|
|
|
|
)
|
2023-09-28 14:32:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, pID := range group.Peers {
|
|
|
|
seenPeers[pID] = true
|
|
|
|
}
|
|
|
|
}
|
2022-08-23 11:09:56 +02:00
|
|
|
}
|
|
|
|
|
2023-09-28 14:32:36 +02:00
|
|
|
if 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 {
|
|
|
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
|
|
|
}
|
|
|
|
if _, ok := seenPeers[peerID]; ok {
|
|
|
|
return status.Errorf(status.AlreadyExists,
|
2024-06-20 13:52:32 +02:00
|
|
|
"failed to add route with %s - peer %s already has this route", getRouteDescriptor(prefix, domains), peerID)
|
2023-09-28 14:32:36 +02:00
|
|
|
}
|
2022-11-07 12:10:56 +01:00
|
|
|
}
|
|
|
|
|
2023-09-28 14:32:36 +02:00
|
|
|
// check that peerGroupIDs are not in any route peerGroups list
|
|
|
|
for _, groupID := range peerGroupIDs {
|
2024-06-20 13:52:32 +02:00
|
|
|
group := account.GetGroup(groupID) // we validated the group existence before entering this function, no need to check again.
|
2022-08-18 18:22:15 +02:00
|
|
|
|
2023-09-28 14:32:36 +02:00
|
|
|
if _, ok := seenPeerGroups[groupID]; ok {
|
|
|
|
return status.Errorf(
|
2024-06-20 13:52:32 +02:00
|
|
|
status.AlreadyExists, "failed to add route with %s - peer group %s already has this route",
|
|
|
|
getRouteDescriptor(prefix, domains), group.Name)
|
2023-09-28 14:32:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// check that the peers from peerGroupIDs groups are not the same peers we saw in routesWithPrefix
|
|
|
|
for _, id := range group.Peers {
|
|
|
|
if _, ok := seenPeers[id]; ok {
|
2023-10-09 14:39:41 +02:00
|
|
|
peer := account.GetPeer(id)
|
2023-09-28 14:32:36 +02:00
|
|
|
if peer == nil {
|
|
|
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
|
|
|
}
|
|
|
|
return status.Errorf(status.AlreadyExists,
|
2024-06-20 13:52:32 +02:00
|
|
|
"failed to add route with %s - peer %s from the group %s already has this route",
|
|
|
|
getRouteDescriptor(prefix, domains), peer.Name, group.Name)
|
2023-09-28 14:32:36 +02:00
|
|
|
}
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-28 14:32:36 +02:00
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-06-20 13:52:32 +02:00
|
|
|
func getRouteDescriptor(prefix netip.Prefix, domains domain.List) string {
|
|
|
|
if len(domains) > 0 {
|
|
|
|
return fmt.Sprintf("domains [%s]", domains.SafeString())
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("prefix %s", prefix.String())
|
|
|
|
}
|
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
// CreateRoute creates and saves a new route
|
2024-06-13 13:24:24 +02:00
|
|
|
func (am *DefaultAccountManager) CreateRoute(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 []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) {
|
2024-05-07 14:30:03 +02:00
|
|
|
unlock := am.Store.AcquireAccountWriteLock(accountID)
|
2022-11-07 17:52:23 +01:00
|
|
|
defer unlock()
|
2022-08-18 18:22:15 +02:00
|
|
|
|
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
|
|
if err != nil {
|
2022-11-11 20:36:45 +01:00
|
|
|
return nil, err
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2024-06-13 13:24:24 +02:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(domains) > 0 {
|
|
|
|
prefix = getPlaceholderIP()
|
|
|
|
}
|
|
|
|
|
2023-09-28 14:32:36 +02:00
|
|
|
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)
|
2023-01-25 16:29:59 +01:00
|
|
|
}
|
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
var newRoute route.Route
|
2024-05-06 14:47:49 +02:00
|
|
|
newRoute.ID = route.ID(xid.New().String())
|
2023-09-28 14:32:36 +02:00
|
|
|
|
|
|
|
if len(peerGroupIDs) > 0 {
|
|
|
|
err = validateGroups(peerGroupIDs, account.Groups)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-13 13:24:24 +02:00
|
|
|
err = am.checkRoutePrefixOrDomainsExistForPeers(account, peerID, newRoute.ID, peerGroupIDs, prefix, domains)
|
2022-08-18 18:22:15 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if metric < route.MinMetric || metric > route.MaxMetric {
|
2022-11-11 20:36:45 +01:00
|
|
|
return nil, status.Errorf(status.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric)
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2024-05-06 14:47:49 +02:00
|
|
|
if utf8.RuneCountInString(string(netID)) > route.MaxNetIDChar || netID == "" {
|
2022-11-11 20:36:45 +01:00
|
|
|
return nil, status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar)
|
2022-08-22 14:10:24 +02:00
|
|
|
}
|
|
|
|
|
2022-12-06 10:11:57 +01:00
|
|
|
err = validateGroups(groups, account.Groups)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-02-03 10:33:28 +01:00
|
|
|
newRoute.Peer = peerID
|
2023-09-28 14:32:36 +02:00
|
|
|
newRoute.PeerGroups = peerGroupIDs
|
2024-06-13 13:24:24 +02:00
|
|
|
newRoute.Network = prefix
|
|
|
|
newRoute.Domains = domains
|
|
|
|
newRoute.NetworkType = networkType
|
2022-08-18 18:22:15 +02:00
|
|
|
newRoute.Description = description
|
2022-08-22 14:10:24 +02:00
|
|
|
newRoute.NetID = netID
|
2022-08-18 18:22:15 +02:00
|
|
|
newRoute.Masquerade = masquerade
|
|
|
|
newRoute.Metric = metric
|
|
|
|
newRoute.Enabled = enabled
|
2022-12-06 10:11:57 +01:00
|
|
|
newRoute.Groups = groups
|
2024-06-13 13:24:24 +02:00
|
|
|
newRoute.KeepRoute = keepRoute
|
2022-08-18 18:22:15 +02:00
|
|
|
|
|
|
|
if account.Routes == nil {
|
2024-05-06 14:47:49 +02:00
|
|
|
account.Routes = make(map[route.ID]*route.Route)
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
account.Routes[newRoute.ID] = &newRoute
|
|
|
|
|
|
|
|
account.Network.IncSerial()
|
|
|
|
if err = am.Store.SaveAccount(account); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-10-04 15:08:50 +02:00
|
|
|
am.updateAccountPeers(account)
|
2023-01-25 16:29:59 +01:00
|
|
|
|
2024-05-06 14:47:49 +02:00
|
|
|
am.StoreEvent(userID, string(newRoute.ID), accountID, activity.RouteCreated, newRoute.EventMeta())
|
2023-01-25 16:29:59 +01:00
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
return &newRoute, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SaveRoute saves route
|
2023-01-25 16:29:59 +01:00
|
|
|
func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave *route.Route) error {
|
2024-05-07 14:30:03 +02:00
|
|
|
unlock := am.Store.AcquireAccountWriteLock(accountID)
|
2022-11-07 17:52:23 +01:00
|
|
|
defer unlock()
|
2022-08-18 18:22:15 +02:00
|
|
|
|
|
|
|
if routeToSave == nil {
|
2022-11-11 20:36:45 +01:00
|
|
|
return status.Errorf(status.InvalidArgument, "route provided is nil")
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if routeToSave.Metric < route.MinMetric || routeToSave.Metric > route.MaxMetric {
|
2022-11-11 20:36:45 +01:00
|
|
|
return status.Errorf(status.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric)
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2024-05-06 14:47:49 +02:00
|
|
|
if utf8.RuneCountInString(string(routeToSave.NetID)) > route.MaxNetIDChar || routeToSave.NetID == "" {
|
2022-11-11 20:36:45 +01:00
|
|
|
return status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar)
|
2022-08-22 14:10:24 +02:00
|
|
|
}
|
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
|
|
if err != nil {
|
2022-11-11 20:36:45 +01:00
|
|
|
return err
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2024-06-13 13:24:24 +02:00
|
|
|
if len(routeToSave.Domains) > 0 && routeToSave.Network.IsValid() {
|
|
|
|
return status.Errorf(status.InvalidArgument, "domains and network should not be provided at the same time")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(routeToSave.Domains) == 0 && !routeToSave.Network.IsValid() {
|
|
|
|
return status.Errorf(status.InvalidArgument, "invalid Prefix")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(routeToSave.Domains) > 0 {
|
|
|
|
routeToSave.Network = getPlaceholderIP()
|
|
|
|
}
|
|
|
|
|
2023-09-28 14:32:36 +02:00
|
|
|
if routeToSave.Peer != "" && len(routeToSave.PeerGroups) != 0 {
|
|
|
|
return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(routeToSave.PeerGroups) > 0 {
|
|
|
|
err = validateGroups(routeToSave.PeerGroups, account.Groups)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-13 13:24:24 +02:00
|
|
|
err = am.checkRoutePrefixOrDomainsExistForPeers(account, routeToSave.Peer, routeToSave.ID, routeToSave.Copy().PeerGroups, routeToSave.Network, routeToSave.Domains)
|
2023-09-28 14:32:36 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-12-06 10:11:57 +01:00
|
|
|
err = validateGroups(routeToSave.Groups, account.Groups)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
account.Routes[routeToSave.ID] = routeToSave
|
|
|
|
|
|
|
|
account.Network.IncSerial()
|
|
|
|
if err = am.Store.SaveAccount(account); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-10-04 15:08:50 +02:00
|
|
|
am.updateAccountPeers(account)
|
2023-02-03 10:33:28 +01:00
|
|
|
|
2024-05-06 14:47:49 +02:00
|
|
|
am.StoreEvent(userID, string(routeToSave.ID), accountID, activity.RouteUpdated, routeToSave.EventMeta())
|
2023-01-25 16:29:59 +01:00
|
|
|
|
2023-02-03 10:33:28 +01:00
|
|
|
return nil
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteRoute deletes route with routeID
|
2024-05-06 14:47:49 +02:00
|
|
|
func (am *DefaultAccountManager) DeleteRoute(accountID string, routeID route.ID, userID string) error {
|
2024-05-07 14:30:03 +02:00
|
|
|
unlock := am.Store.AcquireAccountWriteLock(accountID)
|
2022-11-07 17:52:23 +01:00
|
|
|
defer unlock()
|
2022-08-18 18:22:15 +02:00
|
|
|
|
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
|
|
if err != nil {
|
2022-11-11 20:36:45 +01:00
|
|
|
return err
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2023-01-25 16:29:59 +01:00
|
|
|
routy := account.Routes[routeID]
|
|
|
|
if routy == nil {
|
|
|
|
return status.Errorf(status.NotFound, "route with ID %s doesn't exist", routeID)
|
|
|
|
}
|
2022-08-18 18:22:15 +02:00
|
|
|
delete(account.Routes, routeID)
|
|
|
|
|
|
|
|
account.Network.IncSerial()
|
|
|
|
if err = am.Store.SaveAccount(account); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-05-06 14:47:49 +02:00
|
|
|
am.StoreEvent(userID, string(routy.ID), accountID, activity.RouteRemoved, routy.EventMeta())
|
2023-01-25 16:29:59 +01:00
|
|
|
|
2023-10-04 15:08:50 +02:00
|
|
|
am.updateAccountPeers(account)
|
|
|
|
|
|
|
|
return nil
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ListRoutes returns a list of routes from account
|
2022-11-05 10:24:50 +01:00
|
|
|
func (am *DefaultAccountManager) ListRoutes(accountID, userID string) ([]*route.Route, error) {
|
2024-05-07 14:30:03 +02:00
|
|
|
unlock := am.Store.AcquireAccountWriteLock(accountID)
|
2022-11-07 17:52:23 +01:00
|
|
|
defer unlock()
|
2022-08-18 18:22:15 +02:00
|
|
|
|
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
|
|
if err != nil {
|
2022-11-11 20:36:45 +01:00
|
|
|
return nil, err
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
|
2022-11-05 10:24:50 +01:00
|
|
|
user, err := account.FindUser(userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-25 09:50:27 +01:00
|
|
|
if !(user.HasAdminPower() || user.IsServiceUser) {
|
2023-12-01 17:24:57 +01:00
|
|
|
return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view Network Routes")
|
2022-11-05 10:24:50 +01:00
|
|
|
}
|
|
|
|
|
2022-08-18 18:22:15 +02:00
|
|
|
routes := make([]*route.Route, 0, len(account.Routes))
|
|
|
|
for _, item := range account.Routes {
|
|
|
|
routes = append(routes, item)
|
|
|
|
}
|
|
|
|
|
|
|
|
return routes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func toProtocolRoute(route *route.Route) *proto.Route {
|
|
|
|
return &proto.Route{
|
2024-05-06 14:47:49 +02:00
|
|
|
ID: string(route.ID),
|
|
|
|
NetID: string(route.NetID),
|
2022-08-22 14:10:24 +02:00
|
|
|
Network: route.Network.String(),
|
2024-06-13 13:24:24 +02:00
|
|
|
Domains: route.Domains.ToPunycodeList(),
|
2022-08-22 14:10:24 +02:00
|
|
|
NetworkType: int64(route.NetworkType),
|
|
|
|
Peer: route.Peer,
|
|
|
|
Metric: int64(route.Metric),
|
|
|
|
Masquerade: route.Masquerade,
|
2024-06-13 13:24:24 +02:00
|
|
|
KeepRoute: route.KeepRoute,
|
2022-08-18 18:22:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func toProtocolRoutes(routes []*route.Route) []*proto.Route {
|
|
|
|
protoRoutes := make([]*proto.Route, 0)
|
|
|
|
for _, r := range routes {
|
|
|
|
protoRoutes = append(protoRoutes, toProtocolRoute(r))
|
|
|
|
}
|
|
|
|
return protoRoutes
|
|
|
|
}
|
2024-06-13 13:24:24 +02:00
|
|
|
|
|
|
|
// getPlaceholderIP returns a placeholder IP address for the route if domains are used
|
|
|
|
func getPlaceholderIP() netip.Prefix {
|
|
|
|
// Using an IP from the documentation range to minimize impact in case older clients try to set a route
|
|
|
|
return netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32)
|
|
|
|
}
|