nncp.info
nncp.html
+sp.utxt
MAKEINFO ?= makeinfo
-nncp.info: *.texi pedro.txt
+sp.utxt: sp.txt
+ plantuml -tutxt sp.txt
+
+nncp.info: *.texi sp.utxt pedro.txt
$(MAKEINFO) -o nncp.info index.texi
CSS != cat style.css
-nncp.html: *.texi pedro.txt
+nncp.html: *.texi sp.utxt pedro.txt
rm -f nncp.html/*.html
$(MAKEINFO) --html \
--set-customization-variable CSS_LINES='$(CSS)' \
-
cron: "*/1 * * * MON-FRI"
onlinedeadline: 3600
- nice: 64
+ nice: PRIORITY+10
-
cron: "30 * * * SAT,SUN"
onlinedeadline: 1800
maxonlinetime: 1750
- nice: 64
+ nice: NORMAL
+ rxrate: 10
+ txrate: 20
-
cron: "0 * * * SAT,SUN"
xx: rx
@ref{CfgAddrs, @emph{addrs}} configuration option. It can be either key
from @emph{addrs} dictionary, or an ordinary @option{addr:port}.
+@item rxrate/txrate
+Optional. Override @ref{CfgXxRate, @emph{rxrate/txrate}} configuration
+option when calling.
+
@item onlinedeadline
Optional. Override @ref{CfgOnlineDeadline, @emph{onlinedeadline}}
configuration option when calling.
freqchunked: 1024
freqminsize: 2048
via: [alice]
+ rxrate: 10
+ txrate: 20
@end verbatim
@strong{spool} field contains an absolute path to @ref{Spool, spool}
if either no direct connection exists, or @ref{nncp-call} is used with
forced address specifying.
+@anchor{CfgXxRate}
+@item rxrate/txrate
+If greater than zero, then at most *rate packets per second will be
+sent/received after the handshake. It could be used as crude bandwidth
+traffic shaper: each packet has at most 64 KiB payload size. Could be
+omitted at all -- no rate limits.
+
@anchor{CfgOnlineDeadline}
@item onlinedeadline
Online connection deadline of node inactivity in seconds. It is the time
will be 4 KiB (containing file itself and some junk).
@item -nice
Set desired outgoing packet @ref{Niceness, niceness level}.
- 1-255 values are allowed.
@item -replynice
Set desired reply packet @ref{Niceness, niceness level}. Only freq
- and exec packets look at that niceness level. 1-255 values are
- allowed.
+ and exec packets look at that niceness level.
@item -node
Process only single specified node.
@item -via
@section nncp-call
@verbatim
-% nncp-call [options] [-onlinedeadline INT] [-maxonlinetime INT] [-rx|-tx]
- NODE[:ADDR] [FORCEADDR]
+% nncp-call [options]
+ [-onlinedeadline INT]
+ [-maxonlinetime INT]
+ [-rx|-tx]
+ [-rxrate INT]
+ [-txrate INT]
+ NODE[:ADDR] [FORCEADDR]
@end verbatim
Call (connect to) specified @option{NODE} and run @ref{Sync,
only outbound transmission is performed. @option{-onlinedeadline}
overrides @ref{CfgOnlineDeadline, @emph{onlinedeadline}}.
@option{-maxonlinetime} overrides @ref{CfgMaxOnlineTime,
-@emph{maxonlinetime}}.
+@emph{maxonlinetime}}. @option{-rxrate}/@option{-txrate} override
+@ref{CfgXxRate, rxrate/txrate}.
@node nncp-caller
@section nncp-caller
@multitable {XXXXX} {XXXX KiB} {link sign} {xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
@headitem Version @tab Size @tab Tarball @tab SHA256 checksum
+@item @ref{Release 3.2, 3.2} @tab 1147 KiB
+@tab @url{download/nncp-3.2.tar.xz, link} @url{download/nncp-3.2.tar.xz.sig, sign}
+@tab @code{BE76802F 1E273D1D E91F0648 A7CB23C5 989F5390 A36F2D0C FD873046 51B9141E}
+
@item @ref{Release 3.1, 3.1} @tab 1145 KiB
@tab @url{download/nncp-3.1.tar.xz, link} @url{download/nncp-3.1.tar.xz.sig, sign}
@tab @code{B9344516 4230B58E 8AAADAA2 066F37F2 493CCB71 B025126B BCAD8FAD 6535149F}
@node Новости
@section Новости
+@node Релиз 3.3
+@subsection Релиз 3.3
+@itemize
+@item
+@command{nncp-daemon}, @command{nncp-call}, @command{nncp-caller}
+проверяют существование @file{.seen} файла и расценивают его как то, что
+файл уже был скачан. Возможно передача данных была осуществлена
+сторонним способом и удалённая сторона должна быть оповещена об этом.
+@item
+Если более высокоприоритетный пакет попадает в спул, то
+@command{nncp-daemon} добавит его в очередь отправки первым, прерывая
+низкоприоритетные передачи.
+@item
+К средствам связанным с online-соединениями (@command{nncp-daemon},
+@command{nncp-call}, @command{nncp-caller}) добавлен простой
+ограничитель скорости.
+@item
+Возможность задания приоритета символьными обозначениями:
+@verb{|NORMAL|}, @verb{|BULK+10|}, @verb{|PRIORITY-5|}, итд.
+@item
+Изменены значения приоритетов по-умолчанию:
+для @command{nncp-exec} с 64 на 96,
+для @command{nncp-freq} с 64 на 160,
+для @command{nncp-file} с 196 на 224.
+@end itemize
+
@node Релиз 3.2
@subsection Релиз 3.2
@itemize
See also this page @ref{Новости, on russian}.
+@node Release 3.3
+@section Release 3.3
+@itemize
+@item
+@command{nncp-daemon}, @command{nncp-call}, @command{nncp-caller} check
+if @file{.seen} exists and treat it like file was already downloaded.
+Possibly it was transferred out-of-bound and remote side needs to be
+notifier about that.
+@item
+If higher priority packet is spooled, then @command{nncp-daemon} will
+queue its sending first, interrupting lower priority transmissions.
+@item
+Simple packet rate limiter added to online-related tools
+(@command{nncp-daemon}, @command{nncp-call}, @command{nncp-caller}).
+@item
+Ability to specify niceness with symbolic notation:
+@verb{|NORMAL|}, @verb{|BULK+10|}, @verb{|PRIORITY-5|}, итд.
+@item
+Changed default niceness levels:
+for @command{nncp-exec} from 64 to 96,
+for @command{nncp-freq} from 64 to 160,
+for @command{nncp-file} from 196 to 224.
+@end itemize
+
@node Release 3.2
@section Release 3.2
@itemize
that packet is "nicer" and allows other to bypass him -- that means
lower transmission precedence.
-Send big files with higher nicer level! That will guarantee you that
+Send big files with higher nice level! That will guarantee you that
higher priority packets, like mail messages, will pass first, even when
-lower priority packet was already been partly downloaded.
+lower priority packet was already been partially downloaded.
There are default niceness levels built-in for @ref{nncp-exec},
@ref{nncp-file} and @ref{nncp-freq} commands. But pay attention that it
can give information about underlying payload to the adversary!
+
+There are 1-255 niceness levels. They could be specified either as
+integer, or using aliases with delta modifiers:
+
+@table @emph
+@item FLASH (F)
+Urgent priority.
+@item PRIORITY (P)
+High priority. Command execution/mail use that priority by default.
+@item NORMAL (N)
+Normal priority. File requests use that priority by default.
+@item BULK (B)
+Bundles shipped on a "least effort" basis. File transmission use that
+priority by default.
+@end table
+
+@verbatim
+ 1: F-31 65: P-31 129: N-31 193: B-31
+ 2: F-30 66: P-30 130: N-30 194: B-30
+ ... ... ... ...
+32: F 96: P 160: N 224: B
+33: F+1 97: P+1 161: N+1 225: B+1
+34: F+2 98: P+2 162: N+2 226: B+2
+ ... ... ... ...
+64: F+32 128: P+32 192: N+32 255: B+31 | MAX
+@end verbatim
+
+Precedence could be specified both with single-letter aliases and with
+whole strings. They are case insensitive. @emph{MAX} is an alias for 255
+niceness level.
Typical peer's behaviour is following:
+@verbatiminclude sp.utxt
+
@enumerate
-@item Perform Noise-IK handshake.
-@item When remote peer's identity is known (by definition for initiator
-and after receiving first packet for responser (however it is not
-authenticated yet)), then collect all @emph{tx}-related files
-information and prepare payload packets with all that @emph{INFO}s.
-@item Pad the very first payload packet (that is sent with first Noise
-handshake message) with @emph{HALT}s to the maximal size.
-@item Send all queued payload packets.
-@item When @emph{INFO} packet received, check that is has an acceptable
-niceness level (skip if not), check if file's @file{.part} exists and
-queue @emph{FREQ} outgoing packet (with corresponding offset if
-required).
-@item When @emph{FREQ} packet received, append it to current sending
-queue. Sending queue contains files with offsets that are needed to be
+@item Perform @emph{Noise-IK} handshake:
+
+ @table @strong
+ @item Initiator
+ Collects all @emph{tx}-related files information and prepares
+ payload filled with @emph{INFO}s for including in the @strong{first}
+ handshake message.
+ @item Responder
+ After receiving the first handshake message, it gains remote
+ identity knowledge and similarly prepares the payload for including
+ in the @strong{second} handshake message.
+ @end table
+
+ All payloads are padded to maximal message size with @emph{HALT}s.
+
+@item If queued @emph{INFO}s are not sent completely in handshake
+payloads, then send all of remaining in the transport stage.
+
+@item When @emph{INFO} packet received:
+
+ @itemize
+ @item Check that it has an acceptable niceness level.
+ Ignore it if it is too nice.
+ @item If already downloaded file exists, then queue @emph{DONE}
+ sending.
+ @item If @file{.seen} exists, then queue @emph{DONE} sending.
+ @item If @file{.part} exists, then queue @emph{FREQ} sending with
+ corresponding offset.
+ @end itemize
+
+@item When @emph{FREQ} packet received, insert it to current sending
+queue with niceness level sort: higher priority packets will be sent
+first. Sending queue contains files with offsets that are needed to be
sent.
-@item While sending queue is not empty, send @emph{FILE} packet until
-queue's head is not fully sent. @emph{FREQ} can contain offset equal to
-size -- anyway sent @emph{FILE} packet with an empty payload.
-@item When @emph{FILE} packet received, check if it is not fully
-downloaded (comparing to @emph{INFO}'s packet information). If so, then
-run background integrity checker on it. If check is succeeded, then
+
+@item While sending queue is not empty, send @emph{FILE} packets.
+@emph{FREQ} could contain offset equal to size -- anyway sent
+@emph{FILE} packet with an empty payload. @emph{FILE} sending is
+performed only if no other outgoing packets are queued: @emph{INFO}s
+have higher priority.
+
+@item When @emph{FILE} packet received, check if it is completely
+downloaded (comparing to @emph{INFO}'s packet size information). If so,
+then run background integrity checker on it. If check succeeds, then
delete @file{.part} suffix from file's name and send @emph{DONE} packet.
+
@item When @emph{DONE} packet received, delete corresponding file.
@item When @emph{HALT} packet received, empty file sending queue.
-@item @emph{FILE} sending is performed only if no other outgoing packets
-are queued.
+
@item Each second, node checks: are there any new @emph{tx} packets
appeared and queues corresponding @emph{INFO} packets.
+
@item If no packets are sent and received during @ref{CfgOnlineDeadline,
onlinedeadline} duration, then close the connection. There is no
explicit indication that session is over.
+
@end enumerate
--- /dev/null
+@startuml
+hide footbox
+participant Initiator
+participant Responder
+
+== preparation ==
+
+Initiator <- Responder : [s]
+
+== interactive ==
+
+Initiator -> Responder : [e, es, s, ss], INFO..., HALT...
+Initiator <- Responder : [e, ee, se], INFO..., HALT...
+Initiator -> Responder : INFO..., FREQ..., DONE...
+Initiator <- Responder : INFO..., FREQ..., DONE...
+Initiator -> Responder : FILE..., INFO..., DONE...
+Initiator <- Responder : FILE..., INFO..., DONE...
+
+@enduml
Except for @file{tmp}, all other directories are Base32-encoded node
identifiers (@file{2WHB...OABQ}, @file{BYRR...CG6Q} in our example).
-Each node subdirectory has @file{rx} (received, partly received and
+Each node subdirectory has @file{rx} (received, partially received and
currently unprocessed packets) and @file{tx} (for outbound packets)
directories.
Each @file{rx}/@file{tx} directory contains one file per encrypted
packet. Its filename is Base32 encoded BLAKE2b hash of the contents. So
it can be integrity checked at any time. @file{5ZIB...UMKW.part} is
-partly received file from @file{2WHB...OABQ} node. @file{tx} directory
-can not contain partly written files -- they are moved atomically from
-@file{tmp}.
+partially received file from @file{2WHB...OABQ} node. @file{tx}
+directory can not contain partially written files -- they are moved
+atomically from @file{tmp}.
When @ref{nncp-toss} utility is called with @option{-seen} option, it
will create empty @file{XXX.seen} files, telling that some kind of
-# $FreeBSD: head/net/nncp/Makefile 460314 2018-01-29 16:17:45Z yuri $
+# $FreeBSD: head/net/nncp/Makefile 471003 2018-05-27 20:24:00Z krion $
PORTNAME= nncp
-DISTVERSION= 3.1
+DISTVERSION= 3.3
CATEGORIES= net
MASTER_SITES= http://www.nncpgo.org/download/
MAINTAINER= stargrave@stargrave.org
-COMMENT= Utilities for secure store-and-forward files, mail and command exchanging
+COMMENT= Utilities for secure store-and-forward files, mail, command exchanging
LICENSE= GPLv3+
LICENSE_FILE= ${WRKSRC}/COPYING
import (
"net"
"strconv"
+
+ "github.com/gorhill/cronexpr"
)
-func (ctx *Ctx) CallNode(node *Node, addrs []string, nice uint8, xxOnly TRxTx, onlineDeadline, maxOnlineTime uint) (isGood bool) {
+type Call struct {
+ Cron *cronexpr.Expression
+ Nice uint8
+ Xx TRxTx
+ RxRate int
+ TxRate int
+ Addr *string
+ OnlineDeadline uint
+ MaxOnlineTime uint
+}
+
+func (ctx *Ctx) CallNode(
+ node *Node,
+ addrs []string,
+ nice uint8,
+ xxOnly TRxTx,
+ rxRate, txRate int,
+ onlineDeadline, maxOnlineTime uint) (isGood bool) {
for _, addr := range addrs {
sds := SDS{"node": node.Id, "addr": addr}
ctx.LogD("call", sds, "dialing")
node.Id,
nice,
xxOnly,
+ rxRate,
+ txRate,
onlineDeadline,
maxOnlineTime,
)
Id string
ExchPub string
SignPub string
- NoisePub *string `noisepub,omitempty`
- Exec map[string][]string `exec,omitempty`
- Incoming *string `incoming,omitempty`
- Freq *string `freq,omitempty`
- FreqChunked *uint64 `freqchunked,omitempty`
- FreqMinSize *uint64 `freqminsize,omitempty`
- Via []string `via,omitempty`
- Calls []CallYAML `calls,omitempty`
-
- Addrs map[string]string `addrs,omitempty`
-
- OnlineDeadline *uint `onlinedeadline,omitempty`
- MaxOnlineTime *uint `maxonlinetime,omitempty`
+ NoisePub *string `yaml:"noisepub,omitempty"`
+ Exec map[string][]string `yaml:"exec,omitempty"`
+ Incoming *string `yaml:"incoming,omitempty"`
+ Freq *string `yaml:"freq,omitempty"`
+ FreqChunked *uint64 `yaml:"freqchunked,omitempty"`
+ FreqMinSize *uint64 `yaml:"freqminsize,omitempty"`
+ Via []string `yaml:"via,omitempty"`
+ Calls []CallYAML `yaml:"calls,omitempty"`
+
+ Addrs map[string]string `yaml:"addrs,omitempty"`
+
+ RxRate *int `yaml:"rxrate,omitempty"`
+ TxRate *int `yaml:"txrate,omitempty"`
+ OnlineDeadline *uint `yaml:"onlinedeadline,omitempty"`
+ MaxOnlineTime *uint `yaml:"maxonlinetime,omitempty"`
}
type CallYAML struct {
Cron string
- Nice *int `nice,omitempty`
- Xx string `xx,omitempty`
- Addr *string `addr,omitempty`
- OnlineDeadline *uint `onlinedeadline,omitempty`
- MaxOnlineTime *uint `maxonlinetime,omitempty`
+ Nice *string `yaml:"nice,omitempty"`
+ Xx string `yaml:"xx,omitempty"`
+ RxRate *int `yaml:"rxrate,omitempty"`
+ TxRate *int `yaml:"txrate,omitempty"`
+ Addr *string `yaml:"addr,omitempty"`
+ OnlineDeadline *uint `yaml:"onlinedeadline,omitempty"`
+ MaxOnlineTime *uint `yaml:"maxonlinetime,omitempty"`
}
type NodeOurYAML struct {
}
type NotifyYAML struct {
- File *FromToYAML `file,omitempty`
- Freq *FromToYAML `freq,omitempty`
+ File *FromToYAML `yaml:"file,omitempty"`
+ Freq *FromToYAML `yaml:"freq,omitempty"`
}
type CfgYAML struct {
- Self *NodeOurYAML `self,omitempty`
+ Self *NodeOurYAML `yaml:"self,omitempty"`
Neigh map[string]NodeYAML
Spool string
Log string
- Notify *NotifyYAML `notify,omitempty`
+ Notify *NotifyYAML `yaml:"notify,omitempty"`
}
func NewNode(name string, yml NodeYAML) (*Node, error) {
freqMinSize = int64(*yml.FreqMinSize) * 1024
}
+ defRxRate := 0
+ if yml.RxRate != nil && *yml.RxRate > 0 {
+ defRxRate = *yml.RxRate
+ }
+ defTxRate := 0
+ if yml.TxRate != nil && *yml.TxRate > 0 {
+ defTxRate = *yml.TxRate
+ }
+
defOnlineDeadline := uint(DefaultDeadline)
if yml.OnlineDeadline != nil {
if *yml.OnlineDeadline <= 0 {
if err != nil {
return nil, err
}
+
nice := uint8(255)
if callYml.Nice != nil {
- if *callYml.Nice < 1 || *callYml.Nice > 255 {
- return nil, errors.New("Nice must be between 1 and 255")
+ nice, err = NicenessParse(*callYml.Nice)
+ if err != nil {
+ return nil, err
}
- nice = uint8(*callYml.Nice)
}
+
var xx TRxTx
switch callYml.Xx {
case "rx":
default:
return nil, errors.New("xx field must be either \"rx\" or \"tx\"")
}
+
+ rxRate := 0
+ if callYml.RxRate != nil && *callYml.RxRate > 0 {
+ rxRate = *callYml.RxRate
+ }
+ txRate := 0
+ if callYml.TxRate != nil && *callYml.TxRate > 0 {
+ txRate = *callYml.TxRate
+ }
+
var addr *string
if callYml.Addr != nil {
if a, exists := yml.Addrs[*callYml.Addr]; exists {
addr = callYml.Addr
}
}
+
onlineDeadline := defOnlineDeadline
if callYml.OnlineDeadline != nil {
if *callYml.OnlineDeadline == 0 {
}
onlineDeadline = *callYml.OnlineDeadline
}
+
var maxOnlineTime uint
if callYml.MaxOnlineTime != nil {
maxOnlineTime = *callYml.MaxOnlineTime
}
+
calls = append(calls, &Call{
Cron: expr,
Nice: nice,
Xx: xx,
+ RxRate: rxRate,
+ TxRate: txRate,
Addr: addr,
OnlineDeadline: onlineDeadline,
MaxOnlineTime: maxOnlineTime,
FreqMinSize: freqMinSize,
Calls: calls,
Addrs: yml.Addrs,
+ RxRate: defRxRate,
+ TxRate: defTxRate,
OnlineDeadline: defOnlineDeadline,
MaxOnlineTime: defMaxOnlineTime,
}
func main() {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
- niceRaw = flag.Int("nice", 255, "Minimal required niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(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")
fmt.Println(nncp.VersionGet())
return
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
if *doRx && *doTx {
log.Fatalln("-rx and -tx can not be set simultaneously")
}
func main() {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
- niceRaw = flag.Int("nice", 255, "Minimal required niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(255), "Minimal required niceness")
rxOnly = flag.Bool("rx", false, "Only receive packets")
- txOnly = flag.Bool("tx", false, "Only transfer packets")
+ txOnly = flag.Bool("tx", false, "Only transmit packets")
+ rxRate = flag.Int("rxrate", 0, "Maximal receive rate, pkts/sec")
+ txRate = flag.Int("txrate", 0, "Maximal transmit rate, pkts/sec")
spoolPath = flag.String("spool", "", "Override path to spool")
logPath = flag.String("log", "", "Override path to logfile")
quiet = flag.Bool("quiet", false, "Print only errors")
usage()
os.Exit(1)
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
if *rxOnly && *txOnly {
log.Fatalln("-rx and -tx can not be set simultaneously")
}
}
}
- if !ctx.CallNode(node, addrs, nice, xxOnly, *onlineDeadline, *maxOnlineTime) {
+ if !ctx.CallNode(
+ node,
+ addrs,
+ nice,
+ xxOnly,
+ *rxRate,
+ *txRate,
+ *onlineDeadline,
+ *maxOnlineTime,
+ ) {
os.Exit(1)
}
}
addrs,
call.Nice,
call.Xx,
+ call.RxRate,
+ call.TxRate,
call.OnlineDeadline,
call.MaxOnlineTime,
)
func main() {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
- niceRaw = flag.Int("nice", 255, "Minimal required niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(255), "Minimal required niceness")
bind = flag.String("bind", "[::]:5400", "Address to bind to")
maxConn = flag.Int("maxconn", 128, "Maximal number of simultaneous connections")
spoolPath = flag.String("spool", "", "Override path to spool")
fmt.Println(nncp.VersionGet())
return
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
ctx, err := nncp.CtxFromCmdline(*cfgPath, *spoolPath, *logPath, *quiet, *debug)
if err != nil {
func main() {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
- niceRaw = flag.Int("nice", nncp.DefaultNiceExec, "Outbound packet niceness")
- replyNiceRaw = flag.Int("replynice", nncp.DefaultNiceFile, "Possible reply packet niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(nncp.DefaultNiceExec), "Outbound packet niceness")
+ replyNiceRaw = flag.String("replynice", nncp.NicenessFmt(nncp.DefaultNiceFile), "Possible reply packet niceness")
minSize = flag.Uint64("minsize", 0, "Minimal required resulting packet size, in KiB")
viaOverride = flag.String("via", "", "Override Via path to destination node")
spoolPath = flag.String("spool", "", "Override path to spool")
usage()
os.Exit(1)
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
- if *replyNiceRaw < 1 || *replyNiceRaw > 255 {
- log.Fatalln("-replynice must be between 1 and 255")
+ replyNice, err := nncp.NicenessParse(*replyNiceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- replyNice := uint8(*replyNiceRaw)
ctx, err := nncp.CtxFromCmdline(*cfgPath, *spoolPath, *logPath, *quiet, *debug)
if err != nil {
func main() {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
- niceRaw = flag.Int("nice", nncp.DefaultNiceFile, "Outbound packet niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(nncp.DefaultNiceFile), "Outbound packet niceness")
argMinSize = flag.Int64("minsize", -1, "Minimal required resulting packet size, in KiB")
argChunkSize = flag.Int64("chunked", -1, "Split file on specified size chunks, in KiB")
viaOverride = flag.String("via", "", "Override Via path to destination node")
usage()
os.Exit(1)
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
ctx, err := nncp.CtxFromCmdline(*cfgPath, *spoolPath, *logPath, *quiet, *debug)
if err != nil {
"log"
"os"
"path/filepath"
+ "strconv"
"strings"
"cypherpunks.ru/nncp"
func main() {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
- niceRaw = flag.Int("nice", nncp.DefaultNiceFreq, "Outbound packet niceness")
- replyNiceRaw = flag.Int("replynice", nncp.DefaultNiceFile, "Reply file packet niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(nncp.DefaultNiceFreq), "Outbound packet niceness")
+ replyNiceRaw = flag.String("replynice", strconv.Itoa(nncp.DefaultNiceFile), "Reply file packet niceness")
minSize = flag.Uint64("minsize", 0, "Minimal required resulting packet size, in KiB")
viaOverride = flag.String("via", "", "Override Via path to destination node")
spoolPath = flag.String("spool", "", "Override path to spool")
usage()
os.Exit(1)
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
- if *replyNiceRaw < 1 || *replyNiceRaw > 255 {
- log.Fatalln("-replynice must be between 1 and 255")
+ replyNice, err := nncp.NicenessParse(*replyNiceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- replyNice := uint8(*replyNiceRaw)
ctx, err := nncp.CtxFromCmdline(*cfgPath, *spoolPath, *logPath, *quiet, *debug)
if err != nil {
path = string(pkt.Path[:pkt.PathLen])
}
fmt.Printf(
- "Packet type: plain\nPayload type: %s\nNiceness: %d\nPath: %s\n",
- payloadType, pkt.Nice, path,
+ "Packet type: plain\nPayload type: %s\nNiceness: %s (%d)\nPath: %s\n",
+ payloadType, nncp.NicenessFmt(pkt.Nice), pkt.Nice, path,
)
return
}
return
}
fmt.Printf(
- "Packet type: encrypted\nNiceness: %d\nSender: %s\nRecipient: %s\n",
- pktEnc.Nice, pktEnc.Sender, pktEnc.Recipient,
+ "Packet type: encrypted\nNiceness: %s (%d)\nSender: %s\nRecipient: %s\n",
+ nncp.NicenessFmt(pktEnc.Nice), pktEnc.Nice, pktEnc.Sender, pktEnc.Recipient,
)
return
}
txBytes[job.PktEnc.Nice] = txBytes[job.PktEnc.Nice] + job.Size
}
fmt.Println(node.Name)
- for nice := 0; nice < 256; nice++ {
- rxNum, rxExists := rxNums[uint8(nice)]
- txNum, txExists := txNums[uint8(nice)]
+ var nice uint8
+ for nice = 1; nice > 0; nice++ {
+ rxNum, rxExists := rxNums[nice]
+ txNum, txExists := txNums[nice]
if !(rxExists || txExists) {
continue
}
fmt.Printf(
- "\tnice:% 3d | Rx: % 10s, % 3d pkts | Tx: % 10s, % 3d pkts\n",
- nice,
- humanize.IBytes(uint64(rxBytes[uint8(nice)])),
+ "\tnice:% 4s | Rx: % 10s, % 3d pkts | Tx: % 10s, % 3d pkts\n",
+ nncp.NicenessFmt(nice),
+ humanize.IBytes(uint64(rxBytes[nice])),
rxNum,
- humanize.IBytes(uint64(txBytes[uint8(nice)])),
+ humanize.IBytes(uint64(txBytes[nice])),
txNum,
)
}
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
nodeRaw = flag.String("node", "", "Process only that node")
- niceRaw = flag.Int("nice", 255, "Minimal required niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(255), "Minimal required niceness")
dryRun = flag.Bool("dryrun", false, "Do not actually write any tossed data")
doSeen = flag.Bool("seen", false, "Create .seen files")
cycle = flag.Uint("cycle", 0, "Repeat tossing after N seconds in infinite loop")
fmt.Println(nncp.VersionGet())
return
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
ctx, err := nncp.CtxFromCmdline(*cfgPath, *spoolPath, *logPath, *quiet, *debug)
if err != nil {
var (
cfgPath = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
nodeRaw = flag.String("node", "", "Process only that node")
- niceRaw = flag.Int("nice", 255, "Minimal required niceness")
+ niceRaw = flag.String("nice", nncp.NicenessFmt(255), "Minimal required niceness")
rxOnly = flag.Bool("rx", false, "Only receive packets")
txOnly = flag.Bool("tx", false, "Only transfer packets")
mkdir = flag.Bool("mkdir", false, "Create necessary outbound directories")
usage()
os.Exit(1)
}
- if *niceRaw < 1 || *niceRaw > 255 {
- log.Fatalln("-nice must be between 1 and 255")
+ nice, err := nncp.NicenessParse(*niceRaw)
+ if err != nil {
+ log.Fatalln(err)
}
- nice := uint8(*niceRaw)
if *rxOnly && *txOnly {
log.Fatalln("-rx and -tx can not be set simultaneously")
}
--- /dev/null
+package nncp
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+const (
+ NiceFlash = 32
+ NicePriority = 96
+ NiceNormal = 160
+ NiceBulk = 224
+
+ DefaultNiceExec = NicePriority
+ DefaultNiceFreq = NiceNormal
+ DefaultNiceFile = NiceBulk
+)
+
+var (
+ niceRe *regexp.Regexp = regexp.MustCompile(`^(\w+)([-+])(\d+)$`)
+ niceAliases map[string]uint8 = map[string]uint8{
+ "flash": NiceFlash,
+ "f": NiceFlash,
+ "priority": NicePriority,
+ "p": NicePriority,
+ "normal": NiceNormal,
+ "n": NiceNormal,
+ "bulk": NiceBulk,
+ "b": NiceBulk,
+ "max": 255,
+ }
+)
+
+func NicenessParse(s string) (uint8, error) {
+ if nice, err := strconv.Atoi(s); err == nil {
+ if nice <= 0 || nice > 255 {
+ return 0, errors.New("nice out of bounds")
+ }
+ return uint8(nice), nil
+ }
+ s = strings.ToLower(s)
+ var baseNice uint8
+ var found bool
+ if baseNice, found = niceAliases[s]; found {
+ return baseNice, nil
+ }
+ matches := niceRe.FindStringSubmatch(s)
+ if len(matches) != 1+3 {
+ return 0, errors.New("invalid niceness")
+ }
+ baseNice, found = niceAliases[matches[1]]
+ if !found {
+ return 0, errors.New("invalid niceness")
+ }
+ delta, err := strconv.Atoi(matches[3])
+ if err != nil {
+ return 0, err
+ }
+ if matches[2] == "-" {
+ if delta > 31 {
+ return 0, errors.New("too big niceness delta")
+ }
+ return baseNice - uint8(delta), nil
+ } else {
+ if delta > 32 || (baseNice == NiceBulk && delta > 31) {
+ return 0, errors.New("too big niceness delta")
+ }
+ return baseNice + uint8(delta), nil
+ }
+}
+
+func NicenessFmt(nice uint8) string {
+ switch {
+ case nice == 255:
+ return "MAX"
+ case NiceFlash-31 < nice && nice < NiceFlash:
+ return fmt.Sprintf("F-%d", NiceFlash-nice)
+ case nice == NiceFlash:
+ return "F"
+ case NiceFlash < nice && nice <= (NiceFlash+32):
+ return fmt.Sprintf("F+%d", nice-NiceFlash)
+
+ case NicePriority-31 < nice && nice < NicePriority:
+ return fmt.Sprintf("P-%d", NicePriority-nice)
+ case nice == NicePriority:
+ return "P"
+ case NicePriority < nice && nice <= (NicePriority+32):
+ return fmt.Sprintf("P+%d", nice-NicePriority)
+
+ case NiceNormal-31 < nice && nice < NiceNormal:
+ return fmt.Sprintf("N-%d", NiceNormal-nice)
+ case nice == NiceNormal:
+ return "N"
+ case NiceNormal < nice && nice <= (NiceNormal+32):
+ return fmt.Sprintf("N+%d", nice-NiceNormal)
+
+ case NiceBulk-31 < nice && nice < NiceBulk:
+ return fmt.Sprintf("B-%d", NiceBulk-nice)
+ case nice == NiceBulk:
+ return "B"
+ case NiceBulk < nice && nice <= (NiceBulk+30):
+ return fmt.Sprintf("B+%d", nice-NiceBulk)
+ }
+ return strconv.Itoa(int(nice))
+}
--- /dev/null
+package nncp
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestNiceSymmetric(t *testing.T) {
+ var nice uint8
+ for nice = 1; nice > 0; nice++ {
+ s := NicenessFmt(nice)
+ parsed, err := NicenessParse(s)
+ if err != nil || parsed != nice {
+ t.Error(err)
+ }
+ parsed, err = NicenessParse(strings.ToLower(s))
+ if err != nil || parsed != nice {
+ t.Error(err)
+ }
+ }
+}
"sync"
"github.com/flynn/noise"
- "github.com/gorhill/cronexpr"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/box"
FreqMinSize int64
Via []*NodeId
Addrs map[string]string
+ RxRate int
+ TxRate int
OnlineDeadline uint
MaxOnlineTime uint
Calls []*Call
NoisePrv *[32]byte
}
-type Call struct {
- Cron *cronexpr.Expression
- Nice uint8
- Xx TRxTx
- Addr *string
- OnlineDeadline uint
- MaxOnlineTime uint
-}
-
func NewNodeGenerate() (*NodeOur, error) {
exchPub, exchPrv, err := box.GenerateKey(rand.Reader)
if err != nil {
MaxPathSize = 1<<8 - 1
- DefaultNiceExec = 64
- DefaultNiceFreq = 64
- DefaultNiceFile = 196
-
NNCPBundlePrefix = "NNCP"
)
return written, nil
}
-func PktEncWrite(our *NodeOur, their *Node, pkt *Pkt, nice uint8, size, padSize int64, data io.Reader, out io.Writer) error {
+func PktEncWrite(
+ our *NodeOur,
+ their *Node,
+ pkt *Pkt,
+ nice uint8,
+ size, padSize int64,
+ data io.Reader,
+ out io.Writer) error {
pubEph, prvEph, err := box.GenerateKey(rand.Reader)
if err != nil {
return err
if err != nil {
return err
}
- lr := io.LimitedReader{data, size}
+ lr := io.LimitedReader{R: data, N: size}
mr := io.MultiReader(&pktBuf, &lr)
mw := io.MultiWriter(out, mac)
fullSize := pktBuf.Len() + int(size)
if _, err = io.ReadFull(kdf, keyEnc[:]); err != nil {
return err
}
- lr = io.LimitedReader{DevZero{}, padSize}
+ lr = io.LimitedReader{R: DevZero{}, N: padSize}
written, err = ae(keyEnc, &lr, out)
if err != nil {
return err
return ed25519.Verify(their.SignPub, tbsBuf.Bytes(), pktEnc.Sign[:]), nil
}
-func PktEncRead(our *NodeOur, nodes map[NodeId]*Node, data io.Reader, out io.Writer) (*Node, int64, error) {
+func PktEncRead(
+ our *NodeOur,
+ nodes map[NodeId]*Node,
+ data io.Reader,
+ out io.Writer) (*Node, int64, error) {
var pktEnc PktEnc
_, err := xdr.Unmarshal(data, &pktEnc)
if err != nil {
}
fullSize := PktOverhead + size - 8 - 2*blake2b.Size256
- lr := io.LimitedReader{data, fullSize}
+ lr := io.LimitedReader{R: data, N: fullSize}
tr := io.TeeReader(&lr, mac)
written, err := ae(keyEnc, tr, out)
if err != nil {
if err != nil {
panic(err)
}
- f := func(path string, pathSize uint8, data [1 << 16]byte, size, padSize uint16, junk []byte) bool {
+ f := func(
+ path string,
+ pathSize uint8,
+ data [1 << 16]byte,
+ size, padSize uint16,
+ junk []byte) bool {
dataR := bytes.NewReader(data[:])
var ct bytes.Buffer
if len(path) > int(pathSize) {
Payload []byte
}
+type FreqWithNice struct {
+ freq *SPFreq
+ nice uint8
+}
+
func init() {
var buf bytes.Buffer
spHead := SPHead{Type: SPTypeHalt}
csTheir *noise.CipherState
payloads chan []byte
infosTheir map[[32]byte]*SPInfo
- infosOurSeen map[[32]byte]struct{}
- queueTheir []*SPFreq
+ infosOurSeen map[[32]byte]uint8
+ queueTheir []*FreqWithNice
wg sync.WaitGroup
RxBytes int64
RxLastSeen time.Time
rxLock *os.File
txLock *os.File
xxOnly TRxTx
+ rxRate int
+ txRate int
isDead bool
sync.RWMutex
}
if state.maxOnlineTime > 0 && state.started.Add(time.Duration(state.maxOnlineTime)*time.Second).Before(now) {
return true
}
- return uint(now.Sub(state.RxLastSeen).Seconds()) >= state.onlineDeadline && uint(now.Sub(state.TxLastSeen).Seconds()) >= state.onlineDeadline
+ return uint(now.Sub(state.RxLastSeen).Seconds()) >= state.onlineDeadline &&
+ uint(now.Sub(state.TxLastSeen).Seconds()) >= state.onlineDeadline
}
func (state *SPState) dirUnlock() {
return sp.Payload, nil
}
-func (ctx *Ctx) infosOur(nodeId *NodeId, nice uint8, seen *map[[32]byte]struct{}) [][]byte {
+func (ctx *Ctx) infosOur(nodeId *NodeId, nice uint8, seen *map[[32]byte]uint8) [][]byte {
var infos []*SPInfo
var totalSize int64
for job := range ctx.Jobs(nodeId, TTx) {
Size: uint64(job.Size),
Hash: job.HshValue,
})
- (*seen)[*job.HshValue] = struct{}{}
+ (*seen)[*job.HshValue] = job.PktEnc.Nice
}
sort.Sort(ByNice(infos))
var payloads [][]byte
return payloadsSplit(payloads)
}
-func (ctx *Ctx) StartI(conn net.Conn, nodeId *NodeId, nice uint8, xxOnly TRxTx, onlineDeadline, maxOnlineTime uint) (*SPState, error) {
+func (ctx *Ctx) StartI(
+ conn net.Conn,
+ nodeId *NodeId,
+ nice uint8,
+ xxOnly TRxTx,
+ rxRate, txRate int,
+ onlineDeadline, maxOnlineTime uint) (*SPState, error) {
err := ctx.ensureRxDir(nodeId)
if err != nil {
return nil, err
nice: nice,
payloads: make(chan []byte),
infosTheir: make(map[[32]byte]*SPInfo),
- infosOurSeen: make(map[[32]byte]struct{}),
+ infosOurSeen: make(map[[32]byte]uint8),
started: started,
rxLock: rxLock,
txLock: txLock,
xxOnly: xxOnly,
+ rxRate: rxRate,
+ txRate: txRate,
}
var infosPayloads [][]byte
hs: hs,
nice: nice,
payloads: make(chan []byte),
- infosOurSeen: make(map[[32]byte]struct{}),
+ infosOurSeen: make(map[[32]byte]uint8),
infosTheir: make(map[[32]byte]*SPInfo),
started: started,
xxOnly: xxOnly,
return nil, errors.New("Unknown peer: " + peerId)
}
state.Node = node
+ state.rxRate = node.RxRate
+ state.txRate = node.TxRate
state.onlineDeadline = node.OnlineDeadline
state.maxOnlineTime = node.MaxOnlineTime
sds := SDS{"node": node.Id, "nice": strconv.Itoa(int(nice))}
return &state, err
}
-func (state *SPState) StartWorkers(conn net.Conn, infosPayloads [][]byte, payload []byte) error {
+func (state *SPState) StartWorkers(
+ conn net.Conn,
+ infosPayloads [][]byte,
+ payload []byte) error {
sds := SDS{"node": state.Node.Id, "nice": strconv.Itoa(int(state.nice))}
if len(infosPayloads) > 1 {
go func() {
time.Sleep(100 * time.Millisecond)
continue
}
- freq := state.queueTheir[0]
+ freq := state.queueTheir[0].freq
state.RUnlock()
+
+ if state.txRate > 0 {
+ time.Sleep(time.Second / time.Duration(state.txRate))
+ }
+
sdsp := SdsAdd(sds, SDS{
"xx": string(TTx),
"hash": ToBase32(freq.Hash[:]),
sdsp["fullsize"] = strconv.FormatInt(int64(fullSize), 10)
state.ctx.LogP("sp-file", sdsp, "")
state.Lock()
- if len(state.queueTheir) > 0 && *state.queueTheir[0].Hash == *freq.Hash {
+ if len(state.queueTheir) > 0 && *state.queueTheir[0].freq.Hash == *freq.Hash {
if ourSize == fullSize {
state.ctx.LogD("sp-file", sdsp, "finished")
if len(state.queueTheir) > 1 {
state.queueTheir = state.queueTheir[:0]
}
} else {
- state.queueTheir[0].Offset += uint64(len(buf))
+ state.queueTheir[0].freq.Offset += uint64(len(buf))
}
} else {
state.ctx.LogD("sp-file", sdsp, "queue disappeared")
SdsAdd(sds, SDS{"size": strconv.Itoa(len(payload))}),
"sending",
)
- conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
+ conn.SetWriteDeadline(time.Now().Add(DefaultDeadline * time.Second))
if err := state.WriteSP(conn, state.csOur.Encrypt(nil, nil, payload)); err != nil {
state.ctx.LogE("sp-xmit", SdsAdd(sds, SDS{"err": err}), "")
break
state.payloads <- reply
}
}()
+ if state.rxRate > 0 {
+ time.Sleep(time.Second / time.Duration(state.rxRate))
+ }
}
}()
state.infosTheir[*info.Hash] = &info
state.Unlock()
state.ctx.LogD("sp-process", sdsp, "stating part")
- if _, err = os.Stat(filepath.Join(
+ pktPath := filepath.Join(
state.ctx.Spool,
state.Node.Id.String(),
string(TRx),
ToBase32(info.Hash[:]),
- )); err == nil {
+ )
+ if _, err = os.Stat(pktPath); err == nil {
state.ctx.LogD("sp-process", sdsp, "already done")
replies = append(replies, MarshalSP(SPTypeDone, SPDone{info.Hash}))
continue
}
- fi, err := os.Stat(filepath.Join(
- state.ctx.Spool,
- state.Node.Id.String(),
- string(TRx),
- ToBase32(info.Hash[:])+PartSuffix,
- ))
+ if _, err = os.Stat(pktPath + SeenSuffix); err == nil {
+ state.ctx.LogD("sp-process", sdsp, "already seen")
+ replies = append(replies, MarshalSP(SPTypeDone, SPDone{info.Hash}))
+ continue
+ }
+ fi, err := os.Stat(pktPath + PartSuffix)
var offset int64
if err == nil {
offset = fi.Size()
"hash": ToBase32(freq.Hash[:]),
"offset": strconv.FormatInt(int64(freq.Offset), 10),
}), "queueing")
- state.Lock()
- state.queueTheir = append(state.queueTheir, &freq)
- state.Unlock()
+ nice, exists := state.infosOurSeen[*freq.Hash]
+ if exists {
+ state.Lock()
+ insertIdx := 0
+ var freqWithNice *FreqWithNice
+ for insertIdx, freqWithNice = range state.queueTheir {
+ if freqWithNice.nice > nice {
+ break
+ }
+ }
+ state.queueTheir = append(state.queueTheir, nil)
+ copy(state.queueTheir[insertIdx+1:], state.queueTheir[insertIdx:])
+ state.queueTheir[insertIdx] = &FreqWithNice{&freq, nice}
+ state.Unlock()
+ } else {
+ state.ctx.LogD("sp-process", SdsAdd(sdsp, SDS{
+ "hash": ToBase32(freq.Hash[:]),
+ "offset": strconv.FormatInt(int64(freq.Offset), 10),
+ }), "unknown")
+ }
case SPTypeHalt:
sdsp := SdsAdd(sds, SDS{"type": "halt"})
state.ctx.LogD("sp-process", sdsp, "")
))
}
-func (ctx *Ctx) Toss(nodeId *NodeId, nice uint8, dryRun, doSeen, noFile, noFreq, noExec, noTrns bool) bool {
+func (ctx *Ctx) Toss(
+ nodeId *NodeId,
+ nice uint8,
+ dryRun, doSeen, noFile, noFreq, noExec, noTrns bool) bool {
isBad := false
for job := range ctx.Jobs(nodeId, TRx) {
pktName := filepath.Base(job.Fd.Name())
"golang.org/x/crypto/blake2b"
)
-func (ctx *Ctx) Tx(node *Node, pkt *Pkt, nice uint8, size, minSize int64, src io.Reader) (*Node, error) {
+func (ctx *Ctx) Tx(
+ node *Node,
+ pkt *Pkt,
+ nice uint8,
+ size, minSize int64,
+ src io.Reader) (*Node, error) {
tmp, err := ctx.NewTmpFileWHash()
if err != nil {
return nil, err
return err
}
-func (ctx *Ctx) TxFileChunked(node *Node, nice uint8, srcPath, dstPath string, minSize int64, chunkSize int64) error {
+func (ctx *Ctx) TxFileChunked(
+ node *Node,
+ nice uint8,
+ srcPath, dstPath string,
+ minSize int64,
+ chunkSize int64) error {
if dstPath == "" {
if srcPath == "-" {
return errors.New("Must provide destination filename")
return err
}
-func (ctx *Ctx) TxFreq(node *Node, nice, replyNice uint8, srcPath, dstPath string, minSize int64) error {
+func (ctx *Ctx) TxFreq(
+ node *Node,
+ nice, replyNice uint8,
+ srcPath, dstPath string,
+ minSize int64) error {
dstPath = filepath.Clean(dstPath)
if filepath.IsAbs(dstPath) {
return errors.New("Relative destination path required")
return err
}
-func (ctx *Ctx) TxExec(node *Node, nice, replyNice uint8, handle string, args []string, body []byte, minSize int64) error {
+func (ctx *Ctx) TxExec(
+ node *Node,
+ nice, replyNice uint8,
+ handle string,
+ args []string,
+ body []byte,
+ minSize int64) error {
path := make([][]byte, 0, 1+len(args))
path = append(path, []byte(handle))
for _, arg := range args {
-Subproject commit ab813273cd59e1333f7ae7bff5d027d4aadf528c
+Subproject commit 8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9
-Subproject commit dfa909b99c79129e1100513e5cd36307665e5723
+Subproject commit 1e491301e022f8f977054da4c2d852decd59571f
-Subproject commit c11f84a56e43e20a78cee75a7c034031ecf57d1f
+Subproject commit 9527bec2660bd847c050fda93a0f0c6dee0800bb