Skip to content

Commit

Permalink
ir: Support verified NNS domains of the storage nodes
Browse files Browse the repository at this point in the history
From now, the Inner Ring checks any incoming node for permission to
associate itself with optional private node group (kind of subnet).
Access lists are stored in the NeoFS NNS.

Closes #2280.

Signed-off-by: Leonard Lyubich <[email protected]>
  • Loading branch information
cthulhu-rider committed Oct 10, 2023
1 parent 750e993 commit 6ab5c7c
Show file tree
Hide file tree
Showing 15 changed files with 862 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Changelog for NeoFS Node

## [Unreleased]

### Added
- Support of verified domains for the storage nodes (#2280)

### Fixed
- `neofs-cli netmap netinfo` documentation (#2555)
- `GETRANGEHASH` to a node without an object produced `GETRANGE` or `GET` requests (#2541)
Expand Down
3 changes: 2 additions & 1 deletion cmd/neofs-node/config/node/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ func TestNodeSection(t *testing.T) {

require.Equal(t, true, relay)

require.Len(t, attributes, 2)
require.Len(t, attributes, 3)
require.Equal(t, "Price:11", attributes[0])
require.Equal(t, "UN-LOCODE:RU MSK", attributes[1])
require.Equal(t, "VerifiedNodesDomain:nodes.some-org.neofs", attributes[2])

require.NotNil(t, wKey)
require.Equal(t,
Expand Down
1 change: 1 addition & 0 deletions config/example/node.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ NEOFS_NODE_WALLET_PASSWORD=password
NEOFS_NODE_ADDRESSES="s01.neofs.devenv:8080 /dns4/s02.neofs.devenv/tcp/8081 grpc://127.0.0.1:8082 grpcs://localhost:8083"
NEOFS_NODE_ATTRIBUTE_0=Price:11
NEOFS_NODE_ATTRIBUTE_1="UN-LOCODE:RU MSK"
NEOFS_NODE_ATTRIBUTE_2="VerifiedNodesDomain:nodes.some-org.neofs"
NEOFS_NODE_RELAY=true
NEOFS_NODE_PERSISTENT_SESSIONS_PATH=/sessions
NEOFS_NODE_PERSISTENT_STATE_PATH=/state
Expand Down
1 change: 1 addition & 0 deletions config/example/node.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
],
"attribute_0": "Price:11",
"attribute_1": "UN-LOCODE:RU MSK",
"attribute_2": "VerifiedNodesDomain:nodes.some-org.neofs",
"relay": true,
"persistent_sessions": {
"path": "/sessions"
Expand Down
4 changes: 4 additions & 0 deletions config/example/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ node:
- /dns4/s02.neofs.devenv/tcp/8081
- grpc://127.0.0.1:8082
- grpcs://localhost:8083
# List of colon-separated key-value attributes.
attribute_0: "Price:11"
attribute_1: UN-LOCODE:RU MSK
# Next attribute specifies optional NeoFS NNS domain in order to enter the storage node into a private node group
# (kind of subnet). The node must have public key from the corresponding access list. See docs for more detailed information.
attribute_2: VerifiedNodesDomain:nodes.some-org.neofs
relay: true # start Storage node in relay mode without bootstrapping into the Network map
persistent_sessions:
path: /sessions # path to persistent session tokens file of Storage node (default: in-memory sessions)
Expand Down
2 changes: 1 addition & 1 deletion docs/storage-node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ node:
| `key` | `string` | | Path to the binary-encoded private key. |
| `wallet` | [Wallet config](#wallet-subsection) | | Wallet configuration. Has no effect if `key` is provided. |
| `addresses` | `[]string` | | Addresses advertised in the netmap. |
| `attribute` | `[]string` | | Node attributes as a list of key-value pairs in `<key>:<value>` format. |
| `attribute` | `[]string` | | Node attributes as a list of key-value pairs in `<key>:<value>` format. See also docs about verified nodes' domains.|
| `relay` | `bool` | | Enable relay mode. |
| `persistent_sessions` | [Persistent sessions config](#persistent_sessions-subsection) | | Persistent session token store configuration. |
| `persistent_state` | [Persistent state config](#persistent_state-subsection) | | Persistent state configuration. |
Expand Down
40 changes: 40 additions & 0 deletions docs/verified-node-domains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Verified domains of the NeoFS storage nodes

Storage nodes declare information flexibly via key-value string attributes when
applying to enter the NeoFS network map. In general, any attributes can be
declared, however, some of them may be subject to restrictions. In particular,
some parties may need to limit the relationship to them of any nodes of their
public network. For example, an organization may need to deploy its storage
nodes as a subnet of a public network to implement specific data storage
strategies. In this example, the organization’s nodes will be “normal” for 3rd
parties, while other nodes will not be able to enter the subnet without special
permission at the system level.

NeoFS implements solution of the described task through access lists managed
within NeoFS NNS.

## Access lists

These lists are stored in the NeoFS NNS. Each party may register any available
NNS domain and set records of `TXT` type with Neo addresses of the storage
nodes. After the domain is registered, it becomes an alias to the subnet composed
only from specified storage nodes. Any storage node trying to associate itself
with this subnet while trying to enter the network must have public key
presented in the access list. The Inner Ring will deny everyone else access to
the network map.

### Domain record format

For each public key, a record is created - a structure with at least 3 fields:
1. `ByteString` with name of the corresponding domain
2. `Integer` that is `16` for TXT records (other record types are allowed but left unprocessed)
3. `ByteString` with Neo address of the storage node's public key

## Private subnet entrance

By default, storage nodes do not belong to private groups. Any node wishing to
enter the private subnet of storage nodes must first find out the corresponding
domain name. To request a binding to a given subnet, a node needs to set
related domain name in its information about when registering in the network
map. The domain is set via `VerifiedNodesDomain` attribute. To be admitted to
the network, a node must be present in the access list.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/nspcc-dev/neo-go v0.102.0
github.com/nspcc-dev/neofs-api-go/v2 v2.14.0
github.com/nspcc-dev/neofs-contract v0.18.0
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11.0.20230926161529-a5cb78a74aed
github.com/nspcc-dev/tzhash v1.7.0
github.com/olekukonko/tablewriter v0.0.5
github.com/panjf2000/ants/v2 v2.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ github.com/nspcc-dev/neofs-contract v0.18.0 h1:9g50b16s0mQFFskG93yRSWh4KL7yYOW+x
github.com/nspcc-dev/neofs-contract v0.18.0/go.mod h1:UQr1rUjg0eibLwJd6vfsJJEUBnmRysCg8XQd1HYiS2w=
github.com/nspcc-dev/neofs-crypto v0.4.0 h1:5LlrUAM5O0k1+sH/sktBtrgfWtq1pgpDs09fZo+KYi4=
github.com/nspcc-dev/neofs-crypto v0.4.0/go.mod h1:6XJ8kbXgOfevbI2WMruOtI+qUJXNwSGM/E9eClXxPHs=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11 h1:QOc8ZRN5DXlAeRPh5QG9u8rMLgoeRNiZF5/vL7QupWg=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11/go.mod h1:W+ImTNRnSNMH8w43H1knCcIqwu7dLHePXtlJNZ7EFIs=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11.0.20230926161529-a5cb78a74aed h1:ySOlpzLNU3djblNtjZFiTrfoE9zZ5fd1bwjxbbI3gvM=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11.0.20230926161529-a5cb78a74aed/go.mod h1:W+ImTNRnSNMH8w43H1knCcIqwu7dLHePXtlJNZ7EFIs=
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
github.com/nspcc-dev/tzhash v1.7.0 h1:/+aL33NC7y5OIGnY2kYgjZt8mg7LVGFMdj/KAJLndnk=
Expand Down
9 changes: 9 additions & 0 deletions pkg/innerring/innerring.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap"
nodevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation"
availabilityvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/availability"
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/privatedomains"
statevalidation "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/state"
addrvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/structure"
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/reputation"
Expand Down Expand Up @@ -724,6 +725,13 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<-
var netMapCandidateStateValidator statevalidation.NetMapCandidateValidator
netMapCandidateStateValidator.SetNetworkSettings(netSettings)

nnsContractAddr, err := server.morphClient.NNSHash()
if err != nil {
return nil, fmt.Errorf("get NeoFS NNS contract address: %w", err)
}

nnsService := newNeoFSNNS(nnsContractAddr, server.morphClient)

// create netmap processor
server.netmapProcessor, err = netmap.New(&netmap.Params{
Log: log,
Expand All @@ -749,6 +757,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<-
&netMapCandidateStateValidator,
addrvalidator.New(),
availabilityvalidator.New(),
privatedomains.New(nnsService),
locodeValidator,
),
NodeStateSettings: netSettings,
Expand Down
190 changes: 190 additions & 0 deletions pkg/innerring/nns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package innerring

import (
"errors"
"fmt"
"strings"

"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
nnsrpc "github.com/nspcc-dev/neofs-contract/rpc/nns"
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/privatedomains"
)

// provides services of the NeoFS Name Service consumed by the Inner Ring node.
type neoFSNNS struct {
invoker nep11.Invoker
contract *nnsrpc.ContractReader
}

// creates NeoFS Name Service provider working with the Neo smart contract
// deployed in the Neo network accessed through the specified [nep11.Invoker].
func newNeoFSNNS(contractAddress util.Uint160, contractCaller nep11.Invoker) *neoFSNNS {
return &neoFSNNS{
invoker: contractCaller,
contract: nnsrpc.NewReader(contractCaller, contractAddress),
}
}

var errDomainNotFound = errors.New("domain not found")

type errWrongStackItemType struct {
expected, got stackitem.Type
}

func wrongStackItemTypeError(expected, got stackitem.Type) errWrongStackItemType {
return errWrongStackItemType{
expected: expected,
got: got,
}
}

func (x errWrongStackItemType) Error() string {
return fmt.Sprintf("wrong type: expected %s, got %s", x.expected, x.got)
}

type errInvalidNumberOfStructFields struct {
expected, got int
}

func invalidNumberOfStructFieldsError(expected, got int) errInvalidNumberOfStructFields {
return errInvalidNumberOfStructFields{
expected: expected,
got: got,
}
}

func (x errInvalidNumberOfStructFields) Error() string {
return fmt.Sprintf("invalid number of struct fields: expected %d, got %d", x.expected, x.got)
}

type errInvalidStructField struct {
index uint
cause error
}

func invalidStructFieldError(index uint, cause error) errInvalidStructField {
return errInvalidStructField{
index: index,
cause: cause,
}
}

func (x errInvalidStructField) Error() string {
return fmt.Sprintf("invalid struct field #%d: %v", x.index, x.cause)
}

func (x errInvalidStructField) Unwrap() error {
return x.cause
}

type errInvalidNNSDomainRecord struct {
domain string
record string
cause error
}

func invalidNNSDomainRecordError(domain, record string, cause error) errInvalidNNSDomainRecord {
return errInvalidNNSDomainRecord{
domain: domain,
record: record,
cause: cause,
}
}

func (x errInvalidNNSDomainRecord) Error() string {
if x.record != "" {
return fmt.Sprintf("invalid record %q of the NNS domain %q: %v", x.record, x.domain, x.cause)
}

return fmt.Sprintf("invalid record of the NNS domain %q: %v", x.domain, x.cause)
}

func (x errInvalidNNSDomainRecord) Unwrap() error {
return x.cause
}

// CheckDomainRecord calls iterating 'getAllRecords' method of the parameterized
// Neo smart contract passing the given domain name. If contract throws 'token
// not found' exception, CheckDomainRecord returns errDomainNotFound. Each value
// in the resulting iterator is expected to be structure with at least 3 fields.
// If any value has the 2nd field is a number equal to 16 (TXT record type in
// the NNS) and the 3rd one is a string equal to the specified record,
// CheckDomainRecord returns nil. Otherwise,
// [privatedomains.ErrMissingDomainRecord] is returned.
func (x *neoFSNNS) CheckDomainRecord(domain string, record string) error {
sessionID, iter, err := x.contract.GetAllRecords(domain)
if err != nil {
// Track https://github.com/nspcc-dev/neofs-node/issues/2583.
if strings.Contains(err.Error(), "token not found") {
return errDomainNotFound
}

return fmt.Errorf("get iterator over all records of the NNS domain %q: %w", domain, err)
}

defer func() {
_ = x.invoker.TerminateSession(sessionID)
}()

hasRecords := false

for {
items, err := x.invoker.TraverseIterator(sessionID, &iter, 10)
if err != nil {
return fmt.Errorf("traverse iterator over all records of the NNS domain %q: %w", domain, err)
}

if len(items) == 0 {
break
}

hasRecords = true

for i := range items {
fields, ok := items[i].Value().([]stackitem.Item)
if !ok {
return invalidNNSDomainRecordError(domain, "",
wrongStackItemTypeError(stackitem.StructT, items[i].Type()))
}

if len(fields) < 3 {
return invalidNNSDomainRecordError(domain, "",
invalidNumberOfStructFieldsError(3, len(fields)))
}

_, err = fields[0].TryBytes()
if err != nil {
return invalidNNSDomainRecordError(domain, "",
invalidStructFieldError(0, wrongStackItemTypeError(stackitem.ByteArrayT, fields[0].Type())))
}

typ, err := fields[1].TryInteger()
if err != nil {
return invalidNNSDomainRecordError(domain, "",
invalidStructFieldError(1, wrongStackItemTypeError(stackitem.IntegerT, fields[1].Type())))
}

if typ.Cmp(nnsrpc.TXT) != 0 {
continue
}

data, err := fields[2].TryBytes()
if err != nil {
return invalidNNSDomainRecordError(domain, "",
invalidStructFieldError(2, wrongStackItemTypeError(stackitem.ByteArrayT, fields[2].Type())))
}

if string(data) == record {
return nil
}
}
}

if hasRecords {
return privatedomains.ErrMissingDomainRecord
}

return nil
}
Loading

0 comments on commit 6ab5c7c

Please sign in to comment.