--- /dev/null
+@node Bundles
+@unnumbered Bundles
+
+Usual @ref{nncp-xfer} command requires filesystem it can operate on.
+That presumes random access media storage usage, like hard drives, USB
+flash drives and similar. But media like CD-ROM and especially tape
+drives are sequential by nature. You can prepare intermediate directory
+for recording to CD-ROM disc, but that requires additional storage and
+is inconvenient. Tape drive will require intermediate extract step too.
+
+Bundles, created with @ref{nncp-bundle} command are convenient
+alternative to ordinary @command{nncp-xfer}. Bundle is just a collection
+of @ref{Encrypted, encrypted packets}, stream of packets. It could be
+sequentially streamed for recording and digested back.
+
+@itemize
+
+@item They do not require intermediate storage before recording on
+either CD-ROM or tape drive.
+@verbatim
+% nncp-bundle -tx SOMENODE | cdrecord -tao - # record directly to CD
+% nncp-bundle -tx SOMENODE | dd of=/dev/sa0 bs=512 # record directly to tape
+
+% dd if=/dev/cd0 bs=2048 | nncp-bundle -rx # read directly from CD
+% dd if=/dev/sa0 bs=512 | nncp-bundle -rx # read directly from tape
+@end verbatim
+
+@item They do not require filesystem existence to deal with, simplifying
+administration when operating in heterogeneous systems with varying
+filesystems. No @command{mount}/@command{umount}, @command{zpool
+import}/@command{zpool export} and struggling with file permissions.
+@verbatim
+% nncp-bundle -tx SOMENODE | dd of=/dev/da0 bs=1M # record directly to
+ # hard/flash drive
+% dd if=/dev/da0 bs=1M | nncp-bundle -rx # read directly from drive
+@end verbatim
+
+@item This is the fastest way to record outbound packets for offline
+transmission -- sequential write is always faster, when no
+metainformation needs to be updated.
+
+@item This is convenient to use with write-only/append-only storages,
+just sending/appending new bundles.
+
+@item Bundles could be repeatedly broadcasted in one-way transmission.
+@ref{Sync, Sync protocol} requires interactive connection, but bundles
+can contain mix of various recipients.
+
+@end itemize
+
+Technically bundle is valid POSIX.1
+@url{http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5, tar archive},
+with directory/files hierarchy identical to that is used in
+@ref{nncp-xfer}. So bundle can be created by manual tar-ing of
+@command{nncp-xfer} resulting directory too.
--- /dev/null
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2017 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, either version 3 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/>.
+*/
+
+// Create/digest stream of NNCP encrypted packets
+package main
+
+import (
+ "archive/tar"
+ "bufio"
+ "bytes"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "cypherpunks.ru/nncp"
+ "github.com/davecgh/go-xdr/xdr2"
+ "golang.org/x/crypto/blake2b"
+)
+
+const (
+ CopyBufSize = 1 << 17
+)
+
+func usage() {
+ fmt.Fprintf(os.Stderr, nncp.UsageHeader())
+ fmt.Fprintln(os.Stderr, "nncp-bundle -- Create/digest stream of NNCP encrypted packets\n")
+ fmt.Fprintf(os.Stderr, "Usage: %s [options] -tx [-delete] NODE [NODE ...] > ...\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, " %s [options] -rx -delete [NODE ...] < ...\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, " %s [options] -rx [-check] [NODE ...] < ...\n", os.Args[0])
+ fmt.Fprintln(os.Stderr, "Options:")
+ flag.PrintDefaults()
+}
+
+func main() {
+ var (
+ cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
+ niceRaw = flag.Int("nice", 255, "Minimal required niceness")
+ doRx = flag.Bool("rx", false, "Receive packets")
+ doTx = flag.Bool("tx", false, "Transfer packets")
+ doDelete = flag.Bool("delete", false, "Delete transferred packets")
+ doCheck = flag.Bool("check", false, "Check integrity while receiving")
+ quiet = flag.Bool("quiet", false, "Print only errors")
+ debug = flag.Bool("debug", false, "Print debug messages")
+ version = flag.Bool("version", false, "Print version information")
+ warranty = flag.Bool("warranty", false, "Print warranty information")
+ )
+ flag.Usage = usage
+ flag.Parse()
+ if *warranty {
+ fmt.Println(nncp.Warranty)
+ return
+ }
+ if *version {
+ fmt.Println(nncp.VersionGet())
+ return
+ }
+ if *niceRaw < 1 || *niceRaw > 255 {
+ log.Fatalln("-nice must be between 1 and 255")
+ }
+ nice := uint8(*niceRaw)
+ if *doRx && *doTx {
+ log.Fatalln("-rx and -tx can not be set simultaneously")
+ }
+ if !*doRx && !*doTx {
+ log.Fatalln("At least one of -rx and -tx must be specified")
+ }
+
+ cfgRaw, err := ioutil.ReadFile(nncp.CfgPathFromEnv(cfgPath))
+ if err != nil {
+ log.Fatalln("Can not read config:", err)
+ }
+ ctx, err := nncp.CfgParse(cfgRaw)
+ if err != nil {
+ log.Fatalln("Can not parse config:", err)
+ }
+ ctx.Quiet = *quiet
+ ctx.Debug = *debug
+
+ nodeIds := make(map[nncp.NodeId]struct{}, flag.NArg())
+ for i := 0; i < flag.NArg(); i++ {
+ node, err := ctx.FindNode(flag.Arg(i))
+ if err != nil {
+ log.Fatalln("Invalid specified:", err)
+ }
+ nodeIds[*node.Id] = struct{}{}
+ }
+
+ sds := nncp.SDS{}
+ if *doTx {
+ sds["xx"] = string(nncp.TTx)
+ var pktName string
+ bufStdout := bufio.NewWriter(os.Stdout)
+ tarWr := tar.NewWriter(bufStdout)
+ for nodeId, _ := range nodeIds {
+ sds["node"] = nodeId.String()
+ for job := range ctx.Jobs(&nodeId, nncp.TTx) {
+ pktName = filepath.Base(job.Fd.Name())
+ sds["pkt"] = pktName
+ if job.PktEnc.Nice > nice {
+ ctx.LogD("nncp-bundle", sds, "too nice")
+ job.Fd.Close()
+ continue
+ }
+ if err = tarWr.WriteHeader(&tar.Header{
+ Name: strings.Join([]string{
+ nodeId.String(),
+ ctx.SelfId.String(),
+ pktName,
+ }, "/"),
+ Mode: 0440,
+ Size: job.Size,
+ Typeflag: tar.TypeReg,
+ }); err != nil {
+ log.Fatalln("Error writing tar header:", err)
+ }
+ if _, err = io.Copy(tarWr, job.Fd); err != nil {
+ log.Fatalln("Error during copying to tar:", err)
+ }
+ job.Fd.Close()
+ if err = tarWr.Flush(); err != nil {
+ log.Fatalln("Error during tar flushing:", err)
+ }
+ if err = bufStdout.Flush(); err != nil {
+ log.Fatalln("Error during stdout flushing:", err)
+ }
+ if *doDelete {
+ if err = os.Remove(job.Fd.Name()); err != nil {
+ log.Fatalln("Error during deletion:", err)
+ }
+ }
+ ctx.LogI("nncp-bundle", nncp.SdsAdd(sds, nncp.SDS{
+ "size": strconv.FormatInt(job.Size, 10),
+ }), "")
+ }
+ }
+ if err = tarWr.Close(); err != nil {
+ log.Fatalln("Error during tar closing:", err)
+ }
+ } else {
+ tarR := tar.NewReader(bufio.NewReaderSize(os.Stdin, CopyBufSize))
+ var entry *tar.Header
+ var sepIndex int
+ var exists bool
+ pktEncBuf := make([]byte, nncp.PktEncOverhead)
+ var pktEnc *nncp.PktEnc
+ var pktName string
+ var selfPath string
+ var dstPath string
+ for {
+ sds["xx"] = string(nncp.TRx)
+ entry, err = tarR.Next()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ log.Fatalln("Error during tar reading:", err)
+ }
+ sds["pkt"] = entry.Name
+ if entry.Size < nncp.PktEncOverhead {
+ ctx.LogD("nncp-bundle", sds, "Too small packet")
+ continue
+ }
+ sepIndex = strings.LastIndex(entry.Name, "/")
+ if sepIndex == -1 {
+ ctx.LogD("nncp-bundle", sds, "Bad packet name")
+ continue
+ }
+ pktName = entry.Name[sepIndex+1:]
+ if _, err = nncp.FromBase32(pktName); err != nil {
+ ctx.LogD("nncp-bundle", sds, "Bad packet name")
+ continue
+ }
+ if _, err = io.ReadFull(tarR, pktEncBuf); err != nil {
+ ctx.LogD("nncp-bundle", nncp.SdsAdd(sds, nncp.SDS{"err": err}), "read")
+ continue
+ }
+ if _, err = xdr.Unmarshal(bytes.NewBuffer(pktEncBuf), &pktEnc); err != nil {
+ ctx.LogD("nncp-bundle", sds, "Bad packet structure")
+ continue
+ }
+ if pktEnc.Magic != nncp.MagicNNCPEv2 {
+ ctx.LogD("nncp-bundle", sds, "Bad packet magic number")
+ continue
+ }
+ if pktEnc.Nice > nice {
+ ctx.LogD("nncp-bundle", sds, "too nice")
+ continue
+ }
+ if *pktEnc.Sender == *ctx.SelfId && *doDelete {
+ if len(nodeIds) > 0 {
+ if _, exists = nodeIds[*pktEnc.Recipient]; !exists {
+ ctx.LogD("nncp-bundle", sds, "Recipient is not requested")
+ continue
+ }
+ }
+ nodeId32 := nncp.ToBase32(pktEnc.Recipient[:])
+ sds["xx"] = string(nncp.TTx)
+ sds["node"] = nodeId32
+ sds["pkt"] = pktName
+ dstPath = filepath.Join(
+ ctx.Spool,
+ nodeId32,
+ string(nncp.TTx),
+ pktName,
+ )
+ if _, err = os.Stat(dstPath); err != nil {
+ ctx.LogD("nncp-bundle", sds, "Packet is already missing")
+ continue
+ }
+ hsh, err := blake2b.New256(nil)
+ if err != nil {
+ log.Fatalln("Error during hasher creation:", err)
+ }
+ if _, err = hsh.Write(pktEncBuf); err != nil {
+ log.Fatalln("Error during writing:", err)
+ }
+ if _, err = io.Copy(hsh, tarR); err != nil {
+ log.Fatalln("Error during copying:", err)
+ }
+ if nncp.ToBase32(hsh.Sum(nil)) == pktName {
+ ctx.LogI("nncp-bundle", sds, "removed")
+ os.Remove(dstPath)
+ } else {
+ ctx.LogE("nncp-bundle", sds, "bad checksum")
+ }
+ continue
+ }
+ if *pktEnc.Recipient != *ctx.SelfId {
+ ctx.LogD("nncp-bundle", sds, "Unknown recipient")
+ continue
+ }
+ if len(nodeIds) > 0 {
+ if _, exists = nodeIds[*pktEnc.Sender]; !exists {
+ ctx.LogD("nncp-bundle", sds, "Sender is not requested")
+ continue
+ }
+ }
+ sds["node"] = nncp.ToBase32(pktEnc.Recipient[:])
+ sds["pkt"] = pktName
+ selfPath = filepath.Join(ctx.Spool, ctx.SelfId.String(), string(nncp.TRx))
+ dstPath = filepath.Join(selfPath, pktName)
+ if _, err = os.Stat(dstPath); err == nil || !os.IsNotExist(err) {
+ ctx.LogD("nncp-bundle", sds, "Packet already exists")
+ continue
+ }
+ if *doCheck {
+ tmp, err := ctx.NewTmpFileWHash()
+ if err != nil {
+ log.Fatalln("Error during temporary file creation:", err)
+ }
+ if _, err = tmp.W.Write(pktEncBuf); err != nil {
+ log.Fatalln("Error during writing:", err)
+ }
+ if _, err = io.Copy(tmp.W, tarR); err != nil {
+ log.Fatalln("Error during copying:", err)
+ }
+ if err = tmp.W.Flush(); err != nil {
+ log.Fatalln("Error during flusing:", err)
+ }
+ if nncp.ToBase32(tmp.Hsh.Sum(nil)) == pktName {
+ if err = tmp.Commit(selfPath); err != nil {
+ log.Fatalln("Error during commiting:", err)
+ }
+ } else {
+ ctx.LogE("nncp-bundle", sds, "bad checksum")
+ tmp.Cancel()
+ continue
+ }
+ } else {
+ tmp, err := ctx.NewTmpFile()
+ if err != nil {
+ log.Fatalln("Error during temporary file creation:", err)
+ }
+ bufTmp := bufio.NewWriterSize(tmp, CopyBufSize)
+ if _, err = bufTmp.Write(pktEncBuf); err != nil {
+ log.Fatalln("Error during writing:", err)
+ }
+ if _, err = io.Copy(bufTmp, tarR); err != nil {
+ log.Fatalln("Error during copying:", err)
+ }
+ if err = bufTmp.Flush(); err != nil {
+ log.Fatalln("Error during flushing:", err)
+ }
+ tmp.Sync()
+ tmp.Close()
+ if err = os.MkdirAll(selfPath, os.FileMode(0700)); err != nil {
+ log.Fatalln("Error during mkdir:", err)
+ }
+ if err = os.Rename(tmp.Name(), dstPath); err != nil {
+ log.Fatalln("Error during renaming:", err)
+ }
+ }
+ ctx.LogI("nncp-bundle", nncp.SdsAdd(sds, nncp.SDS{
+ "size": strconv.FormatInt(entry.Size, 10),
+ }), "")
+ }
+ }
+}