Re: Unexpected EOF in 7.2.0

John Goerzen
2021-07-09T20:24:18Z

[-- Attachment #1: Type: text/plain, Size: 881 bytes --]

I think I have a lead on this.

The packet was generated on a Raspberry Pi (32-bit ARM userland),
is 3GB in size, and is being processed on an x86_64 machine
(64-bit userland).

I added some debug info to pkt.go, and this was printed when -dump
was used:

pktEncRead: Making LimitedReader, size: -1097891139
readBytes: 0, len(buf): 131088, len(toRead): 131088, EnbClkSize:
131072, aead.Overhead: 16, doEncrypt %!d(bool=false)
main.go:163: unexpected EOF

So the presence of that LimitedReader looks to me like what caused
the UnexpectedEof... and if it's being called with a negative
size, that could particularly cause it.

I did try decoding this packet on the Raspberry Pi, but got the
same result.

Attached is the modified pkt.go that produced this output.

Hope this helps! Hopefully whatever the issue is in nncp-pkt also
leads to the issue in nncp-toss.

- John

[-- Attachment #2: pkt.go --]
[-- Type: application/octet-stream, Size: 9030 bytes --]

/*
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"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"io"
"fmt"

xdr "github.com/davecgh/go-xdr/xdr2"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/poly1305"
"lukechampine.com/blake3"
)

type PktType uint8

const (
EncBlkSize = 128 * (1 << 10)

PktTypeFile PktType = iota
PktTypeFreq PktType = iota
PktTypeExec PktType = iota
PktTypeTrns PktType = iota
PktTypeExecFat PktType = iota
PktTypeArea PktType = iota

MaxPathSize = 1<<8 - 1

NNCPBundlePrefix = "NNCP"

PktSizeOverhead = 8 + poly1305.TagSize
)

var (
BadPktType error = errors.New("Unknown packet type")

PktOverhead int64
PktEncOverhead int64
)

type Pkt struct {
Magic [8]byte
Type PktType
Nice uint8
PathLen uint8
Path [MaxPathSize]byte
}

type PktTbs struct {
Magic [8]byte
Nice uint8
Sender *NodeId
Recipient *NodeId
ExchPub [32]byte
}

type PktEnc struct {
Magic [8]byte
Nice uint8
Sender *NodeId
Recipient *NodeId
ExchPub [32]byte
Sign [ed25519.SignatureSize]byte
}

func init() {
pkt := Pkt{Type: PktTypeFile}
var buf bytes.Buffer
n, err := xdr.Marshal(&buf, pkt)
if err != nil {
panic(err)
}
PktOverhead = int64(n)
buf.Reset()

dummyId, err := NodeIdFromString(DummyB32Id)
if err != nil {
panic(err)
}
pktEnc := PktEnc{
Magic: MagicNNCPEv5.B,
Sender: dummyId,
Recipient: dummyId,
}
n, err = xdr.Marshal(&buf, pktEnc)
if err != nil {
panic(err)
}
PktEncOverhead = int64(n)
}

func NewPkt(typ PktType, nice uint8, path []byte) (*Pkt, error) {
if len(path) > MaxPathSize {
return nil, errors.New("Too long path")
}
pkt := Pkt{
Magic: MagicNNCPPv3.B,
Type: typ,
Nice: nice,
PathLen: uint8(len(path)),
}
copy(pkt.Path[:], path)
return &pkt, nil
}

func ctrIncr(b []byte) {
for i := len(b) - 1; i >= 0; i-- {
b[i]++
if b[i] != 0 {
return
}
}
panic("counter overflow")
}

func aeadProcess(
aead cipher.AEAD,
nonce, ad []byte,
doEncrypt bool,
r io.Reader,
w io.Writer,
) (int, error) {
ciphCtr := nonce[len(nonce)-8:]
buf := make([]byte, EncBlkSize+aead.Overhead())
var toRead []byte
var toWrite []byte
var n int
var readBytes int
var err error
if doEncrypt {
toRead = buf[:EncBlkSize]
} else {
toRead = buf
}
for {
fmt.Printf("readBytes: %d, len(buf): %d, len(toRead): %d, EnbClkSize: %d, aead.Overhead: %d, doEncrypt %d\n", readBytes, len(buf), len(toRead), EncBlkSize, aead.Overhead(), doEncrypt)
n, err = io.ReadFull(r, toRead)
if err != nil {
if err == io.EOF {
break
}
if err != io.ErrUnexpectedEOF {
return readBytes + n, err
}
}
readBytes += n
ctrIncr(ciphCtr)
if doEncrypt {
toWrite = aead.Seal(buf[:0], nonce, buf[:n], ad)
} else {
toWrite, err = aead.Open(buf[:0], nonce, buf[:n], ad)
if err != nil {
return readBytes, err
}
}
if _, err = w.Write(toWrite); err != nil {
return readBytes, err
}
}
return readBytes, nil
}

func sizeWithTags(size int64) (fullSize int64) {
fullSize = size + (size/EncBlkSize)*poly1305.TagSize
if size%EncBlkSize != 0 {
fullSize += poly1305.TagSize
}
return
}

func PktEncWrite(
our *NodeOur,
their *Node,
pkt *Pkt,
nice uint8,
size, padSize int64,
data io.Reader,
out io.Writer,
) ([]byte, error) {
pubEph, prvEph, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
var pktBuf bytes.Buffer
if _, err := xdr.Marshal(&pktBuf, pkt); err != nil {
return nil, err
}
tbs := PktTbs{
Magic: MagicNNCPEv5.B,
Nice: nice,
Sender: our.Id,
Recipient: their.Id,
ExchPub: *pubEph,
}
var tbsBuf bytes.Buffer
if _, err = xdr.Marshal(&tbsBuf, &tbs); err != nil {
return nil, err
}
signature := new([ed25519.SignatureSize]byte)
copy(signature[:], ed25519.Sign(our.SignPrv, tbsBuf.Bytes()))
pktEnc := PktEnc{
Magic: MagicNNCPEv5.B,
Nice: nice,
Sender: our.Id,
Recipient: their.Id,
ExchPub: *pubEph,
Sign: *signature,
}
ad := blake3.Sum256(tbsBuf.Bytes())
tbsBuf.Reset()
if _, err = xdr.Marshal(&tbsBuf, &pktEnc); err != nil {
return nil, err
}
pktEncRaw := tbsBuf.Bytes()
if _, err = out.Write(pktEncRaw); err != nil {
return nil, err
}
sharedKey := new([32]byte)
curve25519.ScalarMult(sharedKey, prvEph, their.ExchPub)

key := make([]byte, chacha20poly1305.KeySize)
blake3.DeriveKey(key, string(MagicNNCPEv5.B[:]), sharedKey[:])
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
nonce := make([]byte, aead.NonceSize())

fullSize := pktBuf.Len() + int(size)
sizeBuf := make([]byte, 8+aead.Overhead())
binary.BigEndian.PutUint64(sizeBuf, uint64(sizeWithTags(int64(fullSize))))
if _, err = out.Write(aead.Seal(sizeBuf[:0], nonce, sizeBuf[:8], ad[:])); err != nil {
return nil, err
}

fmt.Printf("pktEncWrite: Making LimitedReader, size: %d\n", size);
lr := io.LimitedReader{R: data, N: size}
mr := io.MultiReader(&pktBuf, &lr)
written, err := aeadProcess(aead, nonce, ad[:], true, mr, out)
if err != nil {
return nil, err
}
if written != fullSize {
return nil, io.ErrUnexpectedEOF
}
if padSize > 0 {
blake3.DeriveKey(key, string(MagicNNCPEv5.B[:])+" PAD", sharedKey[:])
xof := blake3.New(32, key).XOF()
if _, err = io.CopyN(out, xof, padSize); err != nil {
return nil, err
}
}
return pktEncRaw, nil
}

func TbsPrepare(our *NodeOur, their *Node, pktEnc *PktEnc) []byte {
tbs := PktTbs{
Magic: MagicNNCPEv5.B,
Nice: pktEnc.Nice,
Sender: their.Id,
Recipient: our.Id,
ExchPub: pktEnc.ExchPub,
}
var tbsBuf bytes.Buffer
if _, err := xdr.Marshal(&tbsBuf, &tbs); err != nil {
panic(err)
}
return tbsBuf.Bytes()
}

func TbsVerify(our *NodeOur, their *Node, pktEnc *PktEnc) ([]byte, bool, error) {
tbs := TbsPrepare(our, their, pktEnc)
return tbs, ed25519.Verify(their.SignPub, tbs, pktEnc.Sign[:]), nil
}

func PktEncRead(
our *NodeOur,
nodes map[NodeId]*Node,
data io.Reader,
out io.Writer,
signatureVerify bool,
sharedKeyCached []byte,
) ([]byte, *Node, int64, error) {
var pktEnc PktEnc
_, err := xdr.Unmarshal(data, &pktEnc)
if err != nil {
return nil, nil, 0, err
}
switch pktEnc.Magic {
case MagicNNCPEv1.B:
err = MagicNNCPEv1.TooOld()
case MagicNNCPEv2.B:
err = MagicNNCPEv2.TooOld()
case MagicNNCPEv3.B:
err = MagicNNCPEv3.TooOld()
case MagicNNCPEv4.B:
err = MagicNNCPEv4.TooOld()
case MagicNNCPEv5.B:
default:
err = BadMagic
}
if err != nil {
return nil, nil, 0, err
}
if *pktEnc.Recipient != *our.Id {
return nil, nil, 0, errors.New("Invalid recipient")
}
var tbsRaw []byte
var their *Node
if signatureVerify {
their = nodes[*pktEnc.Sender]
if their == nil {
return nil, nil, 0, errors.New("Unknown sender")
}
var verified bool
tbsRaw, verified, err = TbsVerify(our, their, &pktEnc)
if err != nil {
return nil, nil, 0, err
}
if !verified {
return nil, their, 0, errors.New("Invalid signature")
}
} else {
tbsRaw = TbsPrepare(our, &Node{Id: pktEnc.Sender}, &pktEnc)
}
ad := blake3.Sum256(tbsRaw)
sharedKey := new([32]byte)
if sharedKeyCached == nil {
curve25519.ScalarMult(sharedKey, our.ExchPrv, &pktEnc.ExchPub)
} else {
copy(sharedKey[:], sharedKeyCached)
}

key := make([]byte, chacha20poly1305.KeySize)
blake3.DeriveKey(key, string(MagicNNCPEv5.B[:]), sharedKey[:])
aead, err := chacha20poly1305.New(key)
if err != nil {
return sharedKey[:], their, 0, err
}
nonce := make([]byte, aead.NonceSize())

sizeBuf := make([]byte, 8+aead.Overhead())
if _, err = io.ReadFull(data, sizeBuf); err != nil {
return sharedKey[:], their, 0, err
}
sizeBuf, err = aead.Open(sizeBuf[:0], nonce, sizeBuf, ad[:])
if err != nil {
return sharedKey[:], their, 0, err
}
size := int64(binary.BigEndian.Uint64(sizeBuf))

fmt.Printf("pktEncRead: Making LimitedReader, size: %d\n", size);
lr := io.LimitedReader{R: data, N: size}
written, err := aeadProcess(aead, nonce, ad[:], false, &lr, out)
if err != nil {
return sharedKey[:], their, int64(written), err
}
if written != int(size) {
return sharedKey[:], their, int64(written), io.ErrUnexpectedEOF
}
return sharedKey[:], their, size, nil
}

[-- Attachment #3: Type: text/plain, Size: 2179 bytes --]

On Fri, Jul 09 2021, John Goerzen wrote:

> I'm fairly baffled by this. I ran it under strace, and there's
> no syscall
> that's returning EOF. nncp-pkt is easier to analyze under
> strace, and it does a
> 4096-byte read, gets a 4096-byte result, then crashes with
> unexpected EOF.
>
> I don't know Go, though... I'm also baffled at how the test at
> pkt.go line 163
> could cause an error.
>
> - John
>
> On Fri, Jul 09 2021, John Goerzen wrote:
>
>> Hello,
>>
>> I have a 3GB packet that was transferred from its origin to my
>> gateway machine
>> via nncp-call. When called on it, nncp-check works fine and
>> verifies its
>> integrity. However:
>>
>> nncp@nncp:~$ nncp-toss -progress
>> 2021-07-09T15:10:27Z ERROR Tossing
>> mccoy/74HUFHYZOB7J7MLU7SHKTRODN7OH7HRLSSTKWEEDKNMXLOJBDI2Q:
>> unmarshal: xdr:DecodeFixedOpaque: unexpected EOF while decoding
>> 8 bytes -
>> read:
>> '[]'
>>
>> And the log shows:
>>
>> When: 2021-07-09T15:12:13.896311727Z
>> Err: xdr:DecodeFixedOpaque: unexpected EOF while decoding 8
>> bytes - read: '[]'
>> Who: rx-unmarshal
>> Node: NE2CD32WK62N3QWWQLJUZXVX2WZAIMM7DBIXQVJTFDSXHAWSULBQ
>> Pkt: 74HUFHYZOB7J7MLU7SHKTRODN7OH7HRLSSTKWEEDKNMXLOJBDI2Q
>> Nice: 226
>> Msg: Tossing
>> mccoy/74HUFHYZOB7J7MLU7SHKTRODN7OH7HRLSSTKWEEDKNMXLOJBDI2Q:
>> unmarshal
>>
>> (this is a transitional packet)
>>
>> Also:
>>
>> nncp@nncp:~$ nncp-pkt -dump <
>> /var/spool/nncp/NE2CD32WK62N3QWWQLJUZXVX2WZAIMM7DBIXQVJTFDSXHAWSULBQ/rx/74HUFHYZOB7J7MLU7SHKTRODN7OH7HRLSSTKWEEDKNMXLOJBDI2Q
>> main.go:163: unexpected EOF
>>
>> These commands that return with "unexpected EOF" are returning
>> almost
>> immediately. They could not possibly have processed 3GB like
>> nncp-check does.
>> If I omit -dump from nncp-pkt, it works fine. I tried
>> reblocking with dd, and
>> that didn't help either.
>>
>> This occurs both with go 1.14 and go 1.15.
>>
>> The only unique thing here is that it's from a new node on my
>> network.
>>
>> Other packets from that node were processed successfully,
>> though they were
>> much
>> smaller. I tried removing the .hdr file and that didn't help
>> either (it was
>> recreated).
>>
>> Thanks!
>>
>> - John