/* NNCP -- Node to Node copy, utilities for store-and-forward data exchange Copyright (C) 2016-2023 Sergey Matveev 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, version 3 of the License. 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 . */ package nncp import ( "bytes" "encoding/json" "errors" "fmt" "log" "os" "path" "strconv" "time" "github.com/gorhill/cronexpr" "github.com/hjson/hjson-go/v4" "golang.org/x/crypto/ed25519" "golang.org/x/term" ) const ( CfgPathEnv = "NNCPCFG" CfgSpoolEnv = "NNCPSPOOL" CfgLogEnv = "NNCPLOG" CfgNoSync = "NNCPNOSYNC" ) var ( DefaultCfgPath string = "/usr/local/etc/nncp.hjson" DefaultSendmailPath string = "/usr/sbin/sendmail" DefaultSpoolPath string = "/var/spool/nncp" DefaultLogPath string = "/var/spool/nncp/log" ) type NodeJSON struct { Id string `json:"id"` ExchPub string `json:"exchpub"` SignPub string `json:"signpub"` NoisePub *string `json:"noisepub,omitempty"` Incoming *string `json:"incoming,omitempty"` Exec map[string][]string `json:"exec,omitempty"` Freq *NodeFreqJSON `json:"freq,omitempty"` ACK *NodeACKJSON `json:"ack,omitempty"` Via []string `json:"via,omitempty"` Calls []CallJSON `json:"calls,omitempty"` Addrs map[string]string `json:"addrs,omitempty"` RxRate *int `json:"rxrate,omitempty"` TxRate *int `json:"txrate,omitempty"` OnlineDeadline *uint `json:"onlinedeadline,omitempty"` MaxOnlineTime *uint `json:"maxonlinetime,omitempty"` } type NodeFreqJSON struct { Path *string `json:"path,omitempty"` Chunked *uint64 `json:"chunked,omitempty"` MinSize *uint64 `json:"minsize,omitempty"` MaxSize *uint64 `json:"maxsize,omitempty"` } type NodeACKJSON struct { MinSize *uint64 `json:"minsize,omitempty"` Nice *string `json:"nice,omitempty"` } type CallJSON struct { Cron string `json:"cron"` Nice *string `json:"nice,omitempty"` Xx *string `json:"xx,omitempty"` RxRate *int `json:"rxrate,omitempty"` TxRate *int `json:"txrate,omitempty"` Addr *string `json:"addr,omitempty"` OnlineDeadline *uint `json:"onlinedeadline,omitempty"` MaxOnlineTime *uint `json:"maxonlinetime,omitempty"` WhenTxExists bool `json:"when-tx-exists,omitempty"` NoCK bool `json:"nock,omitempty"` MCDIgnore bool `json:"mcd-ignore,omitempty"` AutoToss bool `json:"autotoss,omitempty"` AutoTossDoSeen bool `json:"autotoss-doseen,omitempty"` AutoTossNoFile bool `json:"autotoss-nofile,omitempty"` AutoTossNoFreq bool `json:"autotoss-nofreq,omitempty"` AutoTossNoExec bool `json:"autotoss-noexec,omitempty"` AutoTossNoTrns bool `json:"autotoss-notrns,omitempty"` AutoTossNoArea bool `json:"autotoss-noarea,omitempty"` AutoTossNoACK bool `json:"autotoss-noack,omitempty"` AutoTossGenACK bool `json:"autotoss-gen-ack,omitempty"` } type NodeOurJSON struct { Id string `json:"id"` ExchPub string `json:"exchpub"` ExchPrv string `json:"exchprv"` SignPub string `json:"signpub"` SignPrv string `json:"signprv"` NoisePub string `json:"noisepub"` NoisePrv string `json:"noiseprv"` } type FromToJSON struct { From string `json:"from"` To string `json:"to"` } type NotifyJSON struct { File *FromToJSON `json:"file,omitempty"` Freq *FromToJSON `json:"freq,omitempty"` Exec map[string]*FromToJSON `json:"exec,omitempty"` } type AreaJSON struct { Id string `json:"id"` Pub *string `json:"pub,omitempty"` Prv *string `json:"prv,omitempty"` Subs []string `json:"subs"` Incoming *string `json:"incoming,omitempty"` Exec map[string][]string `json:"exec,omitempty"` AllowUnknown bool `json:"allow-unknown,omitempty"` } type CfgJSON struct { Spool string `json:"spool"` Log string `json:"log"` Umask *string `json:"umask,omitempty"` OmitPrgrs bool `json:"noprogress,omitempty"` NoHdr bool `json:"nohdr,omitempty"` MCDRxIfis []string `json:"mcd-listen,omitempty"` MCDTxIfis map[string]int `json:"mcd-send,omitempty"` Notify *NotifyJSON `json:"notify,omitempty"` Self *NodeOurJSON `json:"self"` Neigh map[string]NodeJSON `json:"neigh"` Areas map[string]AreaJSON `json:"areas,omitempty"` YggdrasilAliases map[string]string `json:"yggdrasil-aliases,omitempty"` } func NewNode(name string, cfg NodeJSON) (*Node, error) { nodeId, err := NodeIdFromString(cfg.Id) if err != nil { return nil, err } exchPub, err := Base32Codec.DecodeString(cfg.ExchPub) if err != nil { return nil, err } if len(exchPub) != 32 { return nil, errors.New("Invalid exchPub size") } signPub, err := Base32Codec.DecodeString(cfg.SignPub) if err != nil { return nil, err } if len(signPub) != ed25519.PublicKeySize { return nil, errors.New("Invalid signPub size") } var noisePub []byte if cfg.NoisePub != nil { noisePub, err = Base32Codec.DecodeString(*cfg.NoisePub) if err != nil { return nil, err } if len(noisePub) != 32 { return nil, errors.New("Invalid noisePub size") } } var incoming *string if cfg.Incoming != nil { inc := path.Clean(*cfg.Incoming) if !path.IsAbs(inc) { return nil, errors.New("Incoming path must be absolute") } incoming = &inc } var freqPath *string var freqChunked int64 var freqMinSize int64 freqMaxSize := int64(MaxFileSize) if cfg.Freq != nil { f := cfg.Freq if f.Path != nil { fPath := path.Clean(*f.Path) if !path.IsAbs(fPath) { return nil, errors.New("freq.path path must be absolute") } freqPath = &fPath } if f.Chunked != nil { if *f.Chunked == 0 { return nil, errors.New("freq.chunked value must be greater than zero") } freqChunked = int64(*f.Chunked) * 1024 } if f.MinSize != nil { freqMinSize = int64(*f.MinSize) * 1024 } if f.MaxSize != nil { freqMaxSize = int64(*f.MaxSize) * 1024 } } ackNice := uint8(255) var ackMinSize int64 if cfg.ACK != nil { if cfg.ACK.Nice != nil { ackNice, err = NicenessParse(*cfg.ACK.Nice) if err != nil { return nil, err } } if cfg.ACK.MinSize != nil { ackMinSize = int64(*cfg.ACK.MinSize) * 1024 } } defRxRate := 0 if cfg.RxRate != nil && *cfg.RxRate > 0 { defRxRate = *cfg.RxRate } defTxRate := 0 if cfg.TxRate != nil && *cfg.TxRate > 0 { defTxRate = *cfg.TxRate } defOnlineDeadline := DefaultDeadline if cfg.OnlineDeadline != nil { if *cfg.OnlineDeadline <= 0 { return nil, errors.New("OnlineDeadline must be at least 1 second") } defOnlineDeadline = time.Duration(*cfg.OnlineDeadline) * time.Second } var defMaxOnlineTime time.Duration if cfg.MaxOnlineTime != nil { defMaxOnlineTime = time.Duration(*cfg.MaxOnlineTime) * time.Second } var calls []*Call for _, callCfg := range cfg.Calls { expr, err := cronexpr.Parse(callCfg.Cron) if err != nil { return nil, err } nice := uint8(255) if callCfg.Nice != nil { nice, err = NicenessParse(*callCfg.Nice) if err != nil { return nil, err } } var xx TRxTx if callCfg.Xx != nil { switch *callCfg.Xx { case "rx": xx = TRx case "tx": xx = TTx default: return nil, errors.New("xx field must be either \"rx\" or \"tx\"") } } rxRate := defRxRate if callCfg.RxRate != nil { rxRate = *callCfg.RxRate } txRate := defTxRate if callCfg.TxRate != nil { txRate = *callCfg.TxRate } var addr *string if callCfg.Addr != nil { if a, exists := cfg.Addrs[*callCfg.Addr]; exists { addr = &a } else { addr = callCfg.Addr } } onlineDeadline := defOnlineDeadline if callCfg.OnlineDeadline != nil { if *callCfg.OnlineDeadline == 0 { return nil, errors.New("OnlineDeadline must be at least 1 second") } onlineDeadline = time.Duration(*callCfg.OnlineDeadline) * time.Second } call := Call{ Cron: expr, Nice: nice, Xx: xx, RxRate: rxRate, TxRate: txRate, Addr: addr, OnlineDeadline: onlineDeadline, } if callCfg.MaxOnlineTime != nil { call.MaxOnlineTime = time.Duration(*callCfg.MaxOnlineTime) * time.Second } call.WhenTxExists = callCfg.WhenTxExists call.NoCK = callCfg.NoCK call.MCDIgnore = callCfg.MCDIgnore call.AutoToss = callCfg.AutoToss call.AutoTossDoSeen = callCfg.AutoTossDoSeen call.AutoTossNoFile = callCfg.AutoTossNoFile call.AutoTossNoFreq = callCfg.AutoTossNoFreq call.AutoTossNoExec = callCfg.AutoTossNoExec call.AutoTossNoTrns = callCfg.AutoTossNoTrns call.AutoTossNoArea = callCfg.AutoTossNoArea call.AutoTossNoACK = callCfg.AutoTossNoACK call.AutoTossGenACK = callCfg.AutoTossGenACK calls = append(calls, &call) } node := Node{ Name: name, Id: nodeId, ExchPub: new([32]byte), SignPub: ed25519.PublicKey(signPub), Exec: cfg.Exec, Incoming: incoming, FreqPath: freqPath, FreqChunked: freqChunked, FreqMinSize: freqMinSize, FreqMaxSize: freqMaxSize, ACKNice: ackNice, ACKMinSize: ackMinSize, Calls: calls, Addrs: cfg.Addrs, RxRate: defRxRate, TxRate: defTxRate, OnlineDeadline: defOnlineDeadline, MaxOnlineTime: defMaxOnlineTime, } copy(node.ExchPub[:], exchPub) if len(noisePub) > 0 { node.NoisePub = new([32]byte) copy(node.NoisePub[:], noisePub) } return &node, nil } func NewNodeOur(cfg *NodeOurJSON) (*NodeOur, error) { id, err := NodeIdFromString(cfg.Id) if err != nil { return nil, err } exchPub, err := Base32Codec.DecodeString(cfg.ExchPub) if err != nil { return nil, err } if len(exchPub) != 32 { return nil, errors.New("Invalid exchPub size") } exchPrv, err := Base32Codec.DecodeString(cfg.ExchPrv) if err != nil { return nil, err } if len(exchPrv) != 32 { return nil, errors.New("Invalid exchPrv size") } signPub, err := Base32Codec.DecodeString(cfg.SignPub) if err != nil { return nil, err } if len(signPub) != ed25519.PublicKeySize { return nil, errors.New("Invalid signPub size") } signPrv, err := Base32Codec.DecodeString(cfg.SignPrv) if err != nil { return nil, err } if len(signPrv) != ed25519.PrivateKeySize { return nil, errors.New("Invalid signPrv size") } noisePub, err := Base32Codec.DecodeString(cfg.NoisePub) if err != nil { return nil, err } if len(noisePub) != 32 { return nil, errors.New("Invalid noisePub size") } noisePrv, err := Base32Codec.DecodeString(cfg.NoisePrv) if err != nil { return nil, err } if len(noisePrv) != 32 { return nil, errors.New("Invalid noisePrv size") } node := NodeOur{ Id: id, ExchPub: new([32]byte), ExchPrv: new([32]byte), SignPub: ed25519.PublicKey(signPub), SignPrv: ed25519.PrivateKey(signPrv), NoisePub: new([32]byte), NoisePrv: new([32]byte), } copy(node.ExchPub[:], exchPub) copy(node.ExchPrv[:], exchPrv) copy(node.NoisePub[:], noisePub) copy(node.NoisePrv[:], noisePrv) return &node, nil } func NewArea(ctx *Ctx, name string, cfg *AreaJSON) (*Area, error) { areaId, err := AreaIdFromString(cfg.Id) if err != nil { return nil, err } subs := make([]*NodeId, 0, len(cfg.Subs)) for _, s := range cfg.Subs { node, err := ctx.FindNode(s) if err != nil { return nil, err } subs = append(subs, node.Id) } area := Area{ Name: name, Id: areaId, Subs: subs, Exec: cfg.Exec, Incoming: cfg.Incoming, } if cfg.Pub != nil { pub, err := Base32Codec.DecodeString(*cfg.Pub) if err != nil { return nil, err } if len(pub) != 32 { return nil, errors.New("Invalid pub size") } area.Pub = new([32]byte) copy(area.Pub[:], pub) } if cfg.Prv != nil { if area.Pub == nil { return nil, fmt.Errorf("area %s: prv requires pub presence", name) } prv, err := Base32Codec.DecodeString(*cfg.Prv) if err != nil { return nil, err } if len(prv) != 32 { return nil, errors.New("Invalid prv size") } area.Prv = new([32]byte) copy(area.Prv[:], prv) } area.AllowUnknown = cfg.AllowUnknown return &area, nil } func CfgParse(data []byte) (*CfgJSON, error) { var err error if bytes.Equal(data[:8], MagicNNCPBv3.B[:]) { os.Stderr.WriteString("Passphrase:") password, err := term.ReadPassword(0) if err != nil { log.Fatalln(err) } os.Stderr.WriteString("\n") data, err = DeEBlob(data, password) if err != nil { return nil, err } } else if bytes.Equal(data[:8], MagicNNCPBv2.B[:]) { log.Fatalln(MagicNNCPBv2.TooOld()) } else if bytes.Equal(data[:8], MagicNNCPBv1.B[:]) { log.Fatalln(MagicNNCPBv1.TooOld()) } var cfgGeneral map[string]interface{} if err = hjson.Unmarshal(data, &cfgGeneral); err != nil { return nil, err } marshaled, err := json.Marshal(cfgGeneral) if err != nil { return nil, err } var cfgJSON CfgJSON err = json.Unmarshal(marshaled, &cfgJSON) return &cfgJSON, err } func Cfg2Ctx(cfgJSON *CfgJSON) (*Ctx, error) { if _, exists := cfgJSON.Neigh["self"]; !exists { return nil, errors.New("self neighbour missing") } var self *NodeOur if cfgJSON.Self != nil { var err error self, err = NewNodeOur(cfgJSON.Self) if err != nil { return nil, err } } spoolPath := path.Clean(cfgJSON.Spool) if !path.IsAbs(spoolPath) { return nil, errors.New("Spool path must be absolute") } logPath := path.Clean(cfgJSON.Log) if !path.IsAbs(logPath) { return nil, errors.New("Log path must be absolute") } var umaskForce *int if cfgJSON.Umask != nil { r, err := strconv.ParseUint(*cfgJSON.Umask, 8, 16) if err != nil { return nil, err } rInt := int(r) umaskForce = &rInt } showPrgrs := true if cfgJSON.OmitPrgrs { showPrgrs = false } hdrUsage := true if cfgJSON.NoHdr { hdrUsage = false } ctx := Ctx{ Spool: spoolPath, LogPath: logPath, UmaskForce: umaskForce, ShowPrgrs: showPrgrs, HdrUsage: hdrUsage, Self: self, Neigh: make(map[NodeId]*Node, len(cfgJSON.Neigh)), Alias: make(map[string]*NodeId), MCDRxIfis: cfgJSON.MCDRxIfis, MCDTxIfis: cfgJSON.MCDTxIfis, YggdrasilAliases: cfgJSON.YggdrasilAliases, } if cfgJSON.Notify != nil { if cfgJSON.Notify.File != nil { ctx.NotifyFile = cfgJSON.Notify.File } if cfgJSON.Notify.Freq != nil { ctx.NotifyFreq = cfgJSON.Notify.Freq } if cfgJSON.Notify.Exec != nil { ctx.NotifyExec = cfgJSON.Notify.Exec } } vias := make(map[NodeId][]string) for name, neighJSON := range cfgJSON.Neigh { neigh, err := NewNode(name, neighJSON) if err != nil { return nil, err } ctx.Neigh[*neigh.Id] = neigh if _, already := ctx.Alias[name]; already { return nil, errors.New("Node names conflict") } ctx.Alias[name] = neigh.Id vias[*neigh.Id] = neighJSON.Via } ctx.SelfId = ctx.Alias["self"] for neighId, viasRaw := range vias { for _, viaRaw := range viasRaw { foundNodeId, err := ctx.FindNode(viaRaw) if err != nil { return nil, err } ctx.Neigh[neighId].Via = append( ctx.Neigh[neighId].Via, foundNodeId.Id, ) } } ctx.AreaId2Area = make(map[AreaId]*Area, len(cfgJSON.Areas)) ctx.AreaName2Id = make(map[string]*AreaId, len(cfgJSON.Areas)) for name, areaJSON := range cfgJSON.Areas { area, err := NewArea(&ctx, name, &areaJSON) if err != nil { return nil, err } ctx.AreaId2Area[*area.Id] = area ctx.AreaName2Id[name] = area.Id } return &ctx, nil }