gnunet-go

GNUnet Bindings for Go
Log | Files | Refs | README | LICENSE

commit fef7570713f8ec064b7689f28f201deb0b5030d4
parent 24250ab0325f4217f517a223aa225db7de2833b5
Author: Bernd Fix <brf@hoi-polloi.org>
Date:   Tue, 12 Nov 2019 12:31:44 +0100

Started recursive resolution; GNS2DNS implemented.

Diffstat:
MREADME.md | 34+++++++++++++++++++++++++++++-----
Msrc/cmd/vanityid/main.go | 11+++++++++--
Msrc/gnunet/config/config.go | 19+++++++++++++++++--
Msrc/gnunet/config/gnunet-config.json | 5++++-
Msrc/gnunet/crypto/symmetric.go | 20++++++++++++++++++++
Asrc/gnunet/modules.go | 40++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/gns/crypto.go | 27++++++++++-----------------
Asrc/gnunet/service/gns/dns.go | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/gns/module.go | 342+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/gns/record.go | 5+++++
Asrc/gnunet/service/gns/service.go | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/namecache/module.go | 25+++++++++++++++++++++++++
Msrc/gnunet/util/array.go | 39+++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/util/time.go | 17+++++++++++++++++
Atest.sh | 3+++
15 files changed, 1015 insertions(+), 27 deletions(-)

diff --git a/README.md b/README.md @@ -22,7 +22,7 @@ DOCUMENTATION OR COMPILABLE, RUNNABLE OR EVEN OPERATIONAL SOURCE CODE. ## Source code -All source code is written in Golang (version 1.11+). +All source code is written in Golang (version 1.13+). ### Dependencies @@ -38,19 +38,43 @@ $ go get -u github.com/bfix/gospel/... ### ./src/cmd folder -* `vanityid`: Compute GNUnet vanity peer id for a given regexp pattern. + +#### `gnunet-service-gns-go`: Implementation of the GNS service. + +#### `peer_mockup`: test message exchange on the lowest level (transport). + +#### `vanityid`: Compute GNUnet vanity peer and ego id for a given regexp pattern. + +N.B.: Key generation is slow at the moment, so be patient! To generate a single +matching key some 1,000,000 keys need to be generated for a four letter prefix; +this can take more than 30 minutes on average (depending on your CPU). ```bash $ vanityid "^TST[0-9]" ``` -* `gnunet-service-gns-go`: Implementation of the GNS service. +Keys matching the pattern are printed to the console in the following format: -* `peer_mockup`: test message exchange on the lowest level (transport). +```bash +<vanity_id> [<hex.seed>][<hex.scalar>] (<count> tries, <time> elapsed) +``` +The value of `count` tells how many key had been generated before a match was +found; `time` is the time needed to find a match. + +To generate the key files, make sure GNUnet **is not running** and do: +```bash +$ # use a vanity peer id: +$ echo "<hex.seed>" | xxd -r -p > /var/lib/gnunet/.local/share/gnunet/private_key.ecc +$ sudo chown gnunet:gnunet /var/lib/gnunet/.local/share/gnunet/private_key.ecc +$ sudo chmod 600 /var/lib/gnunet/.local/share/gnunet/private_key.ecc +$ # use a vanity ego id: +$ echo "<hex.scalar>" | xxd -r -p > ~/.local/share/gnunet/identity/egos/<vanity_ego> +$ chmod 600 ~/.local/share/gnunet/identity/egos/<vanity_ego> +``` ### ./src/gnunet folder -Packages used to implement GNUnet protocols (currently only TRANSPORT +Packages used to implement GNUnet protocols (currently only some of TRANSPORT and GNS). ## Documentation diff --git a/src/cmd/vanityid/main.go b/src/cmd/vanityid/main.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "regexp" + "time" "github.com/bfix/gospel/crypto/ed25519" "gnunet/util" @@ -29,7 +30,8 @@ func main() { // generate new keys in a loop seed := make([]byte, 32) - for { + start := time.Now() + for i := 0; ; i++ { n, err := rand.Read(seed) if err != nil || n != 32 { panic(err) @@ -39,7 +41,12 @@ func main() { id := util.EncodeBinaryToString(pub) for _, r := range reg { if r.MatchString(id) { - fmt.Printf("%s [%s]\n", id, hex.EncodeToString(seed)) + elapsed := time.Now().Sub(start) + s1 := hex.EncodeToString(seed) + s2 := hex.EncodeToString(prv.D.Bytes()) + fmt.Printf("%s [%s][%s] (%d tries, %s elapsed)\n", id, s1, s2, i, elapsed) + i = 0 + start = time.Now() } } } diff --git a/src/gnunet/config/config.go b/src/gnunet/config/config.go @@ -7,7 +7,9 @@ import ( "regexp" "strings" + "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/logger" + "gnunet/util" ) /////////////////////////////////////////////////////////////////////// @@ -15,8 +17,21 @@ import ( // GNSConfig type GNSConfig struct { - Endpoint string `json:"endpoint"` // end-point of GNS service - DHTReplLevel int `json:"dhtReplLevel"` // DHT replication level + Endpoint string `json:"endpoint"` // end-point of GNS service + DHTReplLevel int `json:"dhtReplLevel"` // DHT replication level + RootZones map[string]string `json:"rootZones"` // pre-configured root zones +} + +// GetRootZoneKey returns the zone key (PKEY) for a pre-configured root with given name. +func (gns *GNSConfig) GetRootZoneKey(name string) *ed25519.PublicKey { + // lookup key in the dictionary + if dStr, ok := gns.RootZones[name]; ok { + if data, err := util.DecodeStringToBinary(dStr, 32); err == nil { + return ed25519.NewPublicKeyFromBytes(data) + } + } + // no pkey found. + return nil } /////////////////////////////////////////////////////////////////////// diff --git a/src/gnunet/config/gnunet-config.json b/src/gnunet/config/gnunet-config.json @@ -8,7 +8,10 @@ }, "gns": { "endpoint": "unix+${RT_SYS}/gnunet-service-gns-go.sock+perm=0770", - "dhtReplLevel": 10 + "dhtReplLevel": 10, + "rootZones": { + "home": "ACAB23DC3SEECJORPHQNVRH965A6N74B1M37S721IG4RBQ15PJLL" + } }, "namecache": { "endpoint": "unix+${RT_SYS}/gnunet-service-namecache.sock" diff --git a/src/gnunet/crypto/symmetric.go b/src/gnunet/crypto/symmetric.go @@ -57,3 +57,23 @@ func SymmetricDecrypt(data []byte, skey *SymmetricKey, iv *SymmetricIV) ([]byte, stream.XORKeyStream(out, out) return out, nil } + +func SymmetricEncrypt(data []byte, skey *SymmetricKey, iv *SymmetricIV) ([]byte, error) { + // Encrypt with AES CFB stream cipher + aes, err := aes.NewCipher(skey.AESKey) + if err != nil { + return nil, err + } + stream := cipher.NewCFBEncrypter(aes, iv.AESIv) + out := make([]byte, len(data)) + stream.XORKeyStream(out, data) + + // Encrypt with Twofish CFB stream cipher + tf, err := twofish.NewCipher(skey.TwofishKey) + if err != nil { + return nil, err + } + stream = cipher.NewCFBEncrypter(tf, iv.TwofishIv) + stream.XORKeyStream(out, out) + return out, nil +} diff --git a/src/gnunet/modules.go b/src/gnunet/modules.go @@ -0,0 +1,40 @@ +//====================================================================== +// Standalone (all-in-one) implementation of GNUnet: +// ------------------------------------------------- +// Instead of running GNUnet services like GNS or DHT in separate +// processes communicating (exchanging messages) with each other over +// Unix Domain Sockets, the standalone implementation combines all +// service modules into a single binary running go-routines to +// concurrently performing their tasks. +//====================================================================== + +package gnunet + +import ( + "gnunet/service/gns" + "gnunet/service/namecache" +) + +// List of all GNUnet service module instances +type Instances struct { + GNS *gns.GNSModule + Namecache *namecache.NamecacheModule +} + +// Local reference to instance list +var ( + Modules Instances +) + +// Initialize instance list and link module functions as required. +func init() { + + // Namecache (no calls to other modules) + Modules.Namecache = new(namecache.NamecacheModule) + + // GNS (calls Namecache, DHT and Identity) + Modules.GNS = &gns.GNSModule{ + LookupLocal: Modules.Namecache.Get, + StoreLocal: Modules.Namecache.Put, + } +} diff --git a/src/gnunet/service/gns/crypto.go b/src/gnunet/service/gns/crypto.go @@ -9,23 +9,8 @@ import ( "golang.org/x/crypto/hkdf" ) -// QueryFromPublickeyDerive calculates the DHT query for a given label in a -// given zone (identified by PKEY). -func QueryFromPublickeyDerive(pkey *ed25519.PublicKey, label string) *crypto.HashCode { - pd := crypto.DerivePublicKey(pkey, label, "gns") - return crypto.Hash(pd.Bytes()) -} - -// DecryptBlock -func DecryptBlock(data []byte, zoneKey *ed25519.PublicKey, label string) (out []byte, err error) { - // derive key material for decryption - iv, skey := deriveBlockKey(label, zoneKey) - // perform decryption - return crypto.SymmetricDecrypt(data, skey, iv) -} - -// Derive a symmetric key to decipher a GNS block -func deriveBlockKey(label string, pub *ed25519.PublicKey) (iv *crypto.SymmetricIV, skey *crypto.SymmetricKey) { +// DeriveBlockKey returns a symmetric key to decipher a GNS block +func DeriveBlockKey(label string, pub *ed25519.PublicKey) (iv *crypto.SymmetricIV, skey *crypto.SymmetricKey) { // generate symmetric key prk := hkdf.Extract(sha512.New, []byte(label), pub.Bytes()) rdr := hkdf.Expand(sha256.New, prk, []byte("gns-aes-ctx-key")) @@ -39,3 +24,11 @@ func deriveBlockKey(label string, pub *ed25519.PublicKey) (iv *crypto.SymmetricI rdr.Read(iv.TwofishIv) return } + +// DecryptBlock for a given zone and label. +func DecryptBlock(data []byte, zoneKey *ed25519.PublicKey, label string) (out []byte, err error) { + // derive key material for decryption + iv, skey := DeriveBlockKey(label, zoneKey) + // perform decryption + return crypto.SymmetricDecrypt(data, skey, iv) +} diff --git a/src/gnunet/service/gns/dns.go b/src/gnunet/service/gns/dns.go @@ -0,0 +1,198 @@ +package gns + +import ( + "fmt" + "net" + "strings" + "time" + + "gnunet/enums" + "gnunet/message" + "gnunet/util" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/logger" + "github.com/miekg/dns" +) + +// Error codes +var ( + ErrDNSTimedOut = fmt.Errorf("DNS query timed out") + ErrNoDNSQueries = fmt.Errorf("No valid DNS queries") + ErrNoDNSResults = fmt.Errorf("No valid DNS results") +) + +// Convert DNS name from its binary representation [RFC1034]: +// A string is a sequence of a (len,chars...) tupels terminated by a (len=0,). +// The name parts are concatenated with "." as separator. +// The parsing starts at offset in the byte array; the function returns the +// offset after the parsed name as well as the name itself. +func DNSNameFromBytes(b []byte, offset int) (int, string) { + if offset >= len(b) { + return offset, "" + } + str := "" + pos := offset + for b[pos] != 0 { + if len(str) > 0 { + str += "." + } + count := int(b[pos]) + pos++ + str += string(b[pos : pos+count]) + pos += count + } + return pos + 1, str +} + +// queryDNS resolves a name on a given nameserver and delivers all matching +// resource record (of type 'kind') to the result channel. +func queryDNS(id int, name string, server net.IP, kind int, res chan *GNSRecordSet) { + logger.Printf(logger.DBG, "[dns][%d] Starting query for '%s' on '%s'...\n", id, name, server.String()) + + // assemble query + m := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Authoritative: true, + AuthenticatedData: false, + CheckingDisabled: false, + RecursionDesired: true, + Opcode: dns.OpcodeQuery, + }, + Question: make([]dns.Question, 1), + } + m.Question[0] = dns.Question{ + dns.Fqdn(name), + dns.TypeANY, + dns.ClassINET, + } + + // perform query in retry-loop + for retry := 0; retry < 5; retry++ { + // send query with new ID when retrying + m.Id = dns.Id() + in, err := dns.Exchange(m, net.JoinHostPort(server.String(), "53")) + // handle DNS fails + if err != nil { + errMsg := err.Error() + if strings.HasSuffix(errMsg, "i/o timeout") { + logger.Printf(logger.WARN, "[dns][%d] Query timed-out -- retrying (%d/5)\n", id, retry+1) + continue + } + logger.Printf(logger.ERROR, "[dns][%d] Error: %s\n", id, errMsg) + res <- nil + } + // process results + logger.Printf(logger.WARN, "[dns][%d] Response from DNS server received (%d/5).\n", id, retry+1) + if in == nil { + logger.Printf(logger.ERROR, "[dns][%d] No results\n", id) + res <- nil + return + } + set := NewGNSRecordSet() + for _, record := range in.Answer { + // create a new GNS resource record + rr := new(message.GNSResourceRecord) + rr.Expires = util.AbsoluteTimeNever() + rr.Flags = 0 + rr.Type = uint32(record.Header().Rrtype) + rr.Size = uint32(record.Header().Rdlength) + rr.Data = make([]byte, rr.Size) + + // get wire-format of resource record + buf := make([]byte, 2048) + n, err := dns.PackRR(record, buf, 0, nil, false) + if err != nil { + logger.Printf(logger.WARN, "[dns][%d] Failed to get RR data for %s\n", id, err.Error()) + continue + } + if n < int(rr.Size) { + logger.Printf(logger.WARN, "[dns][%d] Nit enough data in RR (%d != %d)\n", id, n, rr.Size) + continue + } + copy(rr.Data, buf[n-int(rr.Size):]) + set.AddRecord(rr) + } + logger.Printf(logger.WARN, "[dns][%d] %d resource records extracted from response (%d/5).\n", id, set.Count, retry+1) + res <- set + return + } + logger.Printf(logger.WARN, "[dns][%d] Resolution failed -- giving up...\n", id) + res <- nil +} + +// ResolveDNS resolves a name in DNS. Multiple DNS servers are queried in +// parallel; the first result delivered by any of the servers is returned +// as the result list of matching resource records. +func (gns *GNSModule) ResolveDNS(name string, servers []string, kind int, pkey *ed25519.PublicKey) (set *GNSRecordSet, err error) { + logger.Printf(logger.DBG, "[dns] Resolution of '%s' starting...\n", name) + + // start DNS queries concurrently + res := make(chan *GNSRecordSet) + running := 0 + for idx, srv := range servers { + // check if srv is an IPv4/IPv6 address + addr := net.ParseIP(srv) + if addr == nil { + // no; resolve server name in GNS + if strings.HasSuffix(srv, ".+") { + // resolve server name relative to current zone + zone := util.EncodeBinaryToString(pkey.Bytes()) + srv = strings.TrimSuffix(srv, ".+") + set, err = gns.Resolve(srv, pkey, enums.GNS_TYPE_ANY, enums.GNS_LO_DEFAULT) + if err != nil { + logger.Printf(logger.ERROR, "[dns] Can't resolve NS server '%s' in '%s'\n", srv, zone) + continue + } + } else { + // resolve absolute GNS name (MUST end in a PKEY) + set, err = gns.Resolve(srv, nil, enums.GNS_TYPE_ANY, enums.GNS_LO_DEFAULT) + if err != nil { + logger.Printf(logger.ERROR, "[dns] Can't resolve NS server '%s'\n", srv) + continue + } + } + // traverse resource records for 'A' and 'AAAA' records. + rec_loop: + for _, rec := range set.Records { + switch int(rec.Type) { + case enums.GNS_TYPE_DNS_AAAA: + addr = net.IP(rec.Data) + break rec_loop + case enums.GNS_TYPE_DNS_A: + addr = net.IP(rec.Data) + } + } + } + // query DNS concurrently + go queryDNS(idx, name, addr, kind, res) + running++ + } + // check if we started some queries at all. + if running == 0 { + return nil, ErrNoDNSQueries + } + // wait for query results + timeout := time.Tick(10 * time.Second) + for { + select { + case set = <-res: + running-- + if set != nil { + // we have a result. + logger.Println(logger.DBG, "[dns] Query result available.") + return + } + if running == 0 { + // no results + logger.Println(logger.WARN, "[dns] No results received from queries.") + return nil, ErrNoDNSResults + } + + case <-timeout: + // no results + logger.Println(logger.WARN, "[dns] Queries timed out.") + return nil, ErrNoDNSResults + } + } +} diff --git a/src/gnunet/service/gns/module.go b/src/gnunet/service/gns/module.go @@ -0,0 +1,342 @@ +package gns + +import ( + "encoding/hex" + "fmt" + "strings" + + "gnunet/config" + "gnunet/crypto" + "gnunet/enums" + "gnunet/message" + "gnunet/util" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/logger" +) + +//====================================================================== +// "GNUnet Name System" implementation +//====================================================================== + +// Error codes +var ( + ErrUnknownTLD = fmt.Errorf("Unknown TLD in name") + ErrInvalidRecordType = fmt.Errorf("Invalid resource record type") + ErrInvalidRecordBody = fmt.Errorf("Invalid resource record body") + ErrInvalidPKEY = fmt.Errorf("Invalid PKEY resource record") +) + +//---------------------------------------------------------------------- +// Query for simple GNS lookups +//---------------------------------------------------------------------- + +// Query specifies the context for a basic GNS name lookup of an (atomic) +// label in a given zone identified by its public key. +type Query struct { + Zone *ed25519.PublicKey // Public zone key + Label string // Atomic label + Derived *ed25519.PublicKey // Derived key from (pkey,label) + Key *crypto.HashCode // Key for repository queries (local/remote) +} + +// NewQuery assembles a new Query object for the given zone and label. +func NewQuery(pkey *ed25519.PublicKey, label string) *Query { + // derive a public key from (pkey,label) and set the repository + // key as the SHA512 hash of the binary key representation. + pd := crypto.DerivePublicKey(pkey, label, "gns") + key := crypto.Hash(pd.Bytes()) + return &Query{ + Zone: pkey, + Label: label, + Derived: pd, + Key: key, + } +} + +//---------------------------------------------------------------------- +// GNS blocks with special types (PKEY, GNS2DNS) require special +// treatment with respect to other resource records with different types +// in the same block. Usually only certain other types (or not at all) +// are allowed and the allowed ones are required to deliver a consistent +// list of resulting resource records passed back to the caller. +//---------------------------------------------------------------------- + +// BlockHandler interface. +type BlockHandler interface { + // TypeAction returns a flag indicating how a resource record of a + // given type is to be treated: + // = -1: Record is not allowed (terminates lookup with an error) + // = 0: Record is allowed but will be ignored + // = 1: Record is allowed and will be processed + TypeAction(int) int +} + +// Gns2DnsHandler implementing the BlockHandler interface +type Gns2DnsHandler struct { + Name string + Servers []string +} + +// NewGns2DnsHandler returns a new BlockHandler instance +func NewGns2DnsHandler() *Gns2DnsHandler { + return &Gns2DnsHandler{ + Name: "", + Servers: make([]string, 0), + } +} + +// TypeAction return a flag indicating how a resource record of a given type +// is to be treated (see RecordMaster interface) +func (m *Gns2DnsHandler) TypeAction(t int) int { + // only process other GNS2DNS records + if t == enums.GNS_TYPE_GNS2DNS { + return 1 + } + // skip everything else + return 0 +} + +// AddRequest adds the DNS request for "name" at "server" to the list +// of requests. All GNS2DNS records must query for the same name +func (m *Gns2DnsHandler) AddRequest(name, server string) bool { + if len(m.Servers) == 0 { + m.Name = name + } + if name != m.Name { + return false + } + m.Servers = append(m.Servers, server) + return true +} + +//---------------------------------------------------------------------- +// The GNS module (recursively) resolves GNS names: +// Resolves DNS-like names (e.g. "minecraft.servers.bob.games") to the +// requested resource records (RRs). In short, the resolution process +// works as follows: +// +// Resolve(name): +// -------------- +// (1) split the full name into elements in reverse order: names[] +// (2) Resolve first element (root zone, right-most name part, name[0]) to +// a zone public key PKEY: +// (a) the name is a string representation of a public key -> (3) +// (b) the zone key for the name is stored in the config file -> (3) +// (c) a local zone with that given name -> (3) +// (d) ERROR: "Unknown root zone" +// (3) names = names[1:] // remove first element +// block = Lookup (PKEY, names[0]): +// (a) If last element of namess: -> (4) +// (b) block is PKEY record: +// PKEY <- block, --> (3) +// (4) return block: it is the responsibility of the caller to assemble +// the desired result from block data (e.g. filter for requested +// resource record types). +//---------------------------------------------------------------------- + +// GNSModule handles the resolution of GNS names to RRs bundled in a block. +type GNSModule struct { + // Use function references for calls to methods in other modules: + // + LookupLocal func(query *Query) (*GNSBlock, error) + StoreLocal func(query *Query, block *GNSBlock) error + LookupRemote func(query *Query) (*GNSBlock, error) + GetLocalZone func(name string) (*ed25519.PublicKey, error) +} + +// Resolve a GNS name with multiple elements, If pkey is not nil, the name +// is interpreted as "relative to current zone". +func (gns *GNSModule) Resolve(path string, pkey *ed25519.PublicKey, kind int, mode int) (set *GNSRecordSet, err error) { + // get the name elements in reverse order + names := util.ReverseStringList(strings.Split(path, ".")) + logger.Printf(logger.DBG, "[gns] Resolver called for %v\n", names) + + // check for relative path + if pkey != nil { + //resolve relative path + return gns.ResolveRelative(names, pkey, kind, mode) + } + // resolve absolute path + return gns.ResolveAbsolute(names, kind, mode) +} + +// Resolve a fully qualified GNS absolute name (with multiple levels). +func (gns *GNSModule) ResolveAbsolute(names []string, kind int, mode int) (set *GNSRecordSet, err error) { + // get the root zone key for the TLD + var ( + pkey *ed25519.PublicKey + data []byte + ) + for { + // (1) check if TLD is a public key string + if len(names[0]) == 52 { + if data, err = util.DecodeStringToBinary(names[0], 32); err == nil { + if pkey = ed25519.NewPublicKeyFromBytes(data); pkey != nil { + break + } + } + } + // (2) check if TLD is in our local config + if pkey = config.Cfg.GNS.GetRootZoneKey(names[0]); pkey != nil { + break + } + // (3) check if TLD is one of our identities + if pkey, err = gns.GetLocalZone(names[0]); err == nil { + break + } + // (4) we can't resolve this TLD + return nil, ErrUnknownTLD + } + // continue with resolution relative to a zone. + return gns.ResolveRelative(names[1:], pkey, kind, mode) +} + +// Resolve relative path (to a given zone) recursively by processing simple +// (PKEY,Label) lookups in sequence and handle intermediate GNS record types +func (gns *GNSModule) ResolveRelative(names []string, pkey *ed25519.PublicKey, kind int, mode int) (set *GNSRecordSet, err error) { + // Process all names in sequence + var records []*message.GNSResourceRecord +name_loop: + for ; len(names) > 0; names = names[1:] { + logger.Printf(logger.DBG, "[gns] ResolveRelative '%s' in '%s'\n", names[0], util.EncodeBinaryToString(pkey.Bytes())) + + // resolve next level + var block *GNSBlock + if block, err = gns.Lookup(pkey, names[0], mode == enums.GNS_LO_DEFAULT); err != nil { + // failed to resolve name + return + } + // set new mode after processing right-most label in LOCAL_MASTER mode + if mode == enums.GNS_LO_LOCAL_MASTER { + mode = enums.GNS_LO_DEFAULT + } + // post-process block by inspecting contained resource records for + // special GNS types + var hdlr BlockHandler + if records, err = block.Records(); err != nil { + return + } + for _, rec := range records { + // let a block handler decide how to handle records + if hdlr != nil { + switch hdlr.TypeAction(int(rec.Type)) { + case -1: + // No records of this type allowed in block + err = ErrInvalidRecordType + return + case 0: + // records of this type are simply ignored + continue + case 1: + // process record of this type + } + } + switch int(rec.Type) { + //---------------------------------------------------------- + case enums.GNS_TYPE_PKEY: + // check for single RR and sane key data + if len(rec.Data) != 32 || len(records) > 1 { + err = ErrInvalidPKEY + return + } + // set new PKEY and continue resolution + pkey = ed25519.NewPublicKeyFromBytes(rec.Data) + continue name_loop + + //---------------------------------------------------------- + case enums.GNS_TYPE_GNS2DNS: + // get the master controlling this block; create a new + // one if necessary + var inst *Gns2DnsHandler + if hdlr == nil { + inst = NewGns2DnsHandler() + hdlr = inst + } else { + inst = hdlr.(*Gns2DnsHandler) + } + // extract list of names in DATA block: + logger.Printf(logger.DBG, "[gns] GNS2DNS data: %s\n", hex.EncodeToString(rec.Data)) + var dnsNames []string + for pos := 0; ; { + next, name := DNSNameFromBytes(rec.Data, pos) + if len(name) == 0 { + break + } + dnsNames = append(dnsNames, name) + pos = next + } + logger.Printf(logger.DBG, "[gns] GNS2DNS params: %v\n", dnsNames) + if len(dnsNames) != 2 { + err = ErrInvalidRecordBody + return + } + // Add to collection of requests + logger.Printf(logger.DBG, "[gns] GNS2DNS: query for '%s' on '%s'\n", dnsNames[0], dnsNames[1]) + if !inst.AddRequest(dnsNames[0], dnsNames[1]) { + err = ErrInvalidRecordBody + return + } + } + } + // handle special block cases + if hdlr != nil { + switch inst := hdlr.(type) { + case *Gns2DnsHandler: + // we need to handle delegation to DNS: returns a list of found + // resource records in DNS (filter by 'kind') + fqdn := strings.Join(util.ReverseStringList(names[1:]), ".") + "." + inst.Name + if set, err = gns.ResolveDNS(fqdn, inst.Servers, kind, pkey); err != nil { + logger.Println(logger.ERROR, "[gns] GNS2DNS resilution failed.") + return + } + // we are done with resolution; pass on records to caller + records = set.Records + break name_loop + } + } + } + // Assemble resulting resource record set + set = NewGNSRecordSet() + for _, rec := range records { + // is this the record type we are looking for? + if kind == enums.GNS_TYPE_ANY || int(rec.Type) == kind { + // add it to the result + set.AddRecord(rec) + } + } + return +} + +// Lookup name in GNS. +func (gns *GNSModule) Lookup(pkey *ed25519.PublicKey, label string, remote bool) (block *GNSBlock, err error) { + + // create query (lookup key) + query := NewQuery(pkey, label) + + // try local lookup first + if block, err = gns.LookupLocal(query); err != nil { + logger.Printf(logger.ERROR, "[gns] local Lookup: %s\n", err.Error()) + block = nil + return + } + if block == nil { + logger.Println(logger.DBG, "[gns] local Lookup: no block found") + if remote { + // get the block from a remote lookup + if block, err = gns.LookupRemote(query); err != nil || block == nil { + if err != nil { + logger.Printf(logger.ERROR, "[gns] remote Lookup: %s\n", err.Error()) + block = nil + } else { + logger.Println(logger.DBG, "[gns] remote Lookup: no block found") + } + // lookup fails completely -- no result + return + } + // store RRs from remote locally. + gns.StoreLocal(query, block) + } + } + return +} diff --git a/src/gnunet/service/gns/record.go b/src/gnunet/service/gns/record.go @@ -28,6 +28,11 @@ func NewGNSRecordSet() *GNSRecordSet { } } +func (rs *GNSRecordSet) AddRecord(rec *message.GNSResourceRecord) { + rs.Count++ + rs.Records = append(rs.Records, rec) +} + type SignedBlockData struct { Purpose *crypto.SignaturePurpose // Size and purpose of signature (8 bytes) Expire util.AbsoluteTime // Expiration time of the block. diff --git a/src/gnunet/service/gns/service.go b/src/gnunet/service/gns/service.go @@ -0,0 +1,257 @@ +package gns + +import ( + "encoding/hex" + "io" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" + "gnunet/config" + "gnunet/crypto" + "gnunet/enums" + "gnunet/message" + "gnunet/service" + "gnunet/transport" + "gnunet/util" +) + +//---------------------------------------------------------------------- +// "GNUnet Name System" service implementation +//---------------------------------------------------------------------- + +// GNSService +type GNSService struct { + GNSModule +} + +// NewGNSService +func NewGNSService() service.Service { + // instantiate service and assemble a new GNS handler. + inst := new(GNSService) + inst.LookupLocal = inst.LookupNamecache + inst.StoreLocal = inst.StoreNamecache + inst.LookupRemote = inst.LookupDHT + inst.GetLocalZone = inst.GetPrivateZone + return inst +} + +// Start the GNS service +func (s *GNSService) Start(spec string) error { + return nil +} + +// Stop the GNS service +func (s *GNSService) Stop() error { + return nil +} + +// Serve a client channel. +func (s *GNSService) ServeClient(mc *transport.MsgChannel) { + for { + // receive next message from client + msg, err := mc.Receive() + if err != nil { + if err == io.EOF { + logger.Println(logger.INFO, "[gns] Client channel closed.") + } else { + logger.Printf(logger.ERROR, "[gns] Message-receive failed: %s\n", err.Error()) + } + break + } + logger.Printf(logger.INFO, "[gns] Received msg: %v\n", msg) + + // perform lookup + var resp message.Message + switch m := msg.(type) { + case *message.GNSLookupMsg: + //---------------------------------------------------------- + // GNS_LOOKUP + //---------------------------------------------------------- + logger.Println(logger.INFO, "[gns] Lookup request received.") + respX := message.NewGNSLookupResultMsg(m.Id) + resp = respX + + // perform lookup on block (locally and remote) + // TODO: run code in a go routine concurrently (would need + // access to the message channel to send responses) + pkey := ed25519.NewPublicKeyFromBytes(m.Zone) + label := m.GetName() + recset, err := s.Resolve(label, pkey, int(m.Type), int(m.Options)) + if err != nil { + logger.Printf(logger.ERROR, "[gns] Failed to lookup block: %s\n", err.Error()) + break + } + // handle records + if recset != nil { + logger.Printf(logger.DBG, "[gns] Received record set with %d entries\n", recset.Count) + + // get records from block + if recset.Count == 0 { + logger.Println(logger.WARN, "[gns] No records in block") + break + } + // process records + for i, rec := range recset.Records { + logger.Printf(logger.DBG, "[gns] Record #%d: %v\n", i, rec) + + // is this the record type we are looking for? + if rec.Type == m.Type || int(m.Type) == enums.GNS_TYPE_ANY { + // add it to the response message + respX.AddRecord(rec) + } + } + } + + default: + //---------------------------------------------------------- + // UNKNOWN message type received + //---------------------------------------------------------- + logger.Printf(logger.ERROR, "[gns] Unhandled message of type (%d)\n", msg.Header().MsgType) + continue + } + + // send response + if err := mc.Send(resp); err != nil { + logger.Printf(logger.ERROR, "[gns] Failed to send response: %s\n", err.Error()) + } + + } + // close client connection + mc.Close() +} + +// LookupNamecache +func (s *GNSService) LookupNamecache(query *Query) (block *GNSBlock, err error) { + logger.Printf(logger.DBG, "[gns] LookupNamecache(%s)...\n", hex.EncodeToString(query.Key.Bits)) + + // assemble Namecache request + req := message.NewNamecacheLookupMsg(query.Key) + req.Id = uint32(util.NextID()) + block = nil + + // get response from Namecache service + var resp message.Message + if resp, err = service.ServiceRequestResponse("gns", "Namecache", config.Cfg.Namecache.Endpoint, req); err != nil { + return + } + + // handle message depending on its type + logger.Println(logger.DBG, "[gns] Handling response from Namecache service") + switch m := resp.(type) { + case *message.NamecacheLookupResultMsg: + // check for matching IDs + if m.Id != req.Id { + logger.Println(logger.ERROR, "[gns] Got response for unknown ID") + break + } + // check if block was found + if len(m.EncData) == 0 { + logger.Println(logger.DBG, "[gns] block not found in namecache") + break + } + // check if record has expired + if m.Expire.Expired() { + logger.Printf(logger.ERROR, "[gns] block expired at %s\n", m.Expire) + break + } + + // assemble GNSBlock from message + block = new(GNSBlock) + block.Signature = m.Signature + block.DerivedKey = m.DerivedKey + sb := new(SignedBlockData) + sb.Purpose = new(crypto.SignaturePurpose) + sb.Purpose.Purpose = enums.SIG_GNS_RECORD_SIGN + sb.Purpose.Size = uint32(16 + len(m.EncData)) + sb.Expire = m.Expire + sb.EncData = m.EncData + block.Block = sb + + // verify and decrypt block + if err = block.Verify(query.Zone, query.Label); err != nil { + break + } + if err = block.Decrypt(query.Zone, query.Label); err != nil { + break + } + } + return +} + +// StoreNamecache +func (s *GNSService) StoreNamecache(query *Query, block *GNSBlock) error { + logger.Println(logger.WARN, "[gns] StoreNamecache() not implemented yet!") + return nil +} + +// LookupDHT +func (s *GNSService) LookupDHT(query *Query) (block *GNSBlock, err error) { + logger.Printf(logger.DBG, "[gns] LookupDHT(%s)...\n", hex.EncodeToString(query.Key.Bits)) + + // assemble DHT request + req := message.NewDHTClientGetMsg(query.Key) + req.Id = uint64(util.NextID()) + req.ReplLevel = uint32(enums.DHT_GNS_REPLICATION_LEVEL) + req.Type = uint32(enums.BLOCK_TYPE_GNS_NAMERECORD) + req.Options = uint32(enums.DHT_RO_DEMULTIPLEX_EVERYWHERE) + block = nil + + // get response from DHT service + var resp message.Message + if resp, err = service.ServiceRequestResponse("gns", "DHT", config.Cfg.DHT.Endpoint, req); err != nil { + return + } + + // handle message depending on its type + logger.Println(logger.DBG, "[gns] Handling response from DHT service") + switch m := resp.(type) { + case *message.DHTClientResultMsg: + // check for matching IDs + if m.Id != req.Id { + logger.Println(logger.ERROR, "[gns] Got response for unknown ID") + break + } + // check if block was found + if len(m.Data) == 0 { + logger.Println(logger.DBG, "[gns] block not found in DHT") + break + } + // check if record has expired + if m.Expire.Expired() { + logger.Printf(logger.ERROR, "[gns] block expired at %s\n", m.Expire) + break + } + // check if result is of requested type + if int(m.Type) != enums.BLOCK_TYPE_GNS_NAMERECORD { + logger.Println(logger.ERROR, "[gns] DHT response has wrong type") + break + } + + // get GNSBlock from message + block = NewGNSBlock() + if err = data.Unmarshal(block, m.Data); err != nil { + logger.Printf(logger.ERROR, "[gns] can't read GNS block: %s\n", err.Error()) + break + } + // verify and decrypt block + if err = block.Verify(query.Zone, query.Label); err != nil { + break + } + if err = block.Decrypt(query.Zone, query.Label); err != nil { + break + } + + // we got a result from DHT that was not in the namecache, + // so store it there now. + if err = s.StoreNamecache(query, block); err != nil { + logger.Printf(logger.ERROR, "[gns] can't store block in Namecache: %s\n", err.Error()) + } + } + return +} + +// GetPrivateZone +func (s *GNSService) GetPrivateZone(name string) (*ed25519.PublicKey, error) { + return nil, nil +} diff --git a/src/gnunet/service/namecache/module.go b/src/gnunet/service/namecache/module.go @@ -0,0 +1,25 @@ +package namecache + +import ( + "gnunet/service/gns" +) + +//====================================================================== +// "GNS name cache" implementation +//====================================================================== + +//---------------------------------------------------------------------- +// Put and get GNS blocks into/from a cache (transient storage) +//---------------------------------------------------------------------- + +// Namecache handles the transient storage of GNS blocks under the query key. +type NamecacheModule struct { +} + +func (nc *NamecacheModule) Get(query *gns.Query) (*gns.GNSBlock, error) { + return nil, nil +} + +func (nc *NamecacheModule) Put(query *gns.Query, block *gns.GNSBlock) error { + return nil +} diff --git a/src/gnunet/util/array.go b/src/gnunet/util/array.go @@ -4,16 +4,23 @@ import ( "fmt" ) +// Error variables var ( ErrUtilArrayTooSmall = fmt.Errorf("Array to small") ) +//---------------------------------------------------------------------- +// Byte array helpers +//---------------------------------------------------------------------- + +// Clone creates a new array of same content as the argument. func Clone(d []byte) []byte { r := make([]byte, len(d)) copy(r, d) return r } +// Reverse the content of a byte array func Reverse(b []byte) []byte { bl := len(b) r := make([]byte, bl) @@ -41,8 +48,40 @@ func CopyBlock(out, in []byte) { copy(out[to:], in[from:]) } +// Fill an array with a value func Fill(b []byte, val byte) { for i := 0; i < len(b); i++ { b[i] = val } } + +//---------------------------------------------------------------------- +// String list helpers +//---------------------------------------------------------------------- + +// Reverse StringList reverse an array of strings +func ReverseStringList(s []string) []string { + sl := len(s) + r := make([]string, sl) + for i := 0; i < sl; i++ { + r[sl-i-1] = s[i] + } + return r +} + +// Convert a binary representation of a string list. Each string is '\0'- +// terminated. The whole byte array is parsed; if the final string is not +// terminated, it is skipped. +func StringList(b []byte) []string { + res := make([]string, 0) + str := "" + for _, ch := range b { + if ch == 0 { + res = append(res, str) + str = "" + continue + } + str += string(ch) + } + return res +} diff --git a/src/gnunet/util/time.go b/src/gnunet/util/time.go @@ -9,10 +9,14 @@ import ( // Absolute time //---------------------------------------------------------------------- +// AbsoluteTime refers to a unique point in time. +// The value is the elapsed time in milliseconds (Unix epoch), so no timestamp +// before January 1st, 1970 is possible (not a restriction for GNUnet). type AbsoluteTime struct { Val uint64 `order:"big"` } +// NewAbsoluteTime set the point in time to the given time value func NewAbsoluteTime(t time.Time) AbsoluteTime { secs := t.Unix() usecs := t.Nanosecond() / 1000 @@ -21,10 +25,17 @@ func NewAbsoluteTime(t time.Time) AbsoluteTime { } } +// AbsoluteTimeNow returns the current point in time. func AbsoluteTimeNow() AbsoluteTime { return NewAbsoluteTime(time.Now()) } +// AbsoluteTimeNever returns the time defined as "never" +func AbsoluteTimeNever() AbsoluteTime { + return AbsoluteTime{math.MaxUint64} +} + +// String returns a human-readable notation of an absolute time. func (t AbsoluteTime) String() string { if t.Val == math.MaxUint64 { return "Never" @@ -33,12 +44,14 @@ func (t AbsoluteTime) String() string { return ts.Format(time.RFC3339Nano) } +// Add a duration to an absolute time yielding a new absolute time. func (t AbsoluteTime) Add(d time.Duration) AbsoluteTime { return AbsoluteTime{ Val: t.Val + uint64(d.Milliseconds()), } } +// Expired returns true if the timestamp is in the past. func (t AbsoluteTime) Expired() bool { // check for "never" if t.Val == math.MaxUint64 { @@ -51,16 +64,20 @@ func (t AbsoluteTime) Expired() bool { // Relative time //---------------------------------------------------------------------- +// Relative time is a timestamp defined relative to the current time. +// It actually is more like a duration than a time... type RelativeTime struct { Val uint64 `order:"big"` } +// NewRelativeTime is initialized with a given duration. func NewRelativeTime(d time.Duration) RelativeTime { return RelativeTime{ Val: uint64(d.Milliseconds()), } } +// String returns a human-readble representation of a relative time (duration). func (t RelativeTime) String() string { if t.Val == math.MaxUint64 { return "Forever" diff --git a/test.sh b/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +GOPATH=$(pwd):${GOPATH} go test -gcflags "-N -l" ./...