From: Sergey Matveev Date: Sat, 26 Jun 2021 16:19:54 +0000 (+0300) Subject: MultiCast Discovery X-Git-Tag: v6.6.0^2 X-Git-Url: http://www.git.cypherpunks.ru/?p=nncp.git;a=commitdiff_plain;h=369ef72fdf833e4d9d76be076dbaf45cf5ca1e62 MultiCast Discovery --- diff --git a/doc/call.texi b/doc/call.texi index fe55de5..86f4bcf 100644 --- a/doc/call.texi +++ b/doc/call.texi @@ -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 diff --git a/doc/cfg.texi b/doc/cfg.texi index cc6a24a..30341f8 100644 --- a/doc/cfg.texi +++ b/doc/cfg.texi @@ -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 diff --git a/doc/cmds.texi b/doc/cmds.texi index 5141b08..4582593 100644 --- a/doc/cmds.texi +++ b/doc/cmds.texi @@ -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 diff --git a/doc/index.texi b/doc/index.texi index 057995c..cbf6aed 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -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 index 0000000..0e47ce6 --- /dev/null +++ b/doc/mcd.texi @@ -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. diff --git a/doc/news.ru.texi b/doc/news.ru.texi index fc18fd5..8271084 100644 --- a/doc/news.ru.texi +++ b/doc/news.ru.texi @@ -10,6 +10,10 @@ ожидают завершения всех процессов фоновой проверки контрольных сумм, после того как соединение закрыто. +@item +Добавлена возможность определения адреса через multicast оповещение в +локальной сети, так называемый MCD (MultiCast Discovery). + @end itemize @node Релиз 6.5.0 diff --git a/doc/news.texi b/doc/news.texi index d0ef49e..b4302f4 100644 --- a/doc/news.texi +++ b/doc/news.texi @@ -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 diff --git a/src/call.go b/src/call.go index ab254b2..6fc6045 100644 --- a/src/call.go +++ b/src/call.go @@ -37,6 +37,7 @@ type Call struct { MaxOnlineTime time.Duration WhenTxExists bool NoCK bool + MCDIgnore bool AutoToss bool AutoTossDoSeen bool diff --git a/src/cfg.go b/src/cfg.go index 6e781d1..6b10007 100644 --- a/src/cfg.go +++ b/src/cfg.go @@ -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 { diff --git a/src/cmd/nncp-caller/main.go b/src/cmd/nncp-caller/main.go index 66b441e..e338ac9 100644 --- a/src/cmd/nncp-caller/main.go +++ b/src/cmd/nncp-caller/main.go @@ -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, diff --git a/src/cmd/nncp-cfgnew/main.go b/src/cmd/nncp-cfgnew/main.go index f823601..d26c901 100644 --- a/src/cmd/nncp-cfgnew/main.go +++ b/src/cmd/nncp-cfgnew/main.go @@ -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 diff --git a/src/cmd/nncp-daemon/main.go b/src/cmd/nncp-daemon/main.go index c5f8a10..75fd40b 100644 --- a/src/cmd/nncp-daemon/main.go +++ b/src/cmd/nncp-daemon/main.go @@ -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() diff --git a/src/ctx.go b/src/ctx.go index 88fd852..fb26185 100644 --- a/src/ctx.go +++ b/src/ctx.go @@ -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 index 0000000..dc733ca --- /dev/null +++ b/src/mcd.go @@ -0,0 +1,216 @@ +/* +NNCP -- Node to Node copy, utilities for store-and-forward data exchange +Copyright (C) 2016-2021 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/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 +}