Browse Source

:rocket: Add experimental DNS on-chain service

Ettore Di Giacinto 3 years ago
parent
commit
429ac0068a

+ 1 - 1
go.mod

@@ -24,7 +24,7 @@ require (
 	github.com/libp2p/go-libp2p-kad-dht v0.15.0
 	github.com/libp2p/go-libp2p-pubsub v0.6.0
 	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/miekg/dns v1.1.45 // indirect
+	github.com/miekg/dns v1.1.45
 	github.com/mudler/go-isterminal v0.0.0-20211031135732-5e4e06fc5a58
 	github.com/mudler/go-processmanager v0.0.0-20211226182900-899fbb0b97f6
 	github.com/multiformats/go-base32 v0.0.4 // indirect

+ 0 - 5
pkg/node/message.go

@@ -34,11 +34,6 @@ func (mw *messageWriter) Write(p []byte) (n int, err error) {
 	return mw.Send(mw.mess.WithMessage(string(p)))
 }
 
-// WriteString writes a string to the message channel
-func (mw *messageWriter) WriteString(p string) (n int, err error) {
-	return mw.Send(mw.mess.WithMessage(p))
-}
-
 // Send sends a message to the channel
 func (mw *messageWriter) Send(copy *hub.Message) (n int, err error) {
 	mw.input <- copy

+ 26 - 0
pkg/node/node_test.go

@@ -50,6 +50,32 @@ var _ = Describe("Node", func() {
 			}, 100*time.Second, 1*time.Second).Should(ContainElement(e2.Host().ID()))
 		})
 
+		It("nodes can write to the ledger", func() {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			e := New(FromBase64(true, true, token), WithStore(&blockchain.MemoryStore{}), l)
+			e2 := New(FromBase64(true, true, token), WithStore(&blockchain.MemoryStore{}), l)
+
+			e.Start(ctx)
+			e2.Start(ctx)
+
+			l, err := e.Ledger()
+			Expect(err).ToNot(HaveOccurred())
+			l2, err := e2.Ledger()
+			Expect(err).ToNot(HaveOccurred())
+
+			l.Announce(ctx, 1*time.Second, func() { l.Add("foo", map[string]interface{}{"bar": "baz"}) })
+
+			Eventually(func() string {
+				var s string
+				v, exists := l2.GetKey("foo", "bar")
+				if exists {
+					v.Unmarshal(&s)
+				}
+				return s
+			}, 100*time.Second, 1*time.Second).Should(Equal("baz"))
+		})
 	})
 
 	Context("connection gater", func() {

+ 3 - 0
pkg/services/alive.go

@@ -43,6 +43,9 @@ func Alive(announcetime, scrubTime time.Duration) []node.Option {
 
 						// Keep-alive scrub
 						nodes := AvailableNodes(b)
+						if len(nodes) == 0 {
+							return
+						}
 						lead := utils.Leader(nodes)
 						if !t.Add(scrubTime).After(time.Now()) {
 							// Update timer so not-leader do not attempt to delete bucket afterwards

+ 144 - 0
pkg/services/dns.go

@@ -0,0 +1,144 @@
+// Copyright © 2021-2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package services
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/miekg/dns"
+	"github.com/mudler/edgevpn/pkg/blockchain"
+	"github.com/mudler/edgevpn/pkg/node"
+	"github.com/pkg/errors"
+)
+
+const (
+	DNSKey string = "dns"
+)
+
+// DNS returns a network service binding a dns blockchain resolver on listenAddr.
+// Takes an associated name for the addresses in the blockchain
+func DNS(listenAddr string, forwarder bool, forward []string) []node.Option {
+	return []node.Option{
+		node.WithNetworkService(
+			func(ctx context.Context, c node.Config, n *node.Node, b *blockchain.Ledger) error {
+
+				server := &dns.Server{Addr: listenAddr, Net: "udp"}
+
+				go func() {
+					dns.HandleFunc(".", dnsHandler{ctx, b, forwarder, forward}.handleDNSRequest())
+					fmt.Println(server.ListenAndServe())
+				}()
+
+				go func() {
+					<-ctx.Done()
+					server.Shutdown()
+				}()
+
+				return nil
+			},
+		),
+	}
+}
+
+func AnnounceDomain(ctx context.Context, b *blockchain.Ledger, announcetime, timeout time.Duration, record, ip string) {
+	b.Announce(ctx, announcetime, func() {
+		b.Add(DNSKey, map[string]interface{}{fmt.Sprintf("%s.", record): ip})
+	})
+}
+
+type dnsHandler struct {
+	ctx       context.Context
+	b         *blockchain.Ledger
+	forwarder bool
+	forward   []string
+}
+
+func (d dnsHandler) parseQuery(m *dns.Msg) {
+	for _, q := range m.Question {
+		// Resolve the entry to an IP from the blockchain data
+		switch q.Qtype {
+		case dns.TypeA, dns.TypeAAAA:
+			if v, exists := d.b.GetKey(DNSKey, q.Name); exists {
+				var res string
+				v.Unmarshal(&res)
+				rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, dns.TypeToString[q.Qtype], res))
+				if err == nil {
+					m.Answer = append(m.Answer, rr)
+				}
+			} else if d.forwarder {
+				r, err := d.forwardQuery(m)
+				if err == nil {
+					m.Answer = r.Answer
+				}
+			}
+		}
+	}
+}
+
+func (d dnsHandler) handleDNSRequest() func(w dns.ResponseWriter, r *dns.Msg) {
+	return func(w dns.ResponseWriter, r *dns.Msg) {
+		m := new(dns.Msg)
+		m.SetReply(r)
+		m.Compress = false
+
+		switch r.Opcode {
+		case dns.OpcodeQuery:
+			d.parseQuery(m)
+		}
+
+		w.WriteMsg(m)
+	}
+}
+
+func (d dnsHandler) forwardQuery(dnsMessage *dns.Msg) (*dns.Msg, error) {
+	for _, server := range d.forward {
+		mess := new(dns.Msg)
+		mess.Question = dnsMessage.Copy().Question
+		r, err := QueryDNS(d.ctx, mess, server)
+		if err != nil {
+			return nil, err
+		}
+		if r == nil || r.Rcode == dns.RcodeNameError || r.Rcode == dns.RcodeSuccess {
+			return r, err
+		}
+	}
+	return nil, errors.New("not available")
+}
+
+// Queries a dns server with a dns message
+func QueryDNS(ctx context.Context, msg *dns.Msg, dnsServer string) (*dns.Msg, error) {
+	c := new(dns.Conn)
+	cc, _ := (&net.Dialer{Timeout: 35 * time.Second}).DialContext(ctx, "udp", dnsServer)
+	c.Conn = cc
+	defer c.Close()
+
+	err := c.SetWriteDeadline(time.Now().Add(30 * time.Second))
+	if err != nil {
+		return nil, err
+	}
+	err = c.WriteMsg(msg)
+	if err != nil {
+		return nil, err
+	}
+	err = c.SetReadDeadline(time.Now().Add(30 * time.Second))
+	if err != nil {
+		return nil, err
+	}
+	return c.ReadMsg()
+}

+ 99 - 0
pkg/services/dns_test.go

@@ -0,0 +1,99 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package services_test
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/ipfs/go-log"
+	"github.com/miekg/dns"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/mudler/edgevpn/pkg/blockchain"
+	"github.com/mudler/edgevpn/pkg/logger"
+	node "github.com/mudler/edgevpn/pkg/node"
+	. "github.com/mudler/edgevpn/pkg/services"
+)
+
+var _ = Describe("DNS service", func() {
+	token := node.GenerateNewConnectionData().Base64()
+
+	logg := logger.New(log.LevelDebug)
+	l := node.Logger(logg)
+
+	e2 := node.New(
+		append(Alive(15*time.Second, 90*time.Minute),
+			node.FromBase64(true, true, token), node.WithStore(&blockchain.MemoryStore{}), l)...)
+
+	Context("DNS service", func() {
+		It("Set DNS records and can resolve IPs", func() {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			opts := DNS("127.0.0.1:19192", true, []string{"8.8.8.8:53"})
+			opts = append(opts, node.FromBase64(true, true, token), node.WithStore(&blockchain.MemoryStore{}), l)
+			e := node.New(opts...)
+
+			e.Start(ctx)
+			e2.Start(ctx)
+
+			ll, _ := e2.Ledger()
+
+			AnnounceDomain(ctx, ll, 15*time.Second, 10*time.Second, "test.foo", "2.2.2.2")
+
+			Eventually(func() string {
+				var s string
+				dnsMessage := new(dns.Msg)
+				dnsMessage.SetQuestion("google.com.", dns.TypeA)
+
+				r, err := QueryDNS(ctx, dnsMessage, "127.0.0.1:19192")
+				if r != nil {
+					answers := r.Answer
+					for _, a := range answers {
+
+						s = a.String() + s
+					}
+				}
+				if err != nil {
+					fmt.Println(err)
+				}
+				return s
+			}, 230*time.Second, 1*time.Second).Should(ContainSubstring("A"))
+
+			Eventually(func() string {
+				var s string
+				dnsMessage := new(dns.Msg)
+				dnsMessage.SetQuestion("test.foo.", dns.TypeA)
+
+				r, err := QueryDNS(ctx, dnsMessage, "127.0.0.1:19192")
+				if r != nil {
+					answers := r.Answer
+					for _, a := range answers {
+						s = a.String() + s
+					}
+				}
+				if err != nil {
+					fmt.Println(err)
+				}
+				//	r.Answer
+				return s
+			}, 230*time.Second, 1*time.Second).Should(ContainSubstring("2.2.2.2"))
+		})
+	})
+})

+ 55 - 0
pkg/utils/crypto_test.go

@@ -0,0 +1,55 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package utils_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	. "github.com/mudler/edgevpn/pkg/utils"
+)
+
+var _ = Describe("Crypto utilities", func() {
+	Context("AES", func() {
+		It("Encode/decode", func() {
+			key := RandStringRunes(32)
+			message := "foo"
+			k := [32]byte{}
+			copy([]byte(key)[:], k[:32])
+
+			encoded, err := AESEncrypt(message, &k)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(encoded).ToNot(Equal(key))
+			Expect(len(encoded)).To(Equal(62))
+
+			// Encode again
+			encoded2, err := AESEncrypt(message, &k)
+			Expect(err).ToNot(HaveOccurred())
+
+			// should differ
+			Expect(encoded2).ToNot(Equal(encoded))
+
+			// Decrypt and check
+			decoded, err := AESDecrypt(encoded, &k)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(decoded).To(Equal(message))
+
+			decoded, err = AESDecrypt(encoded2, &k)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(decoded).To(Equal(message))
+		})
+	})
+})

+ 35 - 0
pkg/utils/leader_test.go

@@ -0,0 +1,35 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package utils_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	. "github.com/mudler/edgevpn/pkg/utils"
+)
+
+var _ = Describe("Leader utilities", func() {
+	Context("Leader", func() {
+		It("returns the correct leader", func() {
+			Expect(Leader([]string{"a", "b", "c", "d"})).To(Equal("b"))
+			Expect(Leader([]string{"a", "b", "c", "d", "e", "f", "G", "bb"})).To(Equal("b"))
+			Expect(Leader([]string{"a", "b", "c", "d", "e", "f", "G", "bb", "z", "b1", "b2"})).To(Equal("z"))
+			Expect(Leader([]string{"1", "2", "3", "4", "5"})).To(Equal("2"))
+			Expect(Leader([]string{"1", "2", "3", "4", "5", "6", "7", "21", "22"})).To(Equal("22"))
+		})
+	})
+})

+ 31 - 0
pkg/utils/strings_test.go

@@ -0,0 +1,31 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package utils_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	. "github.com/mudler/edgevpn/pkg/utils"
+)
+
+var _ = Describe("String utilities", func() {
+	Context("RandStringRunes", func() {
+		It("returns a string with the correct length", func() {
+			Expect(len(RandStringRunes(10))).To(Equal(10))
+		})
+	})
+})