]> Cypherpunks.ru repositories - govpn.git/blobdiff - peer.go
Preparing move to modules
[govpn.git] / peer.go
diff --git a/peer.go b/peer.go
new file mode 100644 (file)
index 0000000..1c6a527
--- /dev/null
+++ b/peer.go
@@ -0,0 +1,454 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2019 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 govpn
+
+import (
+       "bytes"
+       "crypto/subtle"
+       "encoding/binary"
+       "io"
+       "log"
+       "sync"
+       "sync/atomic"
+       "time"
+
+       "go.cypherpunks.ru/govpn/v7/internal/chacha20"
+
+       "golang.org/x/crypto/blake2b"
+       "golang.org/x/crypto/poly1305"
+)
+
+const (
+       NonceSize       = 8
+       NonceBucketSize = 256
+       TagSize         = poly1305.TagSize
+       // S20BS is ChaCha20's internal blocksize in bytes
+       S20BS = 64
+       // Maximal amount of bytes transfered with single key (4 GiB)
+       MaxBytesPerKey uint64 = 1 << 32
+       // Heartbeat rate, relative to Timeout
+       TimeoutHeartbeat = 4
+       // Minimal valid packet length
+       MinPktLength = 1 + 16 + 8
+       // Padding byte
+       PadByte = byte(0x80)
+)
+
+func newNonces(key *[32]byte, i uint64) chan *[NonceSize]byte {
+       macKey := make([]byte, 32)
+       chacha20.XORKeyStream(macKey, make([]byte, 32), new([16]byte), key)
+       mac, err := blake2b.New256(macKey)
+       if err != nil {
+               panic(err)
+       }
+       sum := make([]byte, mac.Size())
+       nonces := make(chan *[NonceSize]byte, NonceBucketSize*3)
+       go func() {
+               for {
+                       buf := new([NonceSize]byte)
+                       binary.BigEndian.PutUint64(buf[:], i)
+                       mac.Write(buf[:])
+                       mac.Sum(sum[:0])
+                       copy(buf[:], sum)
+                       nonces <- buf
+                       mac.Reset()
+                       i += 2
+               }
+       }()
+       return nonces
+}
+
+type Peer struct {
+       // Statistics (they are at the beginning for correct int64 alignment)
+       BytesIn         uint64
+       BytesOut        uint64
+       BytesPayloadIn  uint64
+       BytesPayloadOut uint64
+       FramesIn        uint64
+       FramesOut       uint64
+       FramesUnauth    uint64
+       FramesDup       uint64
+       HeartbeatRecv   uint64
+       HeartbeatSent   uint64
+
+       // Basic
+       Addr string
+       ID   *PeerID
+       Conn io.Writer `json:"-"`
+
+       // Traffic behaviour
+       NoiseEnable bool
+       CPR         int
+       CPRCycle    time.Duration `json:"-"`
+       Encless     bool
+       MTU         int
+
+       key *[SSize]byte
+
+       // Timers
+       Timeout     time.Duration `json:"-"`
+       Established time.Time
+       LastPing    time.Time
+
+       // Receiver
+       BusyR    sync.Mutex `json:"-"`
+       bufR     []byte
+       tagR     *[TagSize]byte
+       keyAuthR *[SSize]byte
+       nonceR   *[16]byte
+       pktSizeR int
+
+       // UDP-related
+       noncesR      chan *[NonceSize]byte
+       nonceRecv    [NonceSize]byte
+       nonceBucketL map[[NonceSize]byte]struct{}
+       nonceBucketM map[[NonceSize]byte]struct{}
+       nonceBucketH map[[NonceSize]byte]struct{}
+
+       // TCP-related
+       NonceExpect  []byte `json:"-"`
+       noncesExpect chan *[NonceSize]byte
+
+       // Transmitter
+       BusyT    sync.Mutex `json:"-"`
+       bufT     []byte
+       tagT     *[TagSize]byte
+       keyAuthT *[SSize]byte
+       nonceT   *[16]byte
+       frameT   []byte
+       noncesT  chan *[NonceSize]byte
+}
+
+func (p *Peer) String() string {
+       return p.ID.String() + ":" + p.Addr
+}
+
+// Zero peer's memory state.
+func (p *Peer) Zero() {
+       p.BusyT.Lock()
+       p.BusyR.Lock()
+       SliceZero(p.key[:])
+       SliceZero(p.bufR)
+       SliceZero(p.bufT)
+       SliceZero(p.keyAuthR[:])
+       SliceZero(p.keyAuthT[:])
+       p.BusyT.Unlock()
+       p.BusyR.Unlock()
+}
+
+func cprCycleCalculate(conf *PeerConf) time.Duration {
+       if conf.CPR == 0 {
+               return time.Duration(0)
+       }
+       rate := conf.CPR * 1 << 10
+       if conf.Encless {
+               rate /= EnclessEnlargeSize + conf.MTU
+       } else {
+               rate /= conf.MTU
+       }
+       return time.Second / time.Duration(rate)
+}
+
+func newPeer(isClient bool, addr string, conn io.Writer, conf *PeerConf, key *[SSize]byte) *Peer {
+       now := time.Now()
+       timeout := conf.Timeout
+
+       cprCycle := cprCycleCalculate(conf)
+       noiseEnable := conf.Noise
+       if conf.CPR > 0 {
+               noiseEnable = true
+               timeout = cprCycle
+       } else {
+               timeout = timeout / TimeoutHeartbeat
+       }
+
+       bufSize := S20BS + 2*conf.MTU
+       if conf.Encless {
+               bufSize += EnclessEnlargeSize
+               noiseEnable = true
+       }
+
+       peer := Peer{
+               Addr: addr,
+               ID:   conf.ID,
+               Conn: conn,
+
+               NoiseEnable: noiseEnable,
+               CPR:         conf.CPR,
+               CPRCycle:    cprCycle,
+               Encless:     conf.Encless,
+               MTU:         conf.MTU,
+
+               key: key,
+
+               Timeout:     timeout,
+               Established: now,
+               LastPing:    now,
+
+               bufR:     make([]byte, bufSize),
+               bufT:     make([]byte, bufSize),
+               tagR:     new([TagSize]byte),
+               tagT:     new([TagSize]byte),
+               keyAuthR: new([SSize]byte),
+               nonceR:   new([16]byte),
+               keyAuthT: new([SSize]byte),
+               nonceT:   new([16]byte),
+       }
+
+       if isClient {
+               peer.noncesT = newNonces(peer.key, 1+2)
+               peer.noncesR = newNonces(peer.key, 0+2)
+               peer.noncesExpect = newNonces(peer.key, 0+2)
+       } else {
+               peer.noncesT = newNonces(peer.key, 0+2)
+               peer.noncesR = newNonces(peer.key, 1+2)
+               peer.noncesExpect = newNonces(peer.key, 1+2)
+       }
+
+       peer.NonceExpect = make([]byte, NonceSize)
+       nonce := <-peer.noncesExpect
+       copy(peer.NonceExpect, nonce[:])
+
+       var i int
+       peer.nonceBucketL = make(map[[NonceSize]byte]struct{}, NonceBucketSize)
+       for i = 0; i < NonceBucketSize; i++ {
+               nonce = <-peer.noncesR
+               peer.nonceBucketL[*nonce] = struct{}{}
+       }
+       peer.nonceBucketM = make(map[[NonceSize]byte]struct{}, NonceBucketSize)
+       for i = 0; i < NonceBucketSize; i++ {
+               nonce = <-peer.noncesR
+               peer.nonceBucketM[*nonce] = struct{}{}
+       }
+       peer.nonceBucketH = make(map[[NonceSize]byte]struct{}, NonceBucketSize)
+       for i = 0; i < NonceBucketSize; i++ {
+               nonce = <-peer.noncesR
+               peer.nonceBucketH[*nonce] = struct{}{}
+       }
+
+       return &peer
+}
+
+// Process incoming Ethernet packet.
+// ready channel is TAPListen's synchronization channel used to tell him
+// that he is free to receive new packets. Encrypted and authenticated
+// packets will be sent to remote Peer side immediately.
+func (p *Peer) EthProcess(data []byte) {
+       if len(data) > p.MTU-1 { // 1 is for padding byte
+               log.Println("Padded data packet size", len(data)+1, "is bigger than MTU", p.MTU, p)
+               return
+       }
+       p.BusyT.Lock()
+
+       // Zero size is a heartbeat packet
+       SliceZero(p.bufT)
+       if len(data) == 0 {
+               p.bufT[S20BS+0] = PadByte
+               p.HeartbeatSent++
+       } else {
+               // Copy payload to our internal buffer and we are ready to
+               // accept the next one
+               copy(p.bufT[S20BS:], data)
+               p.bufT[S20BS+len(data)] = PadByte
+               p.BytesPayloadOut += uint64(len(data))
+       }
+
+       if p.NoiseEnable && !p.Encless {
+               p.frameT = p.bufT[S20BS : S20BS+p.MTU-TagSize]
+       } else if p.Encless {
+               p.frameT = p.bufT[S20BS : S20BS+p.MTU]
+       } else {
+               p.frameT = p.bufT[S20BS : S20BS+len(data)+1+NonceSize]
+       }
+       copy(p.frameT[len(p.frameT)-NonceSize:], (<-p.noncesT)[:])
+       var out []byte
+       copy(p.nonceT[8:], p.frameT[len(p.frameT)-NonceSize:])
+       if p.Encless {
+               var err error
+               out, err = EnclessEncode(p.key, p.nonceT, p.frameT[:len(p.frameT)-NonceSize])
+               if err != nil {
+                       panic(err)
+               }
+               out = append(out, p.frameT[len(p.frameT)-NonceSize:]...)
+       } else {
+               chacha20.XORKeyStream(
+                       p.bufT[:S20BS+len(p.frameT)-NonceSize],
+                       p.bufT[:S20BS+len(p.frameT)-NonceSize],
+                       p.nonceT,
+                       p.key,
+               )
+               copy(p.keyAuthT[:], p.bufT[:SSize])
+               poly1305.Sum(p.tagT, p.frameT, p.keyAuthT)
+               atomic.AddUint64(&p.BytesOut, uint64(len(p.frameT)+TagSize))
+               out = append(p.tagT[:], p.frameT...)
+       }
+       p.FramesOut++
+       p.Conn.Write(out)
+       p.BusyT.Unlock()
+}
+
+func (p *Peer) PktProcess(data []byte, tap io.Writer, reorderable bool) bool {
+       if len(data) < MinPktLength {
+               return false
+       }
+       if !p.Encless && len(data) > len(p.bufR)-S20BS {
+               return false
+       }
+       var out []byte
+       p.BusyR.Lock()
+       copy(p.nonceR[8:], data[len(data)-NonceSize:])
+       if p.Encless {
+               var err error
+               out, err = EnclessDecode(p.key, p.nonceR, data[:len(data)-NonceSize])
+               if err != nil {
+                       p.FramesUnauth++
+                       p.BusyR.Unlock()
+                       return false
+               }
+       } else {
+               for i := 0; i < SSize; i++ {
+                       p.bufR[i] = 0
+               }
+               copy(p.bufR[S20BS:], data[TagSize:])
+               chacha20.XORKeyStream(
+                       p.bufR[:S20BS+len(data)-TagSize-NonceSize],
+                       p.bufR[:S20BS+len(data)-TagSize-NonceSize],
+                       p.nonceR,
+                       p.key,
+               )
+               copy(p.keyAuthR[:], p.bufR[:SSize])
+               copy(p.tagR[:], data[:TagSize])
+               if !poly1305.Verify(p.tagR, data[TagSize:], p.keyAuthR) {
+                       p.FramesUnauth++
+                       p.BusyR.Unlock()
+                       return false
+               }
+               out = p.bufR[S20BS : S20BS+len(data)-TagSize-NonceSize]
+       }
+
+       if reorderable {
+               copy(p.nonceRecv[:], data[len(data)-NonceSize:])
+               _, foundL := p.nonceBucketL[p.nonceRecv]
+               _, foundM := p.nonceBucketM[p.nonceRecv]
+               _, foundH := p.nonceBucketH[p.nonceRecv]
+               // If found is none of buckets: either it is too old,
+               // or too new (many packets were lost)
+               if !(foundL || foundM || foundH) {
+                       p.FramesDup++
+                       p.BusyR.Unlock()
+                       return false
+               }
+               // Delete seen nonce
+               if foundL {
+                       delete(p.nonceBucketL, p.nonceRecv)
+               }
+               if foundM {
+                       delete(p.nonceBucketM, p.nonceRecv)
+               }
+               if foundH {
+                       delete(p.nonceBucketH, p.nonceRecv)
+               }
+               // If we are dealing with the latest bucket, create the new one
+               if foundH {
+                       p.nonceBucketL, p.nonceBucketM = p.nonceBucketM, p.nonceBucketH
+                       p.nonceBucketH = make(map[[NonceSize]byte]struct{})
+                       var nonce *[NonceSize]byte
+                       for i := 0; i < NonceBucketSize; i++ {
+                               nonce = <-p.noncesR
+                               p.nonceBucketH[*nonce] = struct{}{}
+                       }
+               }
+       } else {
+               if subtle.ConstantTimeCompare(data[len(data)-NonceSize:], p.NonceExpect) != 1 {
+                       p.FramesDup++
+                       p.BusyR.Unlock()
+                       return false
+               }
+               copy(p.NonceExpect, (<-p.noncesExpect)[:])
+       }
+
+       p.FramesIn++
+       atomic.AddUint64(&p.BytesIn, uint64(len(data)))
+       p.LastPing = time.Now()
+       p.pktSizeR = bytes.LastIndexByte(out, PadByte)
+       if p.pktSizeR == -1 {
+               p.BusyR.Unlock()
+               return false
+       }
+       // Validate the pad
+       for i := p.pktSizeR + 1; i < len(out); i++ {
+               if out[i] != 0 {
+                       p.BusyR.Unlock()
+                       return false
+               }
+       }
+
+       if p.pktSizeR == 0 {
+               p.HeartbeatRecv++
+               p.BusyR.Unlock()
+               return true
+       }
+       p.BytesPayloadIn += uint64(p.pktSizeR)
+       tap.Write(out[:p.pktSizeR])
+       p.BusyR.Unlock()
+       return true
+}
+
+func PeerTapProcessor(peer *Peer, tap *TAP, terminator chan struct{}) {
+       var data []byte
+       var now time.Time
+       lastSent := time.Now()
+       heartbeat := time.NewTicker(peer.Timeout)
+       if peer.CPRCycle == time.Duration(0) {
+       RawProcessor:
+               for {
+                       select {
+                       case <-terminator:
+                               break RawProcessor
+                       case <-heartbeat.C:
+                               now = time.Now()
+                               if lastSent.Add(peer.Timeout).Before(now) {
+                                       peer.EthProcess(nil)
+                                       lastSent = now
+                               }
+                       case data = <-tap.Sink:
+                               peer.EthProcess(data)
+                               lastSent = time.Now()
+                       }
+               }
+       } else {
+       CPRProcessor:
+               for {
+                       data = nil
+                       select {
+                       case <-terminator:
+                               break CPRProcessor
+                       case data = <-tap.Sink:
+                               peer.EthProcess(data)
+                       default:
+                       }
+                       if data == nil {
+                               peer.EthProcess(nil)
+                       }
+                       time.Sleep(peer.CPRCycle)
+               }
+       }
+       close(terminator)
+       peer.Zero()
+       heartbeat.Stop()
+}