Skip to content

Commit

Permalink
Use blockchain as address table
Browse files Browse the repository at this point in the history
This change rewires thing in a way that we consume less bandwidth.
Instead of broadcasting the interface messages, we broadcast a
blockchain that dynamically tracks records of the address and the nodes
of the network.

The blockchain is sealed and keys are automatically rotated via OTP. The
messages are then sent over p2p network selectively for each stream by
decoding the interface packet, and using the blockchain as resolver
  • Loading branch information
mudler committed Oct 27, 2021
1 parent eeb466a commit ff3292c
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 150 deletions.
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,40 @@

Fully Decentralized. Immutable. Portable. Easy to use Statically compiled VPN

EdgeVPN uses libp2p to connect and create a blockchain between nodes. It keeps the routing table stored in the ledger, while connections are dynamically established via p2p.

## Usage

Generate a config:
Generate a config, and send it over all the nodes you wish to connect:

```bash
./edgevpn -g > config.yaml
```

Run it on multiple hosts:
Run edgevpn on multiple hosts:

```bash
# on Node A
EDGEVPNCONFIG=config.yaml IFACE=edgevpn0 ADDRESS=10.1.0.11/24 ./edgevpn
# on Node B
EDGEVPNCONFIG=config.yaml IFACE=edgevpn0 ADDRESS=10.1.0.12/24 ./edgevpn
# on Node C ...
EDGEVPNCONFIG=config.yaml IFACE=edgevpn0 ADDRESS=10.1.0.13/24 ./edgevpn
...
```

... and that's it!
... and that's it! the `ADDRESS` is a _virtual_ unique IP for each node, and it is actually the ip where the node will be reachable to from the vpn, while `IFACE` is the interface name.

*Note*: It might take up time to build the connection between nodes. Wait at least 5 mins, it depends on the network behind the hosts.

## Architecture

- p2p encryption between peers with libp2p
- randezvous points dynamically generated from OTP keys
- extra AES symmetric encryption on top. In case randezvous point is compromised
- blockchain is used as a sealed encrypted store for the routing table
- connections are created host to host

## Is it for me?

EdgeVPN makes VPN decentralization a first strong requirement.
Expand All @@ -31,8 +44,8 @@ Its mainly use is for edge and low-end devices and especially for development.

The decentralized approach has few cons:

- The underlaying network is chatty. It uses a Gossip protocol and p2p. Every message is broadcasted to all peers.
- Not suited for low latency. On my local tests on very slow connections, ping took ~200ms.
- The underlaying network is chatty. It uses a Gossip protocol for syncronizing the routing table and p2p. Every blockchain message is broadcasted to all peers, while the traffic is to the host only.
- Might be not suited for low latency workload.

Keep that in mind before using it for your prod networks!

Expand Down Expand Up @@ -82,12 +95,6 @@ e.Start()

```

## Architecture

- p2p encryption between peers with libp2p
- randezvous points dynamically generated from OTP keys
- extra AES symmetric encryption on top. In case randezvous point is compromised

## Credits

- The awesome [libp2p](https://github.com/libp2p) library
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ require (
github.com/ipfs/go-ipns v0.1.2 // indirect
github.com/ipfs/go-log/v2 v2.3.0
github.com/kr/text v0.2.0 // indirect
github.com/libp2p/go-libp2p v0.15.0
github.com/libp2p/go-libp2p v0.15.1
github.com/libp2p/go-libp2p-core v0.9.0
github.com/libp2p/go-libp2p-discovery v0.5.1
github.com/libp2p/go-libp2p-kad-dht v0.11.1
github.com/libp2p/go-libp2p-kad-dht v0.12.1
github.com/libp2p/go-libp2p-pubsub v0.5.4
github.com/libp2p/go-libp2p-quic-transport v0.11.2
github.com/libp2p/go-tcp-transport v0.2.8
github.com/libp2p/go-libp2p-quic-transport v0.12.0 // indirect
github.com/lthibault/jitterbug v2.0.0+incompatible
github.com/multiformats/go-multiaddr v0.4.0
github.com/pkg/errors v0.9.1
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/urfave/cli/v2 v2.3.0
github.com/vishvananda/netlink v1.1.0
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
go.opencensus.io v0.23.0 // indirect
go.uber.org/zap v1.19.0
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
gopkg.in/yaml.v2 v2.4.0
)

Expand Down
60 changes: 32 additions & 28 deletions go.sum

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ func main() {
edgevpn.Logger(l),
edgevpn.LogLevel(log.LevelInfo),
edgevpn.MaxMessageSize(2 << 20), // 2MB
edgevpn.WithMTU(1500),
edgevpn.WithInterfaceMTU(1300),
edgevpn.WithInterfaceMTU(1450),
edgevpn.WithPacketMTU(1420),
edgevpn.WithInterfaceAddress(os.Getenv("ADDRESS")),
edgevpn.WithInterfaceName(os.Getenv("IFACE")),
edgevpn.WithInterfaceType(water.TAP),
edgevpn.WithMaxBlockChainSize(1000),
edgevpn.WithInterfaceType(water.TUN),
edgevpn.NetLinkBootstrap(true),
}

Expand Down Expand Up @@ -96,4 +97,7 @@ func main() {
if err := e.Start(); err != nil {
l.Sugar().Fatal(err.Error())
}

for {
}
}
66 changes: 66 additions & 0 deletions pkg/blockchain/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package blockchain

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)

// Block represents each 'item' in the blockchain
type Block struct {
Index int
Timestamp string
AddressMap map[string]string
Hash string
PrevHash string
}

// Blockchain is a series of validated Blocks
type Blockchain []Block

func (b Blockchain) IsMoreRecent(bb Blockchain) bool {
return len(b) > len(bb) || len(b) == len(bb) && b[len(b)-1].Hash != bb[len(bb)-1].Hash
}

// make sure block is valid by checking index, and comparing the hash of the previous block
func (newBlock Block) IsValid(oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}

if oldBlock.Hash != newBlock.PrevHash {
return false
}

if newBlock.Checksum() != newBlock.Hash {
return false
}

return true
}

// Checksum does SHA256 hashing of the block
func (b Block) Checksum() string {
record := fmt.Sprint(b.Index, b.Timestamp, b.AddressMap, b.PrevHash)
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}

// create a new block using previous block's hash
func (oldBlock Block) NewBlock(s map[string]string) Block {

var newBlock Block

t := time.Now()

newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.AddressMap = s
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = newBlock.Checksum()

return newBlock
}
176 changes: 176 additions & 0 deletions pkg/blockchain/ledger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package blockchain

import (
"context"
"encoding/json"
"io"
"log"
"sync"
"time"

"github.com/mudler/edgevpn/pkg/hub"
)

type Ledger struct {
sync.Mutex
Blockchain Blockchain

maxChainSize int
channel io.Writer
}

// New returns a new ledger which writes to the writer
func New(w io.Writer, maxChainSize int) *Ledger {
c := &Ledger{channel: w, maxChainSize: maxChainSize}
c.newGenesis()
return c
}

func (l *Ledger) newGenesis() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), map[string]string{}, genesisBlock.Checksum(), ""}
l.Blockchain = append(l.Blockchain, genesisBlock)
}

// Syncronizer starts a goroutine which
// writes the blockchain to the periodically
func (l *Ledger) Syncronizer(ctx context.Context, t time.Duration) {
go func() {
t := time.NewTicker(t)
defer t.Stop()
for {
select {
case <-t.C:
l.Lock()
bytes, err := json.Marshal(l.Blockchain)
if err != nil {
log.Println(err)
}
l.channel.Write(bytes)

// Reset blockchain if we exceed chainsize
if l.maxChainSize != 0 && len(l.Blockchain) > l.maxChainSize {
l.Blockchain = []Block{}
}
l.Unlock()
case <-ctx.Done():
return
}
}
}()
}

// String returns the blockchain as string
func (l *Ledger) String() string {
bytes, _ := json.MarshalIndent(l.Blockchain, "", " ")
return string(bytes)
}

// Update the blockchain from a message
func (l *Ledger) Update(h *hub.Message) (err error) {
chain := make(Blockchain, 0)

err = json.Unmarshal([]byte(h.Message), &chain)
if err != nil {
return
}

l.Lock()
if chain.IsMoreRecent(l.Blockchain) {
l.Blockchain = chain
}
l.Unlock()

return
}

// Persist an async data to the blockchain.
// Sends a broadcast at the specified interval
// by making sure the async retrieved value is written to the
// blockchain
func (l *Ledger) Persist(ctx context.Context, t time.Duration, key string, async func() string) {
go func() {
t := time.NewTicker(t)
defer t.Stop()
for {
select {
case <-t.C:
value := async()
// Retrieve current ID for ip in the blockchain
existingValue, found := l.GetKey(key)
// If mismatch, update the blockchain
if !found || existingValue != value {
updatedMap := map[string]string{}
updatedMap[key] = value
l.Add(updatedMap)
}
case <-ctx.Done():
return
}
}
}()
}

func (l *Ledger) lastBlock() Block {
return (l.Blockchain[len(l.Blockchain)-1])
}

// GetKey retrieve the current key from the blockchain
func (l *Ledger) GetKey(s string) (value string, exists bool) {
l.Lock()
defer l.Unlock()
if len(l.Blockchain) > 0 {
last := l.lastBlock()
value, exists = last.AddressMap[s]
if exists {
return
}
}

return
}

// ExistsValue returns true if there is one element with a matching value
func (l *Ledger) ExistsValue(v string) (exists bool) {
l.Lock()
defer l.Unlock()
if len(l.Blockchain) > 0 {
for _, bv := range l.lastBlock().AddressMap {
if bv == v {
exists = true
return
}
}
}

return
}

// Add data to the blockchain
func (l *Ledger) Add(s map[string]string) {
l.Lock()
current := l.lastBlock().AddressMap
for s, k := range s {
current[s] = k
}
l.Unlock()
l.writeData(current)
}

func (l *Ledger) writeData(s map[string]string) {
newBlock := l.lastBlock().NewBlock(s)

if newBlock.IsValid(l.lastBlock()) {
l.Lock()
l.Blockchain = append(l.Blockchain, newBlock)
l.Unlock()
}

bytes, err := json.Marshal(l.Blockchain)
if err != nil {
log.Println(err)
}

l.channel.Write(bytes)
}
Loading

0 comments on commit ff3292c

Please sign in to comment.