"crypto/subtle"
"errors"
"io"
- "net"
"os"
"path/filepath"
"sort"
)
const (
- MaxSPSize = 1<<16 - 256
- PartSuffix = ".part"
- DefaultDeadline = 10
-
+ MaxSPSize = 1<<16 - 256
+ PartSuffix = ".part"
SPHeadOverhead = 4
)
SPFreqOverhead int
SPFileOverhead int
SPHaltMarshalized []byte
+ SPPingMarshalized []byte
NoiseCipherSuite noise.CipherSuite = noise.NewCipherSuite(
noise.DH25519,
noise.HashBLAKE2b,
)
+ DefaultDeadline = 10 * time.Second
+ PingTimeout = time.Minute
+
spWorkersGroup sync.WaitGroup
)
SPTypeFile SPType = iota
SPTypeDone SPType = iota
SPTypeHalt SPType = iota
+ SPTypePing SPType = iota
)
type SPHead struct {
copy(SPHaltMarshalized, buf.Bytes())
buf.Reset()
+ spHead = SPHead{Type: SPTypePing}
+ if _, err := xdr.Marshal(&buf, spHead); err != nil {
+ panic(err)
+ }
+ SPPingMarshalized = make([]byte, SPHeadOverhead)
+ copy(SPPingMarshalized, buf.Bytes())
+ buf.Reset()
+
spInfo := SPInfo{Nice: 123, Size: 123, Hash: new([32]byte)}
if _, err := xdr.Marshal(&buf, spInfo); err != nil {
panic(err)
Ctx *Ctx
Node *Node
Nice uint8
- onlineDeadline uint
+ onlineDeadline time.Duration
maxOnlineTime time.Duration
hs *noise.HandshakeState
csOur *noise.CipherState
csTheir *noise.CipherState
payloads chan []byte
+ pings chan struct{}
infosTheir map[[32]byte]*SPInfo
infosOurSeen map[[32]byte]uint8
queueTheir []*FreqWithNice
wg sync.WaitGroup
RxBytes int64
RxLastSeen time.Time
+ RxLastNonPing time.Time
TxBytes int64
TxLastSeen time.Time
+ TxLastNonPing time.Time
started time.Time
+ mustFinishAt time.Time
Duration time.Duration
RxSpeed int64
TxSpeed int64
state.Lock()
defer state.Unlock()
select {
- case _, ok := <-state.isDead:
- if !ok {
- // Already closed channel, dead
- return
- }
+ case <-state.isDead:
+ // Already closed channel, dead
+ return
default:
}
close(state.isDead)
for _ = range state.payloads {
}
}()
+ go func() {
+ for _ = range state.pings {
+ }
+ }()
}
func (state *SPState) NotAlive() bool {
select {
- case _, ok := <-state.isDead:
- if !ok {
- return true
- }
- default:
- }
- now := time.Now()
- if state.maxOnlineTime > 0 && state.started.Add(state.maxOnlineTime).Before(now) {
+ case <-state.isDead:
return true
+ default:
}
- return uint(now.Sub(state.RxLastSeen).Seconds()) >= state.onlineDeadline &&
- uint(now.Sub(state.TxLastSeen).Seconds()) >= state.onlineDeadline
+ return false
}
func (state *SPState) dirUnlock() {
state.Ctx.UnlockDir(state.txLock)
}
-func (state *SPState) WriteSP(dst io.Writer, payload []byte) error {
+func (state *SPState) WriteSP(dst io.Writer, payload []byte, ping bool) error {
state.writeSPBuf.Reset()
n, err := xdr.Marshal(&state.writeSPBuf, SPRaw{
Magic: MagicNNCPLv1,
if n, err = dst.Write(state.writeSPBuf.Bytes()); err == nil {
state.TxLastSeen = time.Now()
state.TxBytes += int64(n)
+ if !ping {
+ state.TxLastNonPing = state.TxLastSeen
+ }
}
return err
}
}
var rxLock *os.File
if !state.listOnly && (state.xxOnly == "" || state.xxOnly == TRx) {
- rxLock, err = state.Ctx.LockDir(nodeId, TRx)
+ rxLock, err = state.Ctx.LockDir(nodeId, string(TRx))
if err != nil {
return err
}
}
var txLock *os.File
if !state.listOnly && (state.xxOnly == "" || state.xxOnly == TTx) {
- txLock, err = state.Ctx.LockDir(nodeId, TTx)
+ txLock, err = state.Ctx.LockDir(nodeId, string(TTx))
if err != nil {
return err
}
}
state.hs = hs
state.payloads = make(chan []byte)
+ state.pings = make(chan struct{})
state.infosTheir = make(map[[32]byte]*SPInfo)
state.infosOurSeen = make(map[[32]byte]uint8)
state.started = started
}
sds := SDS{"node": nodeId, "nice": int(state.Nice)}
state.Ctx.LogD("sp-start", sds, "sending first message")
- conn.SetWriteDeadline(time.Now().Add(DefaultDeadline * time.Second))
- if err = state.WriteSP(conn, buf); err != nil {
+ conn.SetWriteDeadline(time.Now().Add(DefaultDeadline))
+ if err = state.WriteSP(conn, buf, false); err != nil {
state.Ctx.LogE("sp-start", sds, err, "")
state.dirUnlock()
return err
}
state.Ctx.LogD("sp-start", sds, "waiting for first message")
- conn.SetReadDeadline(time.Now().Add(DefaultDeadline * time.Second))
+ conn.SetReadDeadline(time.Now().Add(DefaultDeadline))
if buf, err = state.ReadSP(conn); err != nil {
state.Ctx.LogE("sp-start", sds, err, "")
state.dirUnlock()
xxOnly := TRxTx("")
state.hs = hs
state.payloads = make(chan []byte)
+ state.pings = make(chan struct{})
state.infosOurSeen = make(map[[32]byte]uint8)
state.infosTheir = make(map[[32]byte]*SPInfo)
state.started = started
var buf []byte
var payload []byte
state.Ctx.LogD("sp-start", SDS{"nice": int(state.Nice)}, "waiting for first message")
- conn.SetReadDeadline(time.Now().Add(DefaultDeadline * time.Second))
+ conn.SetReadDeadline(time.Now().Add(DefaultDeadline))
if buf, err = state.ReadSP(conn); err != nil {
state.Ctx.LogE("sp-start", SDS{}, err, "")
return err
}
var rxLock *os.File
if xxOnly == "" || xxOnly == TRx {
- rxLock, err = state.Ctx.LockDir(node.Id, TRx)
+ rxLock, err = state.Ctx.LockDir(node.Id, string(TRx))
if err != nil {
return err
}
state.rxLock = rxLock
var txLock *os.File
if xxOnly == "" || xxOnly == TTx {
- txLock, err = state.Ctx.LockDir(node.Id, TTx)
+ txLock, err = state.Ctx.LockDir(node.Id, string(TTx))
if err != nil {
return err
}
state.dirUnlock()
return err
}
- conn.SetWriteDeadline(time.Now().Add(DefaultDeadline * time.Second))
- if err = state.WriteSP(conn, buf); err != nil {
+ conn.SetWriteDeadline(time.Now().Add(DefaultDeadline))
+ if err = state.WriteSP(conn, buf, false); err != nil {
state.Ctx.LogE("sp-start", sds, err, "")
state.dirUnlock()
return err
infosPayloads [][]byte,
payload []byte,
) error {
- state.isDead = make(chan struct{})
sds := SDS{"node": state.Node.Id, "nice": int(state.Nice)}
+ state.isDead = make(chan struct{})
+ if state.maxOnlineTime > 0 {
+ state.mustFinishAt = state.started.Add(state.maxOnlineTime)
+ }
+ // Remaining handshake payload sending
if len(infosPayloads) > 1 {
state.wg.Add(1)
go func() {
state.wg.Done()
}()
}
+
+ // Processing of first payload and queueing its responses
state.Ctx.LogD(
"sp-work",
SdsAdd(sds, SDS{"size": len(payload)}),
state.Ctx.LogE("sp-work", sds, err, "")
return err
}
-
state.wg.Add(1)
go func() {
for _, reply := range replies {
state.wg.Done()
}()
+ // Periodic jobs
+ state.wg.Add(1)
+ go func() {
+ deadlineTicker := time.NewTicker(time.Second)
+ pingTicker := time.NewTicker(PingTimeout)
+ for {
+ select {
+ case <-state.isDead:
+ state.wg.Done()
+ deadlineTicker.Stop()
+ pingTicker.Stop()
+ return
+ case now := <-deadlineTicker.C:
+ if (now.Sub(state.RxLastNonPing) >= state.onlineDeadline &&
+ now.Sub(state.TxLastNonPing) >= state.onlineDeadline) ||
+ (state.maxOnlineTime > 0 && state.mustFinishAt.Before(now)) ||
+ (now.Sub(state.RxLastSeen) >= 2*PingTimeout) {
+ state.SetDead()
+ conn.Close()
+ }
+ case now := <-pingTicker.C:
+ if now.After(state.TxLastSeen.Add(PingTimeout)) {
+ state.wg.Add(1)
+ go func() {
+ state.pings <- struct{}{}
+ state.wg.Done()
+ }()
+ }
+ }
+ }
+ }()
+
+ // Spool checker and INFOs sender of appearing files
if !state.listOnly && (state.xxOnly == "" || state.xxOnly == TTx) {
state.wg.Add(1)
go func() {
ticker := time.NewTicker(time.Second)
for {
select {
- case _, ok := <-state.isDead:
- if !ok {
- state.wg.Done()
- ticker.Stop()
- return
- }
+ case <-state.isDead:
+ state.wg.Done()
+ ticker.Stop()
+ return
case <-ticker.C:
for _, payload := range state.Ctx.infosOur(
state.Node.Id,
}()
}
+ // Sender
state.wg.Add(1)
go func() {
+ defer conn.Close()
+ defer state.SetDead()
+ defer state.wg.Done()
for {
if state.NotAlive() {
- break
+ return
}
var payload []byte
+ var ping bool
select {
+ case <-state.pings:
+ state.Ctx.LogD("sp-xmit", sds, "got ping")
+ payload = SPPingMarshalized
+ ping = true
case payload = <-state.payloads:
state.Ctx.LogD(
"sp-xmit",
"got payload",
)
default:
- }
- if payload == nil {
state.RLock()
if len(state.queueTheir) == 0 {
state.RUnlock()
}
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),
"pkt": ToBase32(freq.Hash[:]),
))
if err != nil {
state.Ctx.LogE("sp-file", sdsp, err, "")
- break
+ return
}
fi, err := fd.Stat()
if err != nil {
state.Ctx.LogE("sp-file", sdsp, err, "")
- break
+ return
}
fullSize := fi.Size()
var buf []byte
state.Ctx.LogD("sp-file", sdsp, "seeking")
if _, err = fd.Seek(int64(freq.Offset), io.SeekStart); err != nil {
state.Ctx.LogE("sp-file", sdsp, err, "")
- break
+ return
}
buf = make([]byte, MaxSPSize-SPHeadOverhead-SPFileOverhead)
n, err := fd.Read(buf)
if err != nil {
state.Ctx.LogE("sp-file", sdsp, err, "")
- break
+ return
}
buf = buf[:n]
state.Ctx.LogD("sp-file", SdsAdd(sdsp, SDS{"size": n}), "read")
}
state.Unlock()
}
- state.Ctx.LogD(
- "sp-xmit",
- SdsAdd(sds, SDS{"size": len(payload)}),
- "sending",
- )
- conn.SetWriteDeadline(time.Now().Add(DefaultDeadline * time.Second))
- if err := state.WriteSP(conn, state.csOur.Encrypt(nil, nil, payload)); err != nil {
+ state.Ctx.LogD("sp-xmit", SdsAdd(sds, SDS{"size": len(payload)}), "sending")
+ conn.SetWriteDeadline(time.Now().Add(DefaultDeadline))
+ if err := state.WriteSP(conn, state.csOur.Encrypt(nil, nil, payload), ping); err != nil {
state.Ctx.LogE("sp-xmit", sds, err, "")
- break
+ return
}
}
- state.SetDead()
- state.wg.Done()
}()
+ // Receiver
state.wg.Add(1)
go func() {
for {
break
}
state.Ctx.LogD("sp-recv", sds, "waiting for payload")
- conn.SetReadDeadline(time.Now().Add(DefaultDeadline * time.Second))
+ conn.SetReadDeadline(time.Now().Add(DefaultDeadline))
payload, err := state.ReadSP(conn)
if err != nil {
if err == io.EOF {
break
}
unmarshalErr := err.(*xdr.UnmarshalError)
- netErr, ok := unmarshalErr.Err.(net.Error)
- if ok && netErr.Timeout() {
+ if os.IsTimeout(unmarshalErr.Err) {
continue
}
if unmarshalErr.ErrorCode == xdr.ErrIO {
}
state.SetDead()
state.wg.Done()
+ state.SetDead()
+ conn.Close()
}()
return nil
func (state *SPState) Wait() {
state.wg.Wait()
close(state.payloads)
+ close(state.pings)
state.dirUnlock()
state.Duration = time.Now().Sub(state.started)
state.RxSpeed = state.RxBytes
return nil, err
}
switch head.Type {
+ case SPTypeHalt:
+ state.Ctx.LogD("sp-process", SdsAdd(sds, SDS{"type": "halt"}), "")
+ state.Lock()
+ state.queueTheir = nil
+ state.Unlock()
+ case SPTypePing:
+ state.Ctx.LogD("sp-process", SdsAdd(sds, SDS{"type": "ping"}), "")
case SPTypeInfo:
infosGot = true
sdsp := SdsAdd(sds, SDS{"type": "info"})
} else {
state.Ctx.LogD("sp-process", sdsp, "unknown")
}
- case SPTypeHalt:
- state.Ctx.LogD("sp-process", SdsAdd(sds, SDS{"type": "halt"}), "")
- state.Lock()
- state.queueTheir = nil
- state.Unlock()
default:
state.Ctx.LogE(
"sp-process",
)
return nil, BadPktType
}
+ if head.Type != SPTypePing {
+ state.RxLastNonPing = state.RxLastSeen
+ }
}
if infosGot {
var pkts int