]> Cypherpunks.ru repositories - nncp.git/commitdiff
MultiCast Discovery
authorSergey Matveev <stargrave@stargrave.org>
Sat, 26 Jun 2021 16:19:54 +0000 (19:19 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 26 Jun 2021 17:09:02 +0000 (20:09 +0300)
14 files changed:
doc/call.texi
doc/cfg.texi
doc/cmds.texi
doc/index.texi
doc/mcd.texi [new file with mode: 0644]
doc/news.ru.texi
doc/news.texi
src/call.go
src/cfg.go
src/cmd/nncp-caller/main.go
src/cmd/nncp-cfgnew/main.go
src/cmd/nncp-daemon/main.go
src/ctx.go
src/mcd.go [new file with mode: 0644]

index fe55de5456d2b9f82c4aa83b428f6c13155d4c0b..86f4bcfbd442fc7628f501774ac79eeef8a0c4b9 100644 (file)
@@ -32,6 +32,7 @@ calls: [
         cron: "*/5 * * * * * *"
         when-tx-exists: true
         nock: true
+        mcd-ignore: true
     },
 ]
 @end verbatim
@@ -96,4 +97,9 @@ received one, but just sequential writing of the file. Pay attention
 that you have to make a call to remote node after checksumming is done,
 to send notification about successful packet reception.
 
+@anchor{CfgMCDIgnore}
+@item mcd-ignore
+Ignore @ref{MCD} announcements: do not add MCD addresses for possible
+connection attempts.
+
 @end table
index cc6a24ae72190dd020e39a8d41519bb73304c9c0..30341f888dc160c555939530541d5609f1b11a4f 100644 (file)
@@ -11,6 +11,9 @@ Example @url{https://hjson.org/, Hjson} configuration file:
   noprogress: true
   nohdr: true
 
+  mcd-listen: ["em0", "igb1"]
+  mcd-send: {em0: 60, igb1: 5}
+
   notify: {
     file: {
       from: nncp@localhost
@@ -107,6 +110,14 @@ commands by default. You can always force its showing with
 @anchor{CfgNoHdr}
 @strong{nohdr} option disables @ref{HdrFile, .hdr} files usage.
 
+@anchor{CfgMCDListen}
+@strong{mcd-listen} specifies list of network interfaces
+@ref{nncp-caller} will listen for incoming @ref{MCD} announcements.
+
+@anchor{CfgMCDSend}
+@strong{mcd-send} specifies list of network interfaces, and intervals in
+seconds, where @ref{nncp-daemon} will send @ref{MCD} announcements.
+
 @anchor{CfgNotify}
 @strong{notify} section contains notification settings for successfully
 tossed file, freq and exec packets. Corresponding @strong{from} and
index 5141b08dd09bf0d98f0f0c4e686d71ca8452d941..45825932821df2a207ab4668aabe9a84893cf3f8 100644 (file)
@@ -260,7 +260,7 @@ next time entities.
 @example
 $ nncp-daemon [options]
     [-maxconn INT] [-bind ADDR] [-inetd]
-    [-autotoss*] [-nock]
+    [-autotoss*] [-nock] [-mcd-once]
 @end example
 
 Start listening TCP daemon, wait for incoming connections and run
@@ -288,6 +288,9 @@ during the call. All @option{-autotoss-*} options is the same as in
 
 Read @ref{CfgNoCK, more} about @option{-nock} option.
 
+@option{-mcd-once} option sends @ref{MCD} announcements once and quits.
+Could be useful with inetd-based setup, where daemons are not running.
+
 @node nncp-exec
 @section nncp-exec
 
index 057995c6e1268f7c3fbae858c8a8e3cdcf20fd42..cbf6aeda864e7a3f252ff32164c5218babbac2f6 100644 (file)
@@ -53,6 +53,7 @@ There are also articles about its usage outside this website:
 * Log format: Log.
 * Packet format: Packet.
 * Sync protocol: Sync.
+* MultiCast Discovery: MCD.
 * EBlob format: EBlob.
 * Thanks::
 * Contacts and feedback: Contacts.
@@ -77,6 +78,7 @@ There are also articles about its usage outside this website:
 @include log.texi
 @include pkt.texi
 @include sp.texi
+@include mcd.texi
 @include eblob.texi
 @include thanks.texi
 @include contacts.texi
diff --git a/doc/mcd.texi b/doc/mcd.texi
new file mode 100644 (file)
index 0000000..0e47ce6
--- /dev/null
@@ -0,0 +1,35 @@
+@node MCD
+@unnumbered MultiCast Discovery
+
+MCD is an addition to online @ref{Sync, synchronization protocol}, that
+gives ability to make node discovery by sending multicast announcements
+in local area network. It is very simple:
+
+@itemize
+@item
+    @command{nncp-daemon} sends multicast messages about its presence
+    from time to time.
+    See @ref{CfgMCDSend, mcd-send} configuration option.
+@item
+    When @command{nncp-caller} sees them, it adds them as the most
+    preferred addresses to already known ones. If MCD address
+    announcement was not refreshed after two minutes -- it is removed.
+    See @ref{CfgMCDListen, mcd-listen} and
+    @ref{CfgMCDIgnore, mcd-ignore} configuration options.
+@end itemize
+
+MCD announcement is an XDR-encoded packet with only two fields:
+
+@verbatim
++----------------+
+| MAGIC | SENDER |
++----------------+
+@end verbatim
+
+Magic number is @verb{|N N C P D 0x00 0x00 0x01|} and sender is 32-byte
+identifier of the node. It is sent as UDP packet on IPv6 @verb{|ff02::1|}
+multicast address (all hosts in the network) and hard-coded @strong{5400}
+port. Operating system will use IPv6 link-local address as a source one,
+with the port taken from @command{nncp-daemon}'s @option{-bind} option.
+That way, IP packet itself will carry the link-scope reachable address
+of the daemon.
index fc18fd5665cf87fd43a07d194a39caa4e0536110..82710847aa76e08b207b92516897aae8ff687c10 100644 (file)
 ожидают завершения всех процессов фоновой проверки контрольных сумм,
 после того как соединение закрыто.
 
+@item
+Добавлена возможность определения адреса через multicast оповещение в
+локальной сети, так называемый MCD (MultiCast Discovery).
+
 @end itemize
 
 @node Релиз 6.5.0
index d0ef49ec3f9ea6e06f98d14520c8b2caeafe53ae..b4302f48d8453c2e4194b2b89dfae528b2209fd4 100644 (file)
@@ -12,6 +12,10 @@ See also this page @ref{Новости, on russian}.
 commands wait for all background checksummers completion after
 connection is finished.
 
+@item
+Added possibility of address determining through multicast announcement
+in local area network, so called MCD (MultiCast Discovery).
+
 @end itemize
 
 @node Release 6.5.0
index ab254b2dd5b6e8e9affb7e6280373111038c6117..6fc6045d4371ce15144a9aea7579646b4586c319 100644 (file)
@@ -37,6 +37,7 @@ type Call struct {
        MaxOnlineTime  time.Duration
        WhenTxExists   bool
        NoCK           bool
+       MCDIgnore      bool
 
        AutoToss       bool
        AutoTossDoSeen bool
index 6e781d19e62a0ca1d6f0e628466f91be8af22a6d..6b10007bd8646502ee593da517a7c4f8f5d00713 100644 (file)
@@ -83,6 +83,7 @@ type CallJSON struct {
        MaxOnlineTime  *uint   `json:"maxonlinetime,omitempty"`
        WhenTxExists   *bool   `json:"when-tx-exists,omitempty"`
        NoCK           *bool   `json:"nock"`
+       MCDIgnore      *bool   `json:"mcd-ignore"`
 
        AutoToss       *bool `json:"autotoss,omitempty"`
        AutoTossDoSeen *bool `json:"autotoss-doseen,omitempty"`
@@ -125,6 +126,9 @@ type CfgJSON struct {
 
        Self  *NodeOurJSON        `json:"self"`
        Neigh map[string]NodeJSON `json:"neigh"`
+
+       MCDRxIfis []string       `json:"mcd-listen"`
+       MCDTxIfis map[string]int `json:"mcd-send"`
 }
 
 func NewNode(name string, cfg NodeJSON) (*Node, error) {
@@ -289,6 +293,9 @@ func NewNode(name string, cfg NodeJSON) (*Node, error) {
                if callCfg.NoCK != nil {
                        call.NoCK = *callCfg.NoCK
                }
+               if callCfg.MCDIgnore != nil {
+                       call.MCDIgnore = *callCfg.MCDIgnore
+               }
                if callCfg.AutoToss != nil {
                        call.AutoToss = *callCfg.AutoToss
                }
@@ -477,6 +484,8 @@ func CfgParse(data []byte) (*Ctx, error) {
                Self:       self,
                Neigh:      make(map[NodeId]*Node, len(cfgJSON.Neigh)),
                Alias:      make(map[string]*NodeId),
+               MCDRxIfis:  cfgJSON.MCDRxIfis,
+               MCDTxIfis:  cfgJSON.MCDTxIfis,
        }
        if cfgJSON.Notify != nil {
                if cfgJSON.Notify.File != nil {
index 66b441ef2269845b35814bb6d98ae0859d3899fa..e338ac95b5febdc887ef40c00dbcffb8cee09199 100644 (file)
@@ -121,19 +121,25 @@ func main() {
                }
        }
 
+       for _, ifiName := range ctx.MCDRxIfis {
+               if err = ctx.MCDRx(ifiName); err != nil {
+                       log.Fatalln("Can not run MCD reception:", err)
+               }
+       }
+
        var wg sync.WaitGroup
        for _, node := range nodes {
                for i, call := range node.Calls {
                        wg.Add(1)
                        go func(node *nncp.Node, i int, call *nncp.Call) {
                                defer wg.Done()
-                               var addrs []string
+                               var addrsFromCfg []string
                                if call.Addr == nil {
                                        for _, addr := range node.Addrs {
-                                               addrs = append(addrs, addr)
+                                               addrsFromCfg = append(addrsFromCfg, addr)
                                        }
                                } else {
-                                       addrs = append(addrs, *call.Addr)
+                                       addrsFromCfg = append(addrsFromCfg, *call.Addr)
                                }
                                les := nncp.LEs{{K: "Node", V: node.Id}, {K: "CallIndex", V: i}}
                                logMsg := func(les nncp.LEs) string {
@@ -197,9 +203,22 @@ func main() {
                                                        )
                                                }
 
+                                               var addrs []string
+                                               if !call.MCDIgnore {
+                                                       nncp.MCDAddrsM.RLock()
+                                                       for _, mcdAddr := range nncp.MCDAddrs[*node.Id] {
+                                                               ctx.LogD("caller", les, func(les nncp.LEs) string {
+                                                                       return logMsg(les) + ": adding MCD address: " +
+                                                                               mcdAddr.Addr.String()
+                                                               })
+                                                               addrs = append(addrs, mcdAddr.Addr.String())
+                                                       }
+                                                       nncp.MCDAddrsM.RUnlock()
+                                               }
+
                                                ctx.CallNode(
                                                        node,
-                                                       addrs,
+                                                       append(addrs, addrsFromCfg...),
                                                        call.Nice,
                                                        call.Xx,
                                                        call.RxRate,
index f823601084ca7bdd30664b805807483b02036a04..d26c901a14efdd8ecf99ef114772d9e74e62bedb 100644 (file)
@@ -109,6 +109,12 @@ func main() {
   # Do not use .hdr files
   # nohdr: true
 
+  # MultiCast Discovery:
+  # List of interfaces where to listen for MCD announcements
+  # mcd-listen: ["em0", "igb1"]
+  # Interfaces and intervals (in seconds) where to send MCD announcements
+  # mcd-send: {em0: 60, igb1: 5}
+
   # Enable notification email sending
   # notify: {
   #   file: {
@@ -216,6 +222,7 @@ func main() {
     #   #     addr: lan
     #   #     when-tx-exists: true
     #   #     nock: true
+    #   #     mcd-ignore: true
     #   #
     #   #     autotoss: false
     #   #     autotoss-doseen: true
index c5f8a10cddd2489917061aed9e41db5844a53c63..75fd40b8bb9a6136c42a903657efe6e3368c9b01 100644 (file)
@@ -24,6 +24,8 @@ import (
        "log"
        "net"
        "os"
+       "strconv"
+       "strings"
        "time"
 
        "github.com/dustin/go-humanize"
@@ -136,6 +138,7 @@ func main() {
                inetd     = flag.Bool("inetd", false, "Is it started as inetd service")
                maxConn   = flag.Int("maxconn", 128, "Maximal number of simultaneous connections")
                noCK      = flag.Bool("nock", false, "Do no checksum checking")
+               mcdOnce   = flag.Bool("mcd-once", false, "Send MCDs once and quit")
                spoolPath = flag.String("spool", "", "Override path to spool")
                logPath   = flag.String("log", "", "Override path to logfile")
                quiet     = flag.Bool("quiet", false, "Print only errors")
@@ -213,10 +216,32 @@ func main() {
                return
        }
 
+       cols := strings.Split(*bind, ":")
+       port, err := strconv.Atoi(cols[len(cols)-1])
+       if err != nil {
+               log.Fatalln("Can not parse port:", err)
+       }
+
+       if *mcdOnce {
+               for ifiName := range ctx.MCDTxIfis {
+                       if err = ctx.MCDTx(ifiName, port, 0); err != nil {
+                               log.Fatalln("Can not do MCD transmission:", err)
+                       }
+               }
+               return
+       }
+
        ln, err := net.Listen("tcp", *bind)
        if err != nil {
                log.Fatalln("Can not listen:", err)
        }
+
+       for ifiName, secs := range ctx.MCDTxIfis {
+               if err = ctx.MCDTx(ifiName, port, time.Duration(secs)*time.Second); err != nil {
+                       log.Fatalln("Can not run MCD transmission:", err)
+               }
+       }
+
        ln = netutil.LimitListener(ln, *maxConn)
        for {
                conn, err := ln.Accept()
index 88fd852ecb17f1d6fdec67f199c6674e790d34d5..fb26185fbb3af9e4ee6024f8af805d1c2c2e7ee2 100644 (file)
@@ -46,6 +46,9 @@ type Ctx struct {
        NotifyFile *FromToJSON
        NotifyFreq *FromToJSON
        NotifyExec map[string]*FromToJSON
+
+       MCDRxIfis []string
+       MCDTxIfis map[string]int
 }
 
 func (ctx *Ctx) FindNode(id string) (*Node, error) {
diff --git a/src/mcd.go b/src/mcd.go
new file mode 100644 (file)
index 0000000..dc733ca
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+package nncp
+
+import (
+       "bytes"
+       "encoding/hex"
+       "fmt"
+       "net"
+       "sync"
+       "time"
+
+       xdr "github.com/davecgh/go-xdr/xdr2"
+)
+
+const (
+       MCDPort = 5400
+)
+
+type MCD struct {
+       Magic  [8]byte
+       Sender *NodeId
+}
+
+type MCDAddr struct {
+       Addr     net.UDPAddr
+       lastSeen time.Time
+}
+
+var (
+       MagicNNCPDv1 [8]byte = [8]byte{'N', 'N', 'C', 'P', 'D', 0, 0, 1}
+
+       mcdIP           = net.ParseIP("ff02::1")
+       mcdAddrLifetime = 2 * time.Minute
+
+       mcdPktSize int
+       MCDAddrs   map[NodeId][]*MCDAddr
+       MCDAddrsM  sync.RWMutex
+)
+
+func init() {
+       nodeId := new(NodeId)
+       var buf bytes.Buffer
+       mcd := MCD{Sender: nodeId}
+       if _, err := xdr.Marshal(&buf, mcd); err != nil {
+               panic(err)
+       }
+       mcdPktSize = buf.Len()
+
+       MCDAddrs = make(map[NodeId][]*MCDAddr)
+       go func() {
+               for {
+                       time.Sleep(time.Minute)
+                       MCDAddrsM.Lock()
+                       now := time.Now()
+                       for nodeId, addrs := range MCDAddrs {
+                               addrsAlive := make([]*MCDAddr, 0, len(addrs))
+                               for _, addr := range addrs {
+                                       if !addr.lastSeen.Add(mcdAddrLifetime).Before(now) {
+                                               addrsAlive = append(addrsAlive, addr)
+                                       }
+                               }
+                               MCDAddrs[nodeId] = addrsAlive
+                       }
+                       MCDAddrsM.Unlock()
+               }
+       }()
+}
+
+func (ctx *Ctx) MCDRx(ifiName string) error {
+       ifi, err := net.InterfaceByName(ifiName)
+       if err != nil {
+               return err
+       }
+       addr := &net.UDPAddr{IP: mcdIP, Port: MCDPort, Zone: ifiName}
+       conn, err := net.ListenMulticastUDP("udp", ifi, addr)
+       if err != nil {
+               return err
+       }
+       go func() {
+               buf := make([]byte, mcdPktSize)
+               var n int
+               var mcd MCD
+       ListenCycle:
+               for {
+                       les := LEs{{"If", ifiName}}
+                       n, addr, err = conn.ReadFromUDP(buf)
+                       if err != nil {
+                               ctx.LogE("mcd", les, err, func(les LEs) string {
+                                       return fmt.Sprintf("MCD Rx %s/%d", ifiName, MCDPort)
+                               })
+                               continue
+                       }
+                       if n != mcdPktSize {
+                               ctx.LogD("mcd", les, func(les LEs) string {
+                                       return fmt.Sprintf(
+                                               "MCD Rx %s/%d: got packet with invalid size",
+                                               ifiName, MCDPort,
+                                       )
+                               })
+                               continue
+                       }
+                       _, err = xdr.Unmarshal(bytes.NewReader(buf[:n]), &mcd)
+                       if err != nil {
+                               ctx.LogD("mcd", les, func(les LEs) string {
+                                       return fmt.Sprintf(
+                                               "MCD Rx %s/%d: can not unmarshal: %s",
+                                               ifiName, MCDPort, err,
+                                       )
+                               })
+                               continue
+                       }
+                       if mcd.Magic != MagicNNCPDv1 {
+                               ctx.LogD("mcd", les, func(les LEs) string {
+                                       return fmt.Sprintf(
+                                               "MCD Rx %s/%d: unexpected magic: %s",
+                                               ifiName, MCDPort, hex.EncodeToString(mcd.Magic[:]),
+                                       )
+                               })
+                               continue
+                       }
+                       node, known := ctx.Neigh[*mcd.Sender]
+                       if known {
+                               les = append(les, LE{"Node", node.Id})
+                               ctx.LogD("mcd", les, func(les LEs) string {
+                                       return fmt.Sprintf(
+                                               "MCD Rx %s/%d: %s: node %s",
+                                               ifiName, MCDPort, addr, node.Name,
+                                       )
+                               })
+                       } else {
+                               ctx.LogD("mcd", les, func(les LEs) string {
+                                       return fmt.Sprintf(
+                                               "MCD Rx %s/%d: %s: unknown node %s",
+                                               ifiName, MCDPort, addr, node.Id.String(),
+                                       )
+                               })
+                               continue
+                       }
+                       MCDAddrsM.RLock()
+                       for _, mcdAddr := range MCDAddrs[*mcd.Sender] {
+                               if mcdAddr.Addr.IP.Equal(addr.IP) &&
+                                       mcdAddr.Addr.Port == addr.Port &&
+                                       mcdAddr.Addr.Zone == addr.Zone {
+                                       mcdAddr.lastSeen = time.Now()
+                                       MCDAddrsM.RUnlock()
+                                       continue ListenCycle
+                               }
+                       }
+                       MCDAddrsM.RUnlock()
+                       MCDAddrsM.Lock()
+                       MCDAddrs[*mcd.Sender] = append(
+                               MCDAddrs[*mcd.Sender],
+                               &MCDAddr{Addr: *addr, lastSeen: time.Now()},
+                       )
+                       MCDAddrsM.Unlock()
+                       ctx.LogI("mcd-add", les, func(les LEs) string {
+                               return fmt.Sprintf("MCD discovered %s's address: %s", node.Name, addr)
+                       })
+               }
+       }()
+       return nil
+}
+
+func (ctx *Ctx) MCDTx(ifiName string, port int, interval time.Duration) error {
+       conn, err := net.DialUDP("udp",
+               &net.UDPAddr{Port: port, Zone: ifiName},
+               &net.UDPAddr{IP: mcdIP, Port: MCDPort, Zone: ifiName},
+       )
+       if err != nil {
+               return err
+       }
+       var buf bytes.Buffer
+       mcd := MCD{Magic: MagicNNCPDv1, Sender: ctx.Self.Id}
+       if _, err := xdr.Marshal(&buf, mcd); err != nil {
+               panic(err)
+       }
+       if interval == 0 {
+               _, err = conn.Write(buf.Bytes())
+               return err
+       }
+       go func() {
+               les := LEs{{"If", ifiName}}
+               for {
+                       ctx.LogD("mcd", les, func(les LEs) string {
+                               return fmt.Sprintf(
+                                       "MCD Tx %s/%d/%d",
+                                       ifiName, MCDPort, port,
+                               )
+                       })
+                       _, err = conn.Write(buf.Bytes())
+                       if err != nil {
+                               ctx.LogE("mcd", les, err, func(les LEs) string {
+                                       return fmt.Sprintf("MCD on %s/%d/%d", ifiName, MCDPort, port)
+                               })
+                       }
+                       time.Sleep(interval)
+               }
+       }()
+       return nil
+}