Skip to content

Improve peerguard #863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ package cmd

import (
"context"
"encoding/base64"
"fmt"
"net"
"os"
"path/filepath"
"time"

"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/metrics"
"github.com/libp2p/go-libp2p/core/network"

Expand Down Expand Up @@ -58,6 +60,10 @@ func MainFlags() []cli.Flag {
Name: "b",
Usage: "Encodes the new config in base64, so it can be used as a token",
},
&cli.BoolFlag{
Name: "p",
Usage: "Generates a ED25519 private key, converts it to protobuf serialized form, and encodes as base64 string",
},
&cli.BoolFlag{
Name: "debug",
Usage: "Starts API with pprof attached",
Expand Down Expand Up @@ -156,6 +162,22 @@ func Main() func(c *cli.Context) error {

os.Exit(0)
}

if c.Bool("p") {
// Generates a new protobuf encoded priv key and exit
privkey, err := node.GenPrivKey(0)
if err != nil {
return err
}

protoKey, err := crypto.MarshalPrivateKey(privkey)
if err != nil {
return err
}

fmt.Printf("Private key: %s\n", base64.StdEncoding.EncodeToString(protoKey))
os.Exit(0)
}
o, vpnOpts, ll := cliToOpts(c)

// Egress and DHCP needs the Alive service
Expand Down
18 changes: 15 additions & 3 deletions cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ limitations under the License.
package cmd

import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -321,6 +322,11 @@ var CommonFlags []cli.Flag = []cli.Flag{
Usage: "Enable peerguard. (Experimental)",
EnvVars: []string{"PEERGUARD"},
},
&cli.StringFlag{
Name: "privkey",
Usage: "Use fixed base64 <- protobuf encoded privkey. (Experimental)",
EnvVars: []string{"EDGEVPNPRIVKEY"},
},
&cli.BoolFlag{
Name: "privkey-cache",
Usage: "Enable privkey caching. (Experimental)",
Expand Down Expand Up @@ -495,13 +501,19 @@ func cliToOpts(c *cli.Context) ([]node.Option, []vpn.Option, *logger.Logger) {
}
}

// Check if we have any privkey identity cached already
if c.Bool("privkey-cache") {
if c.String("privkey") != "" {
raw, err := base64.StdEncoding.DecodeString(c.String("privkey"))
if err != nil {
checkErr(fmt.Errorf("failed to decode privkey: %v", err))
} else {
nc.Privkey = raw
}
// Check if we have any privkey identity cached already
} else if c.Bool("privkey-cache") {
keyFile := filepath.Join(c.String("privkey-cache-dir"), "privkey")
dat, err := os.ReadFile(keyFile)
if err == nil && len(dat) > 0 {
llger.Info("Reading key from", keyFile)

nc.Privkey = dat
} else {
// generate, write
Expand Down
19 changes: 19 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
otp:
dht:
interval: 360
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is a leftover, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, there are also testing script, and other leftovers for demo purposes out of the box
that ofc will be cleaned if the main direction of that pr resolved, so further cleaning is upcoming

key: YVMwYeeoIJ9yGQeuRNHY3wigEhDEisAPcbt0L30vQ3q
length: 43
crypto:
interval: 360
key: 0Yu91JT1WPmWtmmrFD5tWrSzeyhLUVFWyiEXzIezcyz
length: 43
room: k9xRlX6brHxKMAWhGgvsuoiwq4fcNyNtA7JQivZBYm7
rendezvous: tFMpGqtqtFHckVt62FCklh9xqjTi9upKWCmXNrNBCpL
mdns: NG8LRHaJTRnR0tbOGgr73oQTZqvKEn0kllXQr5SfKDs
max_message_size: 20971520

trusted_peer_ids:
- 12D3KooWQi1XDFy1Ntv5WXLYJWbuFy1zbXM3F6jc4DUJKtaoZPpC
protected_store_key:
- trustzone
- trustzoneAuth
19 changes: 19 additions & 0 deletions config_example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
otp:
dht:
interval: 360
key: YVMwYeeoIJ9yGQeuRNHY3wigEhDEisAPcbt0L30vQ3q
length: 43
crypto:
interval: 360
key: 0Yu91JT1WPmWtmmrFD5tWrSzeyhLUVFWyiEXzIezcyz
length: 43
room: k9xRlX6brHxKMAWhGgvsuoiwq4fcNyNtA7JQivZBYm7
rendezvous: tFMpGqtqtFHckVt62FCklh9xqjTi9upKWCmXNrNBCpL
mdns: NG8LRHaJTRnR0tbOGgr73oQTZqvKEn0kllXQr5SfKDs
max_message_size: 20971520

trusted_peer_ids:
- 12D3KooWQi1XDFy1Ntv5WXLYJWbuFy1zbXM3F6jc4DUJKtaoZPpC
protected_store_key:
- trustzone
- trustzoneAuth
4 changes: 2 additions & 2 deletions docs/content/en/docs/Concepts/Overview/peerguardian.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ $ curl -X PUT 'http://localhost:8080/api/ledger/trustzoneAuth/ecdsa_1/LS0tLS1CRU
Now the private key can be used while starting new nodes:

```bash
PEERGATE_AUTH="{ 'ecdsa' : { 'private_key': 'LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQkhUZnRSTVZSRmlvaWZrdllhZEE2NXVRQXlSZTJSZHM0MW1UTGZlNlRIT3FBTTdkZW9sak0KZXVPbTk2V0hacEpzNlJiVU1tL3BCWnZZcElSZ0UwZDJjdUdnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUdVWStMNQptUzcvVWVoSjg0b3JieGo3ZmZUMHBYZ09MSzNZWEZLMWVrSTlEWnR6YnZWOUdwMHl6OTB3aVZxajdpMDFVRnhVCnRKbU1lWURIRzBTQkNuVWpDZ0FGT3ByUURpTXBFR2xYTmZ4LzIvdEVySDIzZDNwSytraFdJbUIza01QL2tRNEIKZzJmYnk2cXJpY1dHd3B4TXBXNWxKZVZXUGlkeWJmMSs0cVhPTWdQbmRnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=' } }"
PEERGATE_AUTH='{ "ecdsa" : { "private_key": "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQkhUZnRSTVZSRmlvaWZrdllhZEE2NXVRQXlSZTJSZHM0MW1UTGZlNlRIT3FBTTdkZW9sak0KZXVPbTk2V0hacEpzNlJiVU1tL3BCWnZZcElSZ0UwZDJjdUdnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUdVWStMNQptUzcvVWVoSjg0b3JieGo3ZmZUMHBYZ09MSzNZWEZLMWVrSTlEWnR6YnZWOUdwMHl6OTB3aVZxajdpMDFVRnhVCnRKbU1lWURIRzBTQkNuVWpDZ0FGT3ByUURpTXBFR2xYTmZ4LzIvdEVySDIzZDNwSytraFdJbUIza01QL2tRNEIKZzJmYnk2cXJpY1dHd3B4TXBXNWxKZVZXUGlkeWJmMSs0cVhPTWdQbmRnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=" } }'
$ edgevpn --peerguardian --peergate
```

Expand Down Expand Up @@ -88,7 +88,7 @@ $ curl -X PUT 'http://localhost:8080/api/peergate/disable'
To init a new Trusted network, start nodes with `--peergate-relaxed` and add the neccessary auth keys:

```bash
$ edgevpn --peerguardian --peergate --peergate-relaxed
$ edgevpn --peerguard --peergate --peergate-relaxed
$ curl -X PUT 'http://localhost:8080/api/ledger/trustzoneAuth/keytype_1/XXX'
```

Expand Down
37 changes: 32 additions & 5 deletions pkg/blockchain/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"io"
"io/ioutil"
"log"
"maps"
"slices"
"sync"
"time"

Expand All @@ -35,6 +37,10 @@ type Ledger struct {
blockchain Store

channel io.Writer

skipVerify bool
trustedPeerIDS []string
protectedStoreKeys []string
}

type Store interface {
Expand All @@ -59,6 +65,16 @@ func (l *Ledger) newGenesis() {
l.blockchain.Add(genesisBlock)
}

func (l *Ledger) SkipVerify() {
l.skipVerify = true
}
func (l *Ledger) SetTrustedPeerIDS(ids []string) {
l.trustedPeerIDS = ids
}
func (l *Ledger) SetProtectedStoreKeys(keys []string) {
l.protectedStoreKeys = keys
}

// Syncronizer starts a goroutine which
// writes the blockchain to the periodically
func (l *Ledger) Syncronizer(ctx context.Context, t time.Duration) {
Expand Down Expand Up @@ -123,8 +139,17 @@ func (l *Ledger) Update(f *Ledger, h *hub.Message, c chan *hub.Message) (err err
return
}

if len(l.protectedStoreKeys) > 0 && !slices.Contains(l.trustedPeerIDS, h.SenderID) {
for _, key := range l.protectedStoreKeys {
if !maps.Equal(l.blockchain.Last().Storage[key], block.Storage[key]) {
err = errors.Wrapf(err, "unauthorized attempt to write to protected bucket: %s", key)
return
}
}
}

l.Lock()
if block.Index > l.blockchain.Len() {
if l.skipVerify || block.Index > l.blockchain.Len() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure - why do we need still skipVerify? otherwise changes looks good to me!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As i mentioned before, we need to have nodes as passive listeners, and in that mode they listen only from trusted peers

Yeah, either we need to have a reconciliation mechanism implemented, still having priority over trusted nodes vs. block validation Either we having a full passive mode, like i did, which is more simple to implement

We can still verify blocks for everyone, if we haven't defined the trusted nodes, so i would not remove that bits if i understand you correctly, because edgevpn still can be used as fully decentralized non-authoritarian VPN this way (yet, i see that later joined peers never go in sync that way, but for now that is needed only for healthchecks and other simple metadata)

l.blockchain.Add(*block)
}
l.Unlock()
Expand Down Expand Up @@ -350,12 +375,14 @@ func (l *Ledger) Index() int {
func (l *Ledger) writeData(s map[string]map[string]Data) {
newBlock := l.blockchain.Last().NewBlock(s)

if newBlock.IsValid(l.blockchain.Last()) {
l.Lock()
l.blockchain.Add(newBlock)
l.Unlock()
if !l.skipVerify && !newBlock.IsValid(l.blockchain.Last()) {
return
}

l.Lock()
l.blockchain.Add(newBlock)
l.Unlock()

bytes, err := json.Marshal(l.blockchain.Last())
if err != nil {
log.Println(err)
Expand Down
5 changes: 3 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) {
// Build up the authproviders for the peerguardian
aps := []trustzone.AuthProvider{}
for ap, providerOpts := range c.PeerGuard.AuthProviders {
a, err := authProvider(llger, ap, providerOpts)
a, err := AuthProvider(llger, ap, providerOpts)
if err != nil {
return opts, vpnOpts, fmt.Errorf("invalid authprovider: %w", err)
}
Expand All @@ -482,6 +482,7 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) {
node.WithNetworkService(
pg.UpdaterService(dur),
pguardian.Challenger(dur, c.PeerGuard.Autocleanup),
pguardian.AutoTrust(dur),
),
node.EnableGenericHub,
node.GenericChannelHandlers(pguardian.ReceiveMessage),
Expand All @@ -497,7 +498,7 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) {
return opts, vpnOpts, nil
}

func authProvider(ll log.StandardLogger, s string, opts map[string]interface{}) (trustzone.AuthProvider, error) {
func AuthProvider(ll log.StandardLogger, s string, opts map[string]interface{}) (trustzone.AuthProvider, error) {
switch strings.ToLower(s) {
case "ecdsa":
pk, exists := opts["private_key"]
Expand Down
3 changes: 3 additions & 0 deletions pkg/node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ type Config struct {

Sealer Sealer
PeerGater Gater

TrustedPeerIDS []string
ProtectedStoreKeys []string
}

type Gater interface {
Expand Down
16 changes: 16 additions & 0 deletions pkg/node/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"io"
mrand "math/rand"
"net"
"slices"

internalCrypto "github.com/mudler/edgevpn/pkg/crypto"

Expand Down Expand Up @@ -253,6 +254,21 @@ func (e *Node) handleEvents(ctx context.Context, inputChannel chan *hub.Message,
continue
}

// If we have enabled trusted arbiter peers
if len(e.config.TrustedPeerIDS) > 0 && e.host.ID().String() != m.SenderID {
Copy link
Owner

@mudler mudler Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would make sense to be consistent here and instantiate the PeerGater with static trusted peer IDs coming from the config.

For instance, the gater constructor could take as arguments a set of trustedPeerIDs coming from the config

func NewPeerGater(relaxed bool) *PeerGater {

and Gate()

func (pg *PeerGater) Gate(n *node.Node, p peer.ID) bool {

could check both in trustedDB (coming from the blockchain) and the static ones (that you are adding) coming from the constructor

Copy link
Contributor Author

@mintyleaf mintyleaf Mar 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have reviewed that now more deeply, as i have returned to edgevpn code while doing LocalAI PR init commit - the thing is, the configured initial trust IDs should work as a switch for authoritative/passive mode, where we either gating messages based on trustDB in authoritative mode, or gating all messages that came not from the initial trusted IDs in passive mode, disabling peergater further, as it don't have priority in that mode

The peergater most likely remains the same, and stays in the scope of working only for blockchain trustDB

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, but this sounds like implementing a whitelisting mechanism which is very much similar to the PeerGater. To keep code concise and clean we could or either expand the PeerGater module to accept a static ids and act it as a whitelister, or have a separate component dedicated to whitelisting (which would be even more clean).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i will expand peergater then, as for now whitelisting is effective only in pair with peergater

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yet, the ProtectedStoreKeys is relying at TrustedPeerIDs, so most likely the separated whitelisting component is needed

// If we are not the trusted one
if !slices.Contains(e.config.TrustedPeerIDS, e.host.ID().String()) {
// If incoming message is not from trusted one
if !slices.Contains(e.config.TrustedPeerIDS, m.SenderID) {
e.config.Logger.Warnf("%s gated room message from %s - not present in trusted peer IDS", e.host.ID(), m.SenderID)
continue
} else {
// If we a non-trusted peer, and we receive a meesage from the trusted one - disable peerGater
peerGater = false
}
}
}

if peerGater {
if e.config.PeerGater != nil && e.config.PeerGater.Gate(e, peer.ID(m.SenderID)) {
e.config.Logger.Warnf("gated message from %s", m.SenderID)
Expand Down
7 changes: 7 additions & 0 deletions pkg/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package node
import (
"context"
"fmt"
"slices"
"sync"
"time"

Expand Down Expand Up @@ -123,6 +124,12 @@ func (e *Node) Start(ctx context.Context) error {
return err
}

if len(e.config.TrustedPeerIDS) > 0 && !slices.Contains(e.config.TrustedPeerIDS, e.host.ID().String()) {
ledger.SkipVerify()
Copy link
Owner

@mudler mudler Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why skipping verification of blockchain blocks? this smells a bit off because from what I can see it would be used just to skip blockchain checks. These are more for integrity rather then security (it gates for example nodes to update blockchain versions which are older then what the current node has): by removing that check any node could post a older version of the blockchain, or a different blockchain coming from a different genesis block, and all the nodes skipping verification would accept that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first naive approach was the usage of genesis block for store authored peers from the start, as well, as the list of peer ids for protected keys
Later i have found that we don't really have a way to have everyone synced later, if genesis block differs

So i came with the approach of storing that data in the config, which is encoded as string token later, which providing a secure way to have that data from the start

Yet, later, if authored node is running through API mode, i noticed that healthcheck didn't start by default, so the later joined nodes always fails to sync, since the indexes are dramatically out of sync
Later i found that basically any way later joined node will fail to sync with the main blockchain view, because its hash never match, since it storing self view with unique healthchecks first, and in terms of indexes it will never get enough updates from self side to have index match later

So if the authoritarian trusted nodes base is present from the start - they will guarantee the proper view at blockchain, and every regular client most likely need to accept the blocks from them without any checks (since they filter out any other, than trusted nodes messages at top level)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I would then remove these bits as they are replaced by having static nodes via config, no?

The other way around I see it, the nodes having this set should periodically try to reconcile to the ledger the information if missing.

Also: I think you found something missing there, in api mode only healthcheck should run regularly to keep everything update, that probably requires a fix on its own

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, either we need to have a reconciliation mechanism implemented, still having priority over trusted nodes vs. block validation
Either we having a full passive mode, like i did, which is more simple to implement

We can still verify blocks for everyone, if we haven't defined the trusted nodes, so i would not remove that bits if i understand you correctly, because edgevpn still can be used as fully decentralized non-authoritarian VPN this way (yet, i see that later joined peers never go in sync that way, but for now that is needed only for healthchecks and other simple metadata)

About healthchecks in API mode - they can be enabled through flag --enable-healthchecks, so that is looking more like on purpose for me

}
ledger.SetTrustedPeerIDS(e.config.TrustedPeerIDS)
ledger.SetProtectedStoreKeys(e.config.ProtectedStoreKeys)

// Send periodically messages to the channel with our blockchain content
ledger.Syncronizer(ctx, e.config.LedgerSyncronizationTime)

Expand Down
5 changes: 5 additions & 0 deletions pkg/node/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ type YAMLConnectionConfig struct {
Rendezvous string `yaml:"rendezvous"`
MDNS string `yaml:"mdns"`
MaxMessageSize int `yaml:"max_message_size"`

TrustedPeerIDS []string `yaml:"trusted_peer_ids"`
ProtectedStoreKeys []string `yaml:"protected_store_keys"`
}

// Base64 returns the base64 string representation of the connection
Expand Down Expand Up @@ -301,6 +304,8 @@ func (y YAMLConnectionConfig) copy(mdns, dht bool, cfg *Config, d *discovery.DHT
}
cfg.SealKeyLength = y.OTP.Crypto.Length
cfg.MaxMessageSize = y.MaxMessageSize
cfg.TrustedPeerIDS = y.TrustedPeerIDS
cfg.ProtectedStoreKeys = y.ProtectedStoreKeys
}

const defaultKeyLength = 43
Expand Down
11 changes: 6 additions & 5 deletions pkg/trustzone/authprovider/ecdsa/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ func ECDSA521Provider(ll log.StandardLogger, privkey string) (*ECDSA521, error)
// It cycles over all the Trusted zone Auth data ( providers options, not where senders ID are stored)
// and detects any key with ecdsa prefix. Values are assumed to be string and parsed as pubkeys.
// The pubkeys are then used to authenticate nodes and verify if any of the pubkeys validates the challenge.
func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[string]blockchain.Data) bool {
func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[string]blockchain.Data) (bool, string) {

sigs, ok := m.Annotations["sigs"]
if !ok {
e.logger.Debug("No signature in message", m.Message, m.Annotations)

return false
return false, ""
}

e.logger.Debug("ECDSA auth Received", m)
Expand All @@ -67,17 +67,17 @@ func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[
if len(pubKeys) == 0 {
e.logger.Debug("ECDSA auth: No pubkeys to auth against")
// no pubkeys to authenticate present in the ledger
return false
return false, ""
}
for _, pubkey := range pubKeys {
// Try verifying the signature
if err := verify([]byte(pubkey), []byte(fmt.Sprint(sigs)), bytes.NewBufferString(m.Message)); err == nil {
e.logger.Debug("ECDSA auth: Signature verified")
return true
return true, pubkey
}
e.logger.Debug("ECDSA auth: Signature not verified")
}
return false
return false, ""
}

// Challenger sends ECDSA521 challenges over the public channel if the current node is not in the trusted zone.
Expand All @@ -93,6 +93,7 @@ func (e *ECDSA521) Challenger(inTrustZone bool, c node.Config, n *node.Node, b *
msg := hub.NewMessage("challenge")
msg.Annotations = make(map[string]interface{})
msg.Annotations["sigs"] = string(signature)
msg.SenderID = n.Host().ID().String()
n.PublishMessage(msg)
return
}
Expand Down
Loading
Loading