From: Sergey Matveev Date: Sun, 13 Sep 2015 16:39:55 +0000 (+0300) Subject: Huge code refactoring X-Git-Tag: 4.0^2~22 X-Git-Url: http://www.git.cypherpunks.ru/?p=govpn.git;a=commitdiff_plain;h=87f01e08c6135b3e2b092903d56d7c49b3f126a5 Huge code refactoring * Code simplification. Lesser synchronization channels * Processing parallelization of clients on the server side * Double buffers for network/Ethernet packets prefetching * Removed length prefix in TCP messages. Now they are indistinguishable from random * Constant time nonce uniqueness checking * Increased performance Signed-off-by: Sergey Matveev --- diff --git a/doc/developer.texi b/doc/developer.texi index 81360b2..6035225 100644 --- a/doc/developer.texi +++ b/doc/developer.texi @@ -19,10 +19,10 @@ and @url{http://ed25519.cr.yp.to/, Ed25519}. @url{https://en.wikipedia.org/wiki/PBKDF2, PBKDF2} based on @url{https://en.wikipedia.org/wiki/SHA-2, SHA-512}. @item Packet overhead -26 bytes per packet. Two more bytes in TCP mode. +26 bytes per packet. @item Handshake overhead 4 UDP (2 from client, 2 from server) packets (round-trips for TCP), -264 bytes total payload (8 bytes more in TCP mode). +264 bytes total payload. @item Entropy required 832 bits in average on client, 832 bits in average on server side per handshake. diff --git a/doc/netproto.texi b/doc/netproto.texi index 3aef447..d033656 100644 --- a/doc/netproto.texi +++ b/doc/netproto.texi @@ -3,8 +3,7 @@ You can use either UDP or TCP underlying network transport protocols. -TCP consumes more traffic: two additional bytes per packet. Also it is -has more complex and slightly slower code. Moreover because of packet -loss and TCP reliability it can lead to "meltdown" effect: significant -performance decrease of underlying TCP connections. So generally TCP is -not advisable for VPNs, but it can help with some nasty firewalls. +TCP is more resource hungry. Moreover because of packet loss and TCP +reliability it can lead to "meltdown" effect: significant performance +loss of underlying TCP connections. Generally TCP is not advisable for +VPNs, but it can help with some nasty firewalls. diff --git a/doc/todo.texi b/doc/todo.texi index f8064cf..1372c20 100644 --- a/doc/todo.texi +++ b/doc/todo.texi @@ -5,6 +5,5 @@ @item Ability to tunnel only specified TCP connections, without full featured VPN solution. Similar to ssh -R. @item Ability to noise handshake packets sizes. -@item Increase performance. @item Randomize ports usage. @end itemize diff --git a/doc/transport.texi b/doc/transport.texi index 776c8c8..97fdce8 100644 --- a/doc/transport.texi +++ b/doc/transport.texi @@ -2,45 +2,60 @@ @section Transport protocol @verbatim -[PktLen] + ENC(KEY, ENCn(SERIAL), DATA_SIZE+DATA+NOISE) + ENCn(SERIAL) + - AUTH(ENC(KEY, ENCn(SERIAL), DATA_SIZE+DATA+NOISE) + ENCn(SERIAL)) +TAG || ENCRYPTED || NONCE <-- PACKET + ^ ^ ^ + | | | + | | +------------+ + | | | + | +------------+ | + | | | + +-->AUTH(AUTH_KEY, ENCRYPTED || NONCE) + ^ ^ + | | ++-----------------------+ | +| | +| +--------------+ +| | ++--> ENCRYPT(KEY, NONCE, PAYLOAD) + ^ ^ + | | + | +--> SIZE || DATA [|| NOISE] + | + +--> PRP(PRP_KEY, SERIAL) @end verbatim -All transport and handshake messages are indistinguishable from -pseudo random noise, except when using TCP connections. - -@code{PktLen} is used only with TCP connections. It is big-endian -@emph{uint16} length of the whole packet (except @code{PktLen} itself). - @code{SERIAL} is message's serial number. Odds are reserved for client(->server) messages, evens for server(->client) messages. -@code{ENCn} is XTEA block cipher algorithm used here as PRP (pseudo +@code{PRP} is XTEA block cipher algorithm used here as PRP (pseudo random permutation function) to obfuscate @code{SERIAL}. Plaintext @code{SERIAL} state is kept in peers internal state, but encrypted -before transmission. XTEA is compact and fast enough. Salsa20 is PRF -function and requires much more code to create PRP from it. +before transmission. XTEA's encryption key is the first 128-bit of Salsa20's output with established common key and zero nonce (message nonces start from 1). -@code{ENC} is Salsa20 stream cipher, with established session @code{KEY} -and obfuscated @code{SERIAL} used as a nonce. First 256 bits of -Salsa20's output is used as Poly1305 authentication key, next 256 bits -are ignored. All remaining output is XORed with the data, encrypting it. +@verbatim +PRP_KEY = ENCRYPT(KEY, 0, 128-bit) +@end verbatim + +@code{ENCRYPT} is Salsa20 stream cipher, with established session +@code{KEY} and obfuscated @code{SERIAL} used as a nonce. 512 bit of +Salsa20's output is ignored and only remaining is XORed with ther data, +encrypting it. -@code{DATA_SIZE} is big-endian @emph{uint16} storing length of the +@code{SIZE} is big-endian @emph{uint16} storing length of the @code{DATA}. @code{NOISE} is optional. It is just some junk data, intended to fill up packet to MTU size. This is useful for concealing payload packets length. @code{AUTH} is Poly1305 authentication function. First 256 bits of -Salsa20 output are used as a one-time key for @code{AUTH}. +Salsa20's output are used as a one-time key for @code{AUTH}. + +@verbatim +AUTH_KEY = ENCRYPT(KEY, NONCE, 256 bit) +@end verbatim To prevent replay attacks we must remember received @code{SERIAL}s and -if meet one, then drop it. Basically we could just store latest number -and check if received one is greater, but because of UDP packets -reordering this can lead to valid packets dropping and overall -performance degradation. We store up to 256 seen nonces in hash -structure, in two swapping buckets. +drop when receiving duplicate ones. diff --git a/doc/user.texi b/doc/user.texi index 9a5a025..6eae9a3 100644 --- a/doc/user.texi +++ b/doc/user.texi @@ -11,7 +11,7 @@ automate it using up and down shell scripts. What network performance can user expect? For example single @emph{Intel i5-2450M 2.5 GHz} core on @emph{FreeBSD 10.2 amd64} -with @emph{Go 1.5} gives 435 Mbps TCP (over UDP) throughput. +with @emph{Go 1.5.1} gives 786 Mbps (UDP transport) throughput. @menu * EGD:: Entropy gathering daemon diff --git a/src/govpn/cmd/govpn-client/main.go b/src/govpn/cmd/govpn-client/main.go index 6d312ce..d88bb46 100644 --- a/src/govpn/cmd/govpn-client/main.go +++ b/src/govpn/cmd/govpn-client/main.go @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -// Simple secure free software virtual private network daemon client. +// Simple secure, DPI/censorship-resistant free software VPN daemon client. package main import ( @@ -46,11 +46,17 @@ var ( noisy = flag.Bool("noise", false, "Enable noise appending") cpr = flag.Int("cpr", 0, "Enable constant KiB/sec out traffic rate") egdPath = flag.String("egd", "", "Optional path to EGD socket") + + conf *govpn.PeerConf + tap *govpn.TAP + timeout int + firstUpCall bool = true + knownPeers govpn.KnownPeers ) func main() { flag.Parse() - timeout := *timeoutP + timeout = *timeoutP var err error log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) @@ -67,7 +73,7 @@ func main() { } pub, priv := govpn.NewVerifier(id, govpn.StringFromFile(*keyPath)) - conf := &govpn.PeerConf{ + conf = &govpn.PeerConf{ Id: id, Timeout: time.Second * time.Duration(timeout), NoiseEnable: *noisy, @@ -76,41 +82,13 @@ func main() { DSAPriv: priv, } govpn.PeersInitDummy(id, conf) + log.Println(govpn.VersionGet()) - var conn govpn.RemoteConn - var sink chan []byte - var ready chan struct{} - switch *proto { - case "udp": - conn, sink, ready = startUDP() - case "tcp": - if *proxyAddr != "" { - conn, sink, ready = proxyTCP() - } else { - conn, sink, ready = startTCP() - } - default: - log.Fatalln("Unknown protocol specified") - } - - tap, ethSink, ethReady, _, err := govpn.TAPListen( - *ifaceName, - time.Second*time.Duration(timeout), - *cpr, - ) + tap, err = govpn.TAPListen(*ifaceName) if err != nil { log.Fatalln("Can not listen on TAP interface:", err) } - timeouts := 0 - firstUpCall := true - var peer *govpn.Peer - var ethPkt []byte - var pkt []byte - knownPeers := govpn.KnownPeers(map[string]**govpn.Peer{*remoteAddr: &peer}) - - log.Println(govpn.VersionGet()) - log.Println("Connected to", *proto, *remoteAddr) log.Println("Max MTU on TAP interface:", govpn.TAPMaxMTU()) if *stats != "" { log.Println("Stats are going to listen on", *stats) @@ -124,65 +102,35 @@ func main() { termSignal := make(chan os.Signal, 1) signal.Notify(termSignal, os.Interrupt, os.Kill) - log.Println("Starting handshake") - handshake := govpn.HandshakeStart(*remoteAddr, conn, conf) - MainCycle: for { - if peer != nil && (peer.BytesIn+peer.BytesOut) > govpn.MaxBytesPerKey { - peer.Zero() - peer = nil - handshake = govpn.HandshakeStart(*remoteAddr, conn, conf) - log.Println("Rehandshaking") + timeouted := make(chan struct{}) + rehandshaking := make(chan struct{}) + termination := make(chan struct{}) + switch *proto { + case "udp": + go startUDP(timeouted, rehandshaking, termination) + case "tcp": + if *proxyAddr != "" { + go proxyTCP(timeouted, rehandshaking, termination) + } else { + go startTCP(timeouted, rehandshaking, termination) + } + default: + log.Fatalln("Unknown protocol specified") } select { case <-termSignal: + log.Fatalln("Finishing...") + termination <- struct{}{} break MainCycle - case ethPkt = <-ethSink: - if peer == nil { - if len(ethPkt) > 0 { - ethReady <- struct{}{} - } - continue - } - peer.EthProcess(ethPkt, ethReady) - case pkt = <-sink: - timeouts++ - if timeouts >= timeout { - break MainCycle - } - if pkt == nil { - ready <- struct{}{} - continue - } - - if peer == nil { - if govpn.IDsCache.Find(pkt) == nil { - log.Println("Invalid identity in handshake packet") - ready <- struct{}{} - continue - } - if p := handshake.Client(pkt); p != nil { - log.Println("Handshake completed") - if firstUpCall { - go govpn.ScriptCall(*upPath, *ifaceName) - firstUpCall = false - } - peer = p - handshake.Zero() - handshake = nil - } - ready <- struct{}{} - continue - } - if peer == nil { - ready <- struct{}{} - continue - } - if peer.PktProcess(pkt, tap, ready) { - timeouts = 0 - } + case <-timeouted: + break MainCycle + case <-rehandshaking: } + close(timeouted) + close(rehandshaking) + close(termination) } govpn.ScriptCall(*downPath, *ifaceName) } diff --git a/src/govpn/cmd/govpn-client/proxy.go b/src/govpn/cmd/govpn-client/proxy.go index 98c9098..a0a9a54 100644 --- a/src/govpn/cmd/govpn-client/proxy.go +++ b/src/govpn/cmd/govpn-client/proxy.go @@ -24,11 +24,9 @@ import ( "log" "net" "net/http" - - "govpn" ) -func proxyTCP() (govpn.RemoteConn, chan []byte, chan struct{}) { +func proxyTCP(timeouted, rehandshaking, termination chan struct{}) { proxyAddr, err := net.ResolveTCPAddr("tcp", *proxyAddr) if err != nil { log.Fatalln("Can not resolve proxy address:", err) @@ -52,9 +50,5 @@ func proxyTCP() (govpn.RemoteConn, chan []byte, chan struct{}) { if err != nil || resp.StatusCode != http.StatusOK { log.Fatalln("Unexpected response from proxy") } - sink := make(chan []byte) - ready := make(chan struct{}) - go handleTCP(conn, sink, ready) - go func() { ready <- struct{}{} }() - return TCPSender{conn}, sink, ready + go handleTCP(conn, timeouted, rehandshaking, termination) } diff --git a/src/govpn/cmd/govpn-client/tcp.go b/src/govpn/cmd/govpn-client/tcp.go index dcf7b14..49d3ae9 100644 --- a/src/govpn/cmd/govpn-client/tcp.go +++ b/src/govpn/cmd/govpn-client/tcp.go @@ -19,84 +19,144 @@ along with this program. If not, see . package main import ( + "bytes" "encoding/binary" "log" "net" + "sync/atomic" + "time" "govpn" ) -// TCPSender prepends size prefix to each outgoing packet. -type TCPSender struct { - conn *net.TCPConn -} - -func (c TCPSender) Write(data []byte) (int, error) { - size := make([]byte, 2) - binary.BigEndian.PutUint16(size, uint16(len(data))) - return c.conn.Write(append(size, data...)) -} - -func (c TCPSender) Reorderable() bool { - return false -} - -func startTCP() (govpn.RemoteConn, chan []byte, chan struct{}) { +func startTCP(timeouted, rehandshaking, termination chan struct{}) { remote, err := net.ResolveTCPAddr("tcp", *remoteAddr) if err != nil { log.Fatalln("Can not resolve remote address:", err) } - c, err := net.DialTCP("tcp", nil, remote) - conn := TCPSender{c} + conn, err := net.DialTCP("tcp", nil, remote) if err != nil { - log.Fatalln("Can not connect TCP:", err) + log.Fatalln("Can not connect to remote address:", err) } - sink := make(chan []byte) - ready := make(chan struct{}) - go handleTCP(c, sink, ready) - go func() { ready <- struct{}{} }() - return conn, sink, ready + handleTCP(conn, timeouted, rehandshaking, termination) } -func handleTCP(conn *net.TCPConn, sink chan []byte, ready chan struct{}) { - var err error - var n int - var sizeNbuf int - sizeBuf := make([]byte, 2) - var sizeNeed uint16 - var bufN uint16 +func handleTCP(conn *net.TCPConn, timeouted, rehandshaking, termination chan struct{}) { + hs := govpn.HandshakeStart(*remoteAddr, conn, conf) buf := make([]byte, govpn.MTU) + var n int + var err error + var prev int + var peer *govpn.Peer + var terminator chan struct{} +HandshakeCycle: for { - <-ready - if sizeNbuf != 2 { - n, err = conn.Read(sizeBuf[sizeNbuf:2]) - if err != nil { - break - } - sizeNbuf += n - if sizeNbuf != 2 { - sink <- nil - continue - } - sizeNeed = binary.BigEndian.Uint16(sizeBuf) - if int(sizeNeed) > govpn.MTU-2 { - log.Println("Invalid TCP size, skipping") - sizeNbuf = 0 - sink <- nil - continue - } - bufN = 0 + select { + case <-termination: + break HandshakeCycle + default: + } + if prev == govpn.MTU { + break HandshakeCycle + } + + conn.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) + n, err = conn.Read(buf[prev:]) + if err != nil { + // Either EOFed or timeouted + break HandshakeCycle } - ReadMore: - if sizeNeed != bufN { - n, err = conn.Read(buf[bufN:sizeNeed]) - if err != nil { - break + + prev += n + peerId := govpn.IDsCache.Find(buf[:prev]) + if peerId == nil { + continue + } + peer = hs.Client(buf[:prev]) + prev = 0 + if peer == nil { + continue + } + log.Println("Handshake completed") + knownPeers = govpn.KnownPeers(map[string]**govpn.Peer{*remoteAddr: &peer}) + if firstUpCall { + go govpn.ScriptCall(*upPath, *ifaceName) + firstUpCall = false + } + hs.Zero() + terminator = make(chan struct{}) + go func() { + heartbeat := time.NewTicker(peer.Timeout) + var data []byte + Processor: + for { + select { + case <-heartbeat.C: + peer.EthProcess(nil) + case <-terminator: + break Processor + case data = <-tap.Sink: + peer.EthProcess(data) + } } - bufN += uint16(n) - goto ReadMore + heartbeat.Stop() + peer.Zero() + }() + break HandshakeCycle + } + if hs != nil { + hs.Zero() + } + if peer == nil { + return + } + + nonceExpectation := make([]byte, govpn.NonceSize) + binary.BigEndian.PutUint64(nonceExpectation, peer.NonceExpect) + peer.NonceCipher.Encrypt(nonceExpectation, nonceExpectation) + prev = 0 + var i int +TransportCycle: + for { + select { + case <-termination: + break TransportCycle + default: + } + if prev == govpn.MTU { + timeouted <- struct{}{} + break TransportCycle + } + conn.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) + n, err = conn.Read(buf[prev:]) + if err != nil { + // Either EOFed or timeouted + timeouted <- struct{}{} + break TransportCycle + } + prev += n + CheckMore: + i = bytes.Index(buf[:prev], nonceExpectation) + if i == -1 { + continue } - sizeNbuf = 0 - sink <- buf[:sizeNeed] + if !peer.PktProcess(buf[:i+govpn.NonceSize], tap, false) { + timeouted <- struct{}{} + break TransportCycle + } + if atomic.LoadInt64(&peer.BytesIn)+atomic.LoadInt64(&peer.BytesOut) > govpn.MaxBytesPerKey { + log.Println("Need rehandshake") + rehandshaking <- struct{}{} + break TransportCycle + } + binary.BigEndian.PutUint64(nonceExpectation, peer.NonceExpect) + peer.NonceCipher.Encrypt(nonceExpectation, nonceExpectation) + copy(buf, buf[i+govpn.NonceSize:prev]) + prev = prev - i - govpn.NonceSize + goto CheckMore + } + if terminator != nil { + terminator <- struct{}{} } + peer.Zero() } diff --git a/src/govpn/cmd/govpn-client/udp.go b/src/govpn/cmd/govpn-client/udp.go index 57a457f..0df7ab1 100644 --- a/src/govpn/cmd/govpn-client/udp.go +++ b/src/govpn/cmd/govpn-client/udp.go @@ -21,49 +21,101 @@ package main import ( "log" "net" + "sync/atomic" "time" "govpn" ) -type UDPSender struct { - conn *net.UDPConn -} - -func (c UDPSender) Write(data []byte) (int, error) { - return c.conn.Write(data) -} - -func (c UDPSender) Reorderable() bool { - return true -} - -func startUDP() (govpn.RemoteConn, chan []byte, chan struct{}) { +func startUDP(timeouted, rehandshaking, termination chan struct{}) { remote, err := net.ResolveUDPAddr("udp", *remoteAddr) if err != nil { log.Fatalln("Can not resolve remote address:", err) } - c, err := net.DialUDP("udp", nil, remote) + conn, err := net.DialUDP("udp", nil, remote) if err != nil { log.Fatalln("Can not listen on UDP:", err) } - sink := make(chan []byte) - ready := make(chan struct{}) - go func() { - buf := make([]byte, govpn.MTU) - var n int - var err error - for { - <-ready - c.SetReadDeadline(time.Now().Add(time.Second)) - n, err = c.Read(buf) - if err != nil { - sink <- nil - continue + + hs := govpn.HandshakeStart(*remoteAddr, conn, conf) + buf := make([]byte, govpn.MTU) + var n int + var timeouts int + var peer *govpn.Peer + var terminator chan struct{} +MainCycle: + for { + select { + case <-termination: + break MainCycle + default: + } + + conn.SetReadDeadline(time.Now().Add(time.Second)) + n, err = conn.Read(buf) + if timeouts == timeout { + log.Println("Timeouted") + timeouted <- struct{}{} + break + } + if err != nil { + timeouts++ + continue + } + if peer != nil { + if peer.PktProcess(buf[:n], tap, true) { + timeouts = 0 + } else { + timeouts++ + } + if atomic.LoadInt64(&peer.BytesIn)+atomic.LoadInt64(&peer.BytesOut) > govpn.MaxBytesPerKey { + log.Println("Need rehandshake") + terminator <- struct{}{} + terminator = nil + rehandshaking <- struct{}{} + break MainCycle } - sink <- buf[:n] + continue + } + if govpn.IDsCache.Find(buf[:n]) == nil { + log.Println("Invalid identity in handshake packet") + continue + } + timeouts = 0 + peer = hs.Client(buf[:n]) + if peer == nil { + continue + } + log.Println("Handshake completed") + knownPeers = govpn.KnownPeers(map[string]**govpn.Peer{*remoteAddr: &peer}) + if firstUpCall { + go govpn.ScriptCall(*upPath, *ifaceName) + firstUpCall = false } - }() - ready <- struct{}{} - return UDPSender{c}, sink, ready + hs.Zero() + terminator = make(chan struct{}) + go func() { + heartbeat := time.NewTicker(peer.Timeout) + var data []byte + Processor: + for { + select { + case <-heartbeat.C: + peer.EthProcess(nil) + case <-terminator: + break Processor + case data = <-tap.Sink: + peer.EthProcess(data) + } + } + heartbeat.Stop() + peer.Zero() + }() + } + if terminator != nil { + terminator <- struct{}{} + } + if hs != nil { + hs.Zero() + } } diff --git a/src/govpn/cmd/govpn-server/common.go b/src/govpn/cmd/govpn-server/common.go new file mode 100644 index 0000000..92e1bb1 --- /dev/null +++ b/src/govpn/cmd/govpn-server/common.go @@ -0,0 +1,62 @@ +package main + +import ( + "bytes" + "path" + "sync" + "time" + + "govpn" +) + +type PeerState struct { + peer *govpn.Peer + terminator chan struct{} + tap *govpn.TAP +} + +var ( + handshakes map[string]*govpn.Handshake = make(map[string]*govpn.Handshake) + hsLock sync.RWMutex + + peers map[string]*PeerState = make(map[string]*PeerState) + peersLock sync.RWMutex + + peersById map[govpn.PeerId]string = make(map[govpn.PeerId]string) + peersByIdLock sync.RWMutex + + knownPeers govpn.KnownPeers + kpLock sync.RWMutex +) + +func peerReady(ps PeerState) { + var data []byte + heartbeat := time.NewTicker(ps.peer.Timeout) +Processor: + for { + select { + case <-heartbeat.C: + ps.peer.EthProcess(nil) + case <-ps.terminator: + break Processor + case data = <-ps.tap.Sink: + ps.peer.EthProcess(data) + } + } + ps.peer.Zero() + heartbeat.Stop() +} + +func callUp(peerId *govpn.PeerId) (string, error) { + upPath := path.Join(govpn.PeersPath, peerId.String(), "up.sh") + result, err := govpn.ScriptCall(upPath, "") + if err != nil { + return "", err + } + sepIndex := bytes.Index(result, []byte{'\n'}) + if sepIndex < 0 { + sepIndex = len(result) + } + ifaceName := string(result[:sepIndex]) + return ifaceName, nil +} diff --git a/src/govpn/cmd/govpn-server/main.go b/src/govpn/cmd/govpn-server/main.go index 2aa5100..dd338a2 100644 --- a/src/govpn/cmd/govpn-server/main.go +++ b/src/govpn/cmd/govpn-server/main.go @@ -16,11 +16,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -// Simple secure free software virtual private network daemon. +// Simple secure, DPI/censorship-resistant free software VPN daemon. package main import ( - "bytes" "flag" "log" "net" @@ -42,48 +41,6 @@ var ( egdPath = flag.String("egd", "", "Optional path to EGD socket") ) -type Pkt struct { - addr string - conn govpn.RemoteConn - data []byte - ready chan struct{} -} - -type PeerReadyEvent struct { - peer *govpn.Peer - iface string -} - -type PeerState struct { - peer *govpn.Peer - tap *govpn.TAP - sink chan []byte - ready chan struct{} - terminate chan struct{} -} - -func NewPeerState(peer *govpn.Peer, iface string) *PeerState { - tap, sink, ready, terminate, err := govpn.TAPListen(iface, peer.Timeout, peer.CPR) - if err != nil { - log.Println("Unable to create Eth", err) - return nil - } - state := PeerState{ - peer: peer, - tap: tap, - sink: sink, - ready: ready, - terminate: terminate, - } - return &state -} - -type EthEvent struct { - peer *govpn.Peer - data []byte - ready chan struct{} -} - func main() { flag.Parse() timeout := time.Second * time.Duration(govpn.TimeoutDefault) @@ -92,21 +49,21 @@ func main() { govpn.MTU = *mtu govpn.PeersInit(*peersPath) + knownPeers = govpn.KnownPeers(make(map[string]**govpn.Peer)) if *egdPath != "" { log.Println("Using", *egdPath, "EGD") govpn.EGDInit(*egdPath) } - sink := make(chan Pkt) switch *proto { case "udp": - startUDP(sink) + startUDP() case "tcp": - startTCP(sink) + startTCP() case "all": - startUDP(sink) - startTCP(sink) + startUDP() + startTCP() default: log.Fatalln("Unknown protocol specified") } @@ -117,22 +74,6 @@ func main() { hsHeartbeat := time.Tick(timeout) go func() { <-hsHeartbeat }() - var state *govpn.Handshake - var peerState *PeerState - var peer *govpn.Peer - var exists bool - states := make(map[string]*govpn.Handshake) - peers := make(map[string]*PeerState) - peerReadySink := make(chan PeerReadyEvent) - knownPeers := govpn.KnownPeers(make(map[string]**govpn.Peer)) - var peerReady PeerReadyEvent - var pkt Pkt - var ethEvent EthEvent - var peerId *govpn.PeerId - var peerConf *govpn.PeerConf - var handshakeProcessForce bool - ethSink := make(chan EthEvent) - log.Println("Max MTU on TAP interface:", govpn.TAPMaxMTU()) if *stats != "" { log.Println("Stats are going to listen on", *stats) @@ -143,10 +84,11 @@ func main() { go govpn.StatsProcessor(statsPort, &knownPeers) } if *proxy != "" { - go proxyStart(sink) + go proxyStart() } log.Println("Server started") + var needsDeletion bool MainCycle: for { select { @@ -154,128 +96,39 @@ MainCycle: break MainCycle case <-hsHeartbeat: now := time.Now() - for addr, hs := range states { + hsLock.Lock() + for addr, hs := range handshakes { if hs.LastPing.Add(timeout).Before(now) { log.Println("Deleting handshake state", addr) hs.Zero() - delete(states, addr) + delete(handshakes, addr) } } - for addr, state := range peers { - if state.peer.LastPing.Add(timeout).Before(now) { - log.Println("Deleting peer", state.peer) + peersLock.Lock() + peersByIdLock.Lock() + kpLock.Lock() + for addr, ps := range peers { + ps.peer.BusyR.Lock() + needsDeletion = ps.peer.LastPing.Add(timeout).Before(now) + ps.peer.BusyR.Unlock() + if needsDeletion { + log.Println("Deleting peer", ps.peer) delete(peers, addr) delete(knownPeers, addr) + delete(peersById, *ps.peer.Id) downPath := path.Join( govpn.PeersPath, - state.peer.Id.String(), + ps.peer.Id.String(), "down.sh", ) - go govpn.ScriptCall(downPath, state.tap.Name) - state.terminate <- struct{}{} - state.peer.Zero() + go govpn.ScriptCall(downPath, ps.tap.Name) + ps.terminator <- struct{}{} } } - case peerReady = <-peerReadySink: - for addr, state := range peers { - if state.tap.Name != peerReady.iface { - continue - } - delete(peers, addr) - delete(knownPeers, addr) - state.terminate <- struct{}{} - state.peer.Zero() - break - } - state := NewPeerState(peerReady.peer, peerReady.iface) - if state == nil { - continue - } - peers[peerReady.peer.Addr] = state - knownPeers[peerReady.peer.Addr] = &peerReady.peer - states[peerReady.peer.Addr].Zero() - delete(states, peerReady.peer.Addr) - log.Println("Registered interface", peerReady.iface, "with peer", peer) - go func(state *PeerState) { - for data := range state.sink { - ethSink <- EthEvent{ - peer: state.peer, - data: data, - ready: state.ready, - } - } - }(state) - case ethEvent = <-ethSink: - if s, exists := peers[ethEvent.peer.Addr]; !exists || s.peer != ethEvent.peer { - continue - } - ethEvent.peer.EthProcess(ethEvent.data, ethEvent.ready) - case pkt = <-sink: - if pkt.data == nil { - pkt.ready <- struct{}{} - continue - } - handshakeProcessForce = false - HandshakeProcess: - if _, exists = peers[pkt.addr]; handshakeProcessForce || !exists { - peerId = govpn.IDsCache.Find(pkt.data) - if peerId == nil { - log.Println("Unknown identity from", pkt.addr) - pkt.ready <- struct{}{} - continue - } - peerConf = peerId.Conf() - if peerConf == nil { - log.Println("Can not get peer configuration", peerId.String()) - pkt.ready <- struct{}{} - continue - } - state, exists = states[pkt.addr] - if !exists { - state = govpn.HandshakeNew(pkt.addr, pkt.conn, peerConf) - states[pkt.addr] = state - } - peer = state.Server(pkt.data) - if peer != nil { - log.Println("Peer handshake finished", peer) - if _, exists = peers[pkt.addr]; exists { - go func() { - peerReadySink <- PeerReadyEvent{ - peer, peers[pkt.addr].tap.Name, - } - }() - } else { - go func() { - upPath := path.Join(govpn.PeersPath, peer.Id.String(), "up.sh") - result, err := govpn.ScriptCall(upPath, "") - if err != nil { - return - } - sepIndex := bytes.Index(result, []byte{'\n'}) - if sepIndex < 0 { - sepIndex = len(result) - } - ifaceName := string(result[:sepIndex]) - peerReadySink <- PeerReadyEvent{peer, ifaceName} - }() - } - } - if !handshakeProcessForce { - pkt.ready <- struct{}{} - } - continue - } - peerState, exists = peers[pkt.addr] - if !exists { - pkt.ready <- struct{}{} - continue - } - // If it fails during processing, then try to work with it - // as with handshake packet - if !peerState.peer.PktProcess(pkt.data, peerState.tap, pkt.ready) { - handshakeProcessForce = true - goto HandshakeProcess - } + hsLock.Unlock() + peersLock.Unlock() + peersByIdLock.Unlock() + kpLock.Unlock() } } } diff --git a/src/govpn/cmd/govpn-server/proxy.go b/src/govpn/cmd/govpn-server/proxy.go index f1e8419..c05a360 100644 --- a/src/govpn/cmd/govpn-server/proxy.go +++ b/src/govpn/cmd/govpn-server/proxy.go @@ -23,9 +23,7 @@ import ( "net/http" ) -type proxyHandler struct { - sink chan Pkt -} +type proxyHandler struct{} func (p proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { conn, _, err := w.(http.Hijacker).Hijack() @@ -34,17 +32,14 @@ func (p proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } conn.Write([]byte("HTTP/1.0 200 OK\n\n")) - ready := make(chan struct{}, 1) - go handleTCP(conn, p.sink, ready) - ready <- struct{}{} - + go handleTCP(conn) } -func proxyStart(sink chan Pkt) { +func proxyStart() { log.Println("HTTP proxy listening on:", *proxy) s := &http.Server{ Addr: *proxy, - Handler: proxyHandler{sink}, + Handler: proxyHandler{}, } log.Println("HTTP proxy result:", s.ListenAndServe()) } diff --git a/src/govpn/cmd/govpn-server/tcp.go b/src/govpn/cmd/govpn-server/tcp.go index d02565c..8aedfa9 100644 --- a/src/govpn/cmd/govpn-server/tcp.go +++ b/src/govpn/cmd/govpn-server/tcp.go @@ -19,28 +19,16 @@ along with this program. If not, see . package main import ( + "bytes" "encoding/binary" "log" "net" + "time" "govpn" ) -type TCPSender struct { - conn net.Conn -} - -func (c TCPSender) Write(data []byte) (int, error) { - size := make([]byte, 2) - binary.BigEndian.PutUint16(size, uint16(len(data))) - return c.conn.Write(append(size, data...)) -} - -func (c TCPSender) Reorderable() bool { - return false -} - -func startTCP(sink chan Pkt) { +func startTCP() { bind, err := net.ResolveTCPAddr("tcp", *bindAddr) if err != nil { log.Fatalln("Can not resolve bind address:", err) @@ -52,59 +40,148 @@ func startTCP(sink chan Pkt) { log.Println("Listening on TCP", *bindAddr) go func() { for { - conn, _ := listener.AcceptTCP() - ready := make(chan struct{}, 1) - go handleTCP(conn, sink, ready) - ready <- struct{}{} + conn, err := listener.AcceptTCP() + if err != nil { + log.Println("Error accepting TCP:", err) + continue + } + go handleTCP(conn) } }() } -func handleTCP(conn net.Conn, sink chan Pkt, ready chan struct{}) { +func handleTCP(conn net.Conn) { addr := conn.RemoteAddr().String() - var err error - var n int - var sizeNbuf int - sizeBuf := make([]byte, 2) - var sizeNeed uint16 - var bufN uint16 buf := make([]byte, govpn.MTU) + var n int + var err error + var prev int + var hs *govpn.Handshake + var ps *PeerState + var peer *govpn.Peer + var tap *govpn.TAP + var conf *govpn.PeerConf for { - <-ready - if sizeNbuf != 2 { - n, err = conn.Read(sizeBuf[sizeNbuf:2]) - if err != nil { + if prev == govpn.MTU { + break + } + conn.SetReadDeadline(time.Now().Add(time.Duration(govpn.TimeoutDefault) * time.Second)) + n, err = conn.Read(buf[prev:]) + if err != nil { + // Either EOFed or timeouted + break + } + prev += n + peerId := govpn.IDsCache.Find(buf[:prev]) + if peerId == nil { + continue + } + if hs == nil { + conf = peerId.Conf() + if conf == nil { + log.Println("Can not get peer configuration:", peerId.String()) break } - sizeNbuf += n - if sizeNbuf != 2 { - sink <- Pkt{ready: ready} - continue + hs = govpn.NewHandshake(addr, conn, conf) + } + peer = hs.Server(buf[:prev]) + prev = 0 + if peer == nil { + continue + } + hs.Zero() + peersByIdLock.RLock() + addrPrev, exists := peersById[*peer.Id] + peersByIdLock.RUnlock() + if exists { + peersLock.RLock() + tap = peers[addrPrev].tap + ps = &PeerState{ + peer: peer, + tap: tap, + terminator: peers[addrPrev].terminator, } - sizeNeed = binary.BigEndian.Uint16(sizeBuf) - if int(sizeNeed) > govpn.MTU-2 { - log.Println("Invalid TCP size, skipping") - sizeNbuf = 0 - sink <- Pkt{ready: ready} - continue + peersLock.RUnlock() + ps.terminator <- struct{}{} + peersLock.Lock() + peersByIdLock.Lock() + kpLock.Lock() + delete(peers, addrPrev) + delete(knownPeers, addrPrev) + delete(peersById, *peer.Id) + peers[addr] = ps + knownPeers[addr] = &peer + peersById[*peer.Id] = addr + peersLock.Unlock() + peersByIdLock.Unlock() + kpLock.Unlock() + go peerReady(*ps) + log.Println("Rehandshake finished:", peer.Id.String()) + } else { + ifaceName, err := callUp(peer.Id) + if err != nil { + break } - bufN = 0 - } - ReadMore: - if sizeNeed != bufN { - n, err = conn.Read(buf[bufN:sizeNeed]) + tap, err = govpn.TAPListen(ifaceName) if err != nil { + log.Println("Unable to create TAP:", err) break } - bufN += uint16(n) - goto ReadMore + ps = &PeerState{ + peer: peer, + tap: tap, + terminator: make(chan struct{}, 1), + } + go peerReady(*ps) + peersLock.Lock() + peersByIdLock.Lock() + kpLock.Lock() + peers[addr] = ps + peersById[*peer.Id] = addr + knownPeers[addr] = &peer + peersLock.Unlock() + peersByIdLock.Unlock() + kpLock.Unlock() + log.Println("New peer:", peer.Id.String()) + } + break + } + if hs != nil { + hs.Zero() + } + if peer == nil { + return + } + + nonceExpectation := make([]byte, govpn.NonceSize) + binary.BigEndian.PutUint64(nonceExpectation, peer.NonceExpect) + peer.NonceCipher.Encrypt(nonceExpectation, nonceExpectation) + prev = 0 + var i int + for { + if prev == govpn.MTU { + break + } + conn.SetReadDeadline(time.Now().Add(conf.Timeout)) + n, err = conn.Read(buf[prev:]) + if err != nil { + // Either EOFed or timeouted + break + } + prev += n + CheckMore: + i = bytes.Index(buf[:prev], nonceExpectation) + if i == -1 { + continue } - sizeNbuf = 0 - sink <- Pkt{ - addr, - TCPSender{conn}, - buf[:sizeNeed], - ready, + if !peer.PktProcess(buf[:i+govpn.NonceSize], tap, false) { + break } + binary.BigEndian.PutUint64(nonceExpectation, peer.NonceExpect) + peer.NonceCipher.Encrypt(nonceExpectation, nonceExpectation) + copy(buf, buf[i+govpn.NonceSize:prev]) + prev = prev - i - govpn.NonceSize + goto CheckMore } + peer.Zero() } diff --git a/src/govpn/cmd/govpn-server/udp.go b/src/govpn/cmd/govpn-server/udp.go index e6bbc5e..4a57d87 100644 --- a/src/govpn/cmd/govpn-server/udp.go +++ b/src/govpn/cmd/govpn-server/udp.go @@ -21,7 +21,6 @@ package main import ( "log" "net" - "time" "govpn" ) @@ -35,41 +34,163 @@ func (c UDPSender) Write(data []byte) (int, error) { return c.conn.WriteToUDP(data, c.addr) } -func (c UDPSender) Reorderable() bool { - return true -} +var ( + // Buffers for UDP parallel processing + udpBufs chan []byte = make(chan []byte, 1<<8) +) -func startUDP(sink chan Pkt) { +func startUDP() { bind, err := net.ResolveUDPAddr("udp", *bindAddr) - ready := make(chan struct{}) if err != nil { log.Fatalln("Can not resolve bind address:", err) } - lconn, err := net.ListenUDP("udp", bind) + conn, err := net.ListenUDP("udp", bind) if err != nil { log.Fatalln("Can not listen on UDP:", err) } log.Println("Listening on UDP", *bindAddr) + udpBufs <- make([]byte, govpn.MTU) go func() { - buf := make([]byte, govpn.MTU) - var n int + var buf []byte var raddr *net.UDPAddr + var addr string + var n int var err error + var ps *PeerState + var hs *govpn.Handshake + var addrPrev string + var exists bool + var peerId *govpn.PeerId + var peer *govpn.Peer + var conf *govpn.PeerConf for { - <-ready - lconn.SetReadDeadline(time.Now().Add(time.Second)) - n, raddr, err = lconn.ReadFromUDP(buf) + buf = <-udpBufs + + n, raddr, err = conn.ReadFromUDP(buf) if err != nil { - sink <- Pkt{ready: ready} - continue + break + } + addr = raddr.String() + + peersLock.RLock() + ps, exists = peers[addr] + peersLock.RUnlock() + if !exists { + goto CheckHandshake + } + go func(ps *govpn.Peer, tap *govpn.TAP, buf []byte, n int) { + peer.PktProcess(buf[:n], tap, true) + udpBufs <- buf + }(ps.peer, ps.tap, buf, n) + continue + CheckHandshake: + hsLock.RLock() + hs, exists = handshakes[addr] + hsLock.RUnlock() + if !exists { + goto CheckID + } + peer = hs.Server(buf[:n]) + if peer == nil { + goto Finished + } + + log.Println("Peer handshake finished:", addr) + hs.Zero() + hsLock.Lock() + delete(handshakes, addr) + hsLock.Unlock() + + go func() { + udpBufs <- make([]byte, govpn.MTU) + udpBufs <- make([]byte, govpn.MTU) + }() + peersByIdLock.RLock() + addrPrev, exists = peersById[*peer.Id] + peersByIdLock.RUnlock() + if exists { + peersLock.RLock() + ps = &PeerState{ + peer: peer, + tap: peers[addrPrev].tap, + terminator: peers[addrPrev].terminator, + } + peersLock.RUnlock() + ps.terminator <- struct{}{} + peersLock.Lock() + peersByIdLock.Lock() + kpLock.Lock() + delete(peers, addrPrev) + delete(knownPeers, addrPrev) + delete(peersById, *peer.Id) + peers[addr] = ps + knownPeers[addr] = &peer + peersById[*peer.Id] = addr + peersLock.Unlock() + peersByIdLock.Unlock() + kpLock.Unlock() + go func(ps PeerState) { + peerReady(ps) + <-udpBufs + <-udpBufs + }(*ps) + log.Println("Rehandshake finished:", peer.Id.String()) + } else { + go func(addr string, peer *govpn.Peer) { + ifaceName, err := callUp(peer.Id) + if err != nil { + return + } + tap, err := govpn.TAPListen(ifaceName) + if err != nil { + log.Println("Unable to create TAP:", err) + return + } + ps = &PeerState{ + peer: peer, + tap: tap, + terminator: make(chan struct{}, 1), + } + go func(ps PeerState) { + peerReady(ps) + <-udpBufs + <-udpBufs + }(*ps) + peersLock.Lock() + peersByIdLock.Lock() + kpLock.Lock() + peers[addr] = ps + knownPeers[addr] = &peer + peersById[*peer.Id] = addr + peersLock.Unlock() + peersByIdLock.Unlock() + kpLock.Unlock() + log.Println("New peer:", peer.Id.String()) + }(addr, peer) + } + goto Finished + CheckID: + peerId = govpn.IDsCache.Find(buf[:n]) + if peerId == nil { + log.Println("Unknown identity from:", addr) + goto Finished } - sink <- Pkt{ - raddr.String(), - UDPSender{lconn, raddr}, - buf[:n], - ready, + conf = peerId.Conf() + if conf == nil { + log.Println("Can not get peer configuration:", peerId.String()) + goto Finished } + hs = govpn.NewHandshake( + addr, + UDPSender{conn: conn, addr: raddr}, + conf, + ) + hs.Server(buf[:n]) + hsLock.Lock() + handshakes[addr] = hs + hsLock.Unlock() + Finished: + udpBufs <- buf } }() - ready <- struct{}{} } diff --git a/src/govpn/common.go b/src/govpn/common.go index fbafd56..263ef45 100644 --- a/src/govpn/common.go +++ b/src/govpn/common.go @@ -23,6 +23,7 @@ import ( "os" "os/exec" "runtime" + "time" ) const ( @@ -62,3 +63,10 @@ func sliceZero(data []byte) { func VersionGet() string { return "GoVPN version " + Version + " built with " + runtime.Version() } + +func cprCycleCalculate(rate int) time.Duration { + if rate == 0 { + return time.Duration(0) + } + return time.Second / time.Duration(rate*(1<<10)/MTU) +} diff --git a/src/govpn/govpn.go b/src/govpn/govpn.go index 9a7b114..996f31f 100644 --- a/src/govpn/govpn.go +++ b/src/govpn/govpn.go @@ -16,5 +16,5 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -// Simple secure free software virtual private network daemon +// Simple secure, DPI/censorship-resistant free software VPN daemon. package govpn diff --git a/src/govpn/handshake.go b/src/govpn/handshake.go index d99154a..b771d49 100644 --- a/src/govpn/handshake.go +++ b/src/govpn/handshake.go @@ -22,6 +22,7 @@ import ( "crypto/rand" "crypto/subtle" "encoding/binary" + "io" "log" "time" @@ -40,7 +41,7 @@ const ( type Handshake struct { addr string - conn RemoteConn + conn io.Writer LastPing time.Time Conf *PeerConf dsaPubH *[ed25519.PublicKeySize]byte @@ -133,7 +134,7 @@ func dhKeyGen(priv, pub *[32]byte) *[32]byte { } // Create new handshake state. -func HandshakeNew(addr string, conn RemoteConn, conf *PeerConf) *Handshake { +func NewHandshake(addr string, conn io.Writer, conf *PeerConf) *Handshake { state := Handshake{ addr: addr, conn: conn, @@ -160,8 +161,8 @@ func idTag(id *PeerId, data []byte) []byte { // Start handshake's procedure from the client. It is the entry point // for starting the handshake procedure. // First handshake packet // will be sent immediately. -func HandshakeStart(addr string, conn RemoteConn, conf *PeerConf) *Handshake { - state := HandshakeNew(addr, conn, conf) +func HandshakeStart(addr string, conn io.Writer, conf *PeerConf) *Handshake { + state := NewHandshake(addr, conn, conf) var dhPubRepr *[32]byte state.dhPriv, dhPubRepr = dhKeypairGen() diff --git a/src/govpn/peer.go b/src/govpn/peer.go new file mode 100644 index 0000000..5c6e13b --- /dev/null +++ b/src/govpn/peer.go @@ -0,0 +1,309 @@ +package govpn + +import ( + "encoding/binary" + "io" + "sync" + "sync/atomic" + "time" + + "golang.org/x/crypto/poly1305" + "golang.org/x/crypto/salsa20" + "golang.org/x/crypto/xtea" +) + +const ( + NonceSize = 8 + NonceBucketSize = 128 + TagSize = poly1305.TagSize + // S20BS is Salsa20's internal blocksize in bytes + S20BS = 64 + // Maximal amount of bytes transfered with single key (4 GiB) + MaxBytesPerKey int64 = 1 << 32 + // Size of packet's size mark in bytes + PktSizeSize = 2 + // Heartbeat rate, relative to Timeout + TimeoutHeartbeat = 4 +) + +func newNonceCipher(key *[32]byte) *xtea.Cipher { + nonceKey := make([]byte, 16) + salsa20.XORKeyStream( + nonceKey, + make([]byte, 32), + make([]byte, xtea.BlockSize), + key, + ) + ciph, err := xtea.NewCipher(nonceKey) + if err != nil { + panic(err) + } + return ciph +} + +type Peer struct { + Addr string + Id *PeerId + Conn io.Writer + + // Traffic behaviour + NoiseEnable bool + CPR int + CPRCycle time.Duration `json:"-"` + + // Cryptography related + Key *[SSize]byte `json:"-"` + NonceCipher *xtea.Cipher `json:"-"` + nonceRecv uint64 + nonceLatest uint64 + nonceOur uint64 + NonceExpect uint64 `json:"-"` + nonceBucket0 map[uint64]struct{} + nonceBucket1 map[uint64]struct{} + nonceFound0 bool + nonceFound1 bool + nonceBucketN int32 + + // Timers + Timeout time.Duration `json:"-"` + Established time.Time + LastPing time.Time + LastSent time.Time + willSentCycle time.Time + + // Statistics + BytesIn int64 + BytesOut int64 + BytesPayloadIn int64 + BytesPayloadOut int64 + FramesIn int + FramesOut int + FramesUnauth int + FramesDup int + HeartbeatRecv int + HeartbeatSent int + + // Receiver + BusyR sync.Mutex + bufR []byte + tagR *[TagSize]byte + keyAuthR *[SSize]byte + pktSizeR uint16 + + // Transmitter + BusyT sync.Mutex + bufT []byte + tagT *[TagSize]byte + keyAuthT *[SSize]byte + frameT []byte + now time.Time +} + +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 newPeer(isClient bool, addr string, conn io.Writer, conf *PeerConf, key *[SSize]byte) *Peer { + now := time.Now() + timeout := conf.Timeout + + cprCycle := cprCycleCalculate(conf.CPR) + noiseEnable := conf.NoiseEnable + if conf.CPR > 0 { + noiseEnable = true + timeout = cprCycle + } else { + timeout = timeout / TimeoutHeartbeat + } + + peer := Peer{ + Addr: addr, + Id: conf.Id, + Conn: conn, + + NoiseEnable: noiseEnable, + CPR: conf.CPR, + CPRCycle: cprCycle, + + Key: key, + NonceCipher: newNonceCipher(key), + nonceBucket0: make(map[uint64]struct{}, NonceBucketSize), + nonceBucket1: make(map[uint64]struct{}, NonceBucketSize), + + Timeout: timeout, + Established: now, + LastPing: now, + + bufR: make([]byte, S20BS+MTU+NonceSize), + bufT: make([]byte, S20BS+MTU+NonceSize), + tagR: new([TagSize]byte), + tagT: new([TagSize]byte), + keyAuthR: new([SSize]byte), + keyAuthT: new([SSize]byte), + } + if isClient { + peer.nonceOur = 1 + peer.NonceExpect = 0 + 2 + } else { + peer.nonceOur = 0 + peer.NonceExpect = 1 + 2 + } + 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) { + p.now = time.Now() + p.BusyT.Lock() + + // Zero size is a heartbeat packet + if len(data) == 0 { + // If this heartbeat is necessary + if !p.LastSent.Add(p.Timeout).Before(p.now) { + p.BusyT.Unlock() + return + } + p.bufT[S20BS+0] = byte(0) + p.bufT[S20BS+1] = byte(0) + p.HeartbeatSent++ + } else { + // Copy payload to our internal buffer and we are ready to + // accept the next one + binary.BigEndian.PutUint16( + p.bufT[S20BS:S20BS+PktSizeSize], + uint16(len(data)), + ) + copy(p.bufT[S20BS+PktSizeSize:], data) + p.BytesPayloadOut += int64(len(data)) + } + + if p.NoiseEnable { + p.frameT = p.bufT[S20BS : S20BS+MTU-TagSize] + } else { + p.frameT = p.bufT[S20BS : S20BS+PktSizeSize+len(data)+NonceSize] + } + p.nonceOur += 2 + binary.BigEndian.PutUint64(p.frameT[len(p.frameT)-NonceSize:], p.nonceOur) + p.NonceCipher.Encrypt( + p.frameT[len(p.frameT)-NonceSize:], + p.frameT[len(p.frameT)-NonceSize:], + ) + for i := 0; i < SSize; i++ { + p.bufT[i] = byte(0) + } + salsa20.XORKeyStream( + p.bufT[:S20BS+len(p.frameT)-NonceSize], + p.bufT[:S20BS+len(p.frameT)-NonceSize], + p.frameT[len(p.frameT)-NonceSize:], + p.Key, + ) + + copy(p.keyAuthT[:], p.bufT[:SSize]) + poly1305.Sum(p.tagT, p.frameT, p.keyAuthT) + + atomic.AddInt64(&p.BytesOut, int64(len(p.frameT)+TagSize)) + p.FramesOut++ + + if p.CPRCycle != time.Duration(0) { + p.willSentCycle = p.LastSent.Add(p.CPRCycle) + if p.willSentCycle.After(p.now) { + time.Sleep(p.willSentCycle.Sub(p.now)) + p.now = p.willSentCycle + } + } + + p.LastSent = p.now + p.Conn.Write(append(p.tagT[:], p.frameT...)) + p.BusyT.Unlock() +} + +func (p *Peer) PktProcess(data []byte, tap io.Writer, reorderable bool) bool { + p.BusyR.Lock() + for i := 0; i < SSize; i++ { + p.bufR[i] = byte(0) + } + copy(p.bufR[S20BS:], data[TagSize:]) + salsa20.XORKeyStream( + p.bufR[:S20BS+len(data)-TagSize-NonceSize], + p.bufR[:S20BS+len(data)-TagSize-NonceSize], + data[len(data)-NonceSize:], + 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 + } + + // Check if received nonce is known to us in either of two buckets. + // If yes, then this is ignored duplicate. + // Check from the oldest bucket, as in most cases this will result + // in constant time check. + // If Bucket0 is filled, then it becomes Bucket1. + p.NonceCipher.Decrypt( + data[len(data)-NonceSize:], + data[len(data)-NonceSize:], + ) + p.nonceRecv = binary.BigEndian.Uint64(data[len(data)-NonceSize:]) + if reorderable { + _, p.nonceFound0 = p.nonceBucket0[p.nonceRecv] + _, p.nonceFound1 = p.nonceBucket1[p.nonceRecv] + if p.nonceFound0 || p.nonceFound1 || p.nonceRecv+2*NonceBucketSize < p.nonceLatest { + p.FramesDup++ + p.BusyR.Unlock() + return false + } + p.nonceBucket0[p.nonceRecv] = struct{}{} + p.nonceBucketN++ + if p.nonceBucketN == NonceBucketSize { + p.nonceBucket1 = p.nonceBucket0 + p.nonceBucket0 = make(map[uint64]struct{}, NonceBucketSize) + p.nonceBucketN = 0 + } + } else { + if p.nonceRecv != p.NonceExpect { + p.FramesDup++ + p.BusyR.Unlock() + return false + } + p.NonceExpect += 2 + } + if p.nonceRecv > p.nonceLatest { + p.nonceLatest = p.nonceRecv + } + + p.FramesIn++ + atomic.AddInt64(&p.BytesIn, int64(len(data))) + p.LastPing = time.Now() + p.pktSizeR = binary.BigEndian.Uint16(p.bufR[S20BS : S20BS+PktSizeSize]) + + if p.pktSizeR == 0 { + p.HeartbeatRecv++ + p.BusyR.Unlock() + return true + } + p.BytesPayloadIn += int64(p.pktSizeR) + tap.Write(p.bufR[S20BS+PktSizeSize : S20BS+PktSizeSize+p.pktSizeR]) + p.BusyR.Unlock() + return true +} diff --git a/src/govpn/tap.go b/src/govpn/tap.go index c5e3bf8..9c999b4 100644 --- a/src/govpn/tap.go +++ b/src/govpn/tap.go @@ -29,14 +29,18 @@ const ( ) type TAP struct { - Name string - dev io.ReadWriter - buf []byte - sink chan []byte - ready chan struct{} - synced bool + Name string + Sink chan []byte + dev io.ReadWriter + buf0 []byte + buf1 []byte + bufZ bool } +var ( + taps = make(map[string]*TAP) +) + // Return maximal acceptable TAP interface MTU. This is daemon's MTU // minus nonce, MAC, packet size mark and Ethernet header sizes. func TAPMaxMTU() int { @@ -50,23 +54,28 @@ func NewTAP(ifaceName string) (*TAP, error) { return nil, err } tap := TAP{ - Name: ifaceName, - dev: tapRaw, - buf: make([]byte, maxIfacePktSize), - sink: make(chan []byte), - ready: make(chan struct{}), - synced: false, + Name: ifaceName, + dev: tapRaw, + buf0: make([]byte, maxIfacePktSize), + buf1: make([]byte, maxIfacePktSize), + Sink: make(chan []byte), } go func() { var n int var err error + var buf []byte for { - <-tap.ready - n, err = tap.dev.Read(tap.buf) + if tap.bufZ { + buf = tap.buf0 + } else { + buf = tap.buf1 + } + tap.bufZ = !tap.bufZ + n, err = tap.dev.Read(buf) if err != nil { panic("Reading TAP:" + err.Error()) } - tap.sink <- tap.buf[:n] + tap.Sink <- buf[:n] } }() return &tap, nil @@ -75,3 +84,16 @@ func NewTAP(ifaceName string) (*TAP, error) { func (t *TAP) Write(data []byte) (n int, err error) { return t.dev.Write(data) } + +func TAPListen(ifaceName string) (*TAP, error) { + tap, exists := taps[ifaceName] + if exists { + return tap, nil + } + tap, err := NewTAP(ifaceName) + if err != nil { + return nil, err + } + taps[ifaceName] = tap + return tap, nil +} diff --git a/src/govpn/transport.go b/src/govpn/transport.go deleted file mode 100644 index 9c7a733..0000000 --- a/src/govpn/transport.go +++ /dev/null @@ -1,379 +0,0 @@ -/* -GoVPN -- simple secure free software virtual private network daemon -Copyright (C) 2014-2015 Sergey Matveev - -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 . -*/ - -package govpn - -import ( - "encoding/binary" - "io" - "time" - - "golang.org/x/crypto/poly1305" - "golang.org/x/crypto/salsa20" - "golang.org/x/crypto/xtea" -) - -const ( - NonceSize = 8 - NonceBucketSize = 128 - // S20BS is Salsa20's internal blocksize in bytes - S20BS = 64 - // Maximal amount of bytes transfered with single key (4 GiB) - MaxBytesPerKey int64 = 1 << 32 - // Size of packet's size mark in bytes - PktSizeSize = 2 - // Heartbeat rate, relative to Timeout - TimeoutHeartbeat = 4 -) - -type RemoteConn interface { - io.Writer - // Can incoming packets be reordered - Reorderable() bool -} - -type Peer struct { - Addr string - Id *PeerId - Conn RemoteConn - - // Traffic behaviour - NoiseEnable bool - CPR int - CPRCycle time.Duration `json:"-"` - - // Cryptography related - Key *[SSize]byte `json:"-"` - NonceOur uint64 `json:"-"` - NonceRecv uint64 `json:"-"` - NonceCipher *xtea.Cipher `json:"-"` - nonceExpect uint64 `json:"-"` - nonceBucket0 map[uint64]struct{} - nonceBucket1 map[uint64]struct{} - nonceFound bool - nonceBucketN int32 - - // Timers - Timeout time.Duration `json:"-"` - Established time.Time - LastPing time.Time - LastSent time.Time - willSentCycle time.Time - - // This variables are initialized only once to relief GC - buf []byte - tag *[poly1305.TagSize]byte - keyAuth *[32]byte - nonceRecv uint64 - frame []byte - nonce []byte - pktSize uint16 - size int - now time.Time - - // Statistics - BytesIn int64 - BytesOut int64 - BytesPayloadIn int64 - BytesPayloadOut int64 - FramesIn int - FramesOut int - FramesUnauth int - FramesDup int - HeartbeatRecv int - HeartbeatSent int -} - -func (p *Peer) String() string { - return p.Id.String() + ":" + p.Addr -} - -// Zero peer's memory state. -func (p *Peer) Zero() { - sliceZero(p.Key[:]) - sliceZero(p.tag[:]) - sliceZero(p.keyAuth[:]) - sliceZero(p.buf) - sliceZero(p.frame) - sliceZero(p.nonce) -} - -var ( - taps = make(map[string]*TAP) -) - -// Create TAP listening goroutine. -// This function takes required TAP interface name, opens it and allocates -// a buffer where all frame data will be written, channel where information -// about number of read bytes is sent to, synchronization channel (external -// processes tell that read buffer can be used again) and possible channel -// opening error. -func TAPListen(ifaceName string, timeout time.Duration, cpr int) (*TAP, chan []byte, chan struct{}, chan struct{}, error) { - var tap *TAP - var err error - tap, exists := taps[ifaceName] - if !exists { - tap, err = NewTAP(ifaceName) - if err != nil { - return nil, nil, nil, nil, err - } - taps[ifaceName] = tap - } - sink := make(chan []byte) - sinkReady := make(chan struct{}) - sinkTerminate := make(chan struct{}) - sinkSkip := make(chan struct{}) - - go func() { - cprCycle := cprCycleCalculate(cpr) - if cprCycle != time.Duration(0) { - timeout = cprCycle - } else { - timeout = timeout / TimeoutHeartbeat - } - heartbeat := time.Tick(timeout) - var pkt []byte - ListenCycle: - for { - select { - case <-sinkTerminate: - break ListenCycle - case <-heartbeat: - go func() { sink <- make([]byte, 0) }() - continue - case <-sinkSkip: - case <-sinkReady: - tap.ready <- struct{}{} - tap.synced = true - } - HeartbeatCatched: - select { - case <-heartbeat: - go func() { sink <- make([]byte, 0) }() - goto HeartbeatCatched - case <-sinkTerminate: - break ListenCycle - case pkt = <-tap.sink: - tap.synced = false - sink <- pkt - } - } - close(sink) - close(sinkReady) - close(sinkTerminate) - }() - if exists && tap.synced { - sinkSkip <- struct{}{} - } else { - sinkReady <- struct{}{} - } - return tap, sink, sinkReady, sinkTerminate, nil -} - -func newNonceCipher(key *[32]byte) *xtea.Cipher { - nonceKey := make([]byte, 16) - salsa20.XORKeyStream( - nonceKey, - make([]byte, 32), - make([]byte, xtea.BlockSize), - key, - ) - ciph, err := xtea.NewCipher(nonceKey) - if err != nil { - panic(err) - } - return ciph -} - -func cprCycleCalculate(rate int) time.Duration { - if rate == 0 { - return time.Duration(0) - } - return time.Second / time.Duration(rate*(1<<10)/MTU) -} - -func newPeer(isClient bool, addr string, conn RemoteConn, conf *PeerConf, key *[SSize]byte) *Peer { - now := time.Now() - timeout := conf.Timeout - cprCycle := cprCycleCalculate(conf.CPR) - noiseEnable := conf.NoiseEnable - if conf.CPR > 0 { - noiseEnable = true - timeout = cprCycle - } else { - timeout = timeout / TimeoutHeartbeat - } - peer := Peer{ - Addr: addr, - Conn: conn, - Timeout: timeout, - Established: now, - LastPing: now, - Id: conf.Id, - NoiseEnable: noiseEnable, - CPR: conf.CPR, - CPRCycle: cprCycle, - NonceRecv: 0, - Key: key, - NonceCipher: newNonceCipher(key), - buf: make([]byte, MTU+S20BS+NonceSize+poly1305.TagSize), - tag: new([poly1305.TagSize]byte), - keyAuth: new([SSize]byte), - nonce: make([]byte, NonceSize), - } - if isClient { - peer.NonceOur = 1 - peer.nonceExpect = 0 + 2 - } else { - peer.NonceOur = 0 - peer.nonceExpect = 1 + 2 - } - if conn.Reorderable() { - peer.nonceBucket0 = make(map[uint64]struct{}, NonceBucketSize) - peer.nonceBucket1 = make(map[uint64]struct{}, NonceBucketSize) - } - return &peer -} - -// Process incoming UDP packet. -// ConnListen'es synchronization channel used to tell him that he is -// free to receive new packets. Authenticated and decrypted packets -// will be written to the interface immediately (except heartbeat ones). -func (p *Peer) PktProcess(data []byte, tap io.Writer, ready chan struct{}) bool { - p.size = len(data) - p.frame = make([]byte, p.size) - copy(p.frame, data) - ready <- struct{}{} - - copy(p.buf[S20BS:], p.frame[:p.size-NonceSize-poly1305.TagSize]) - for i := 0; i < S20BS; i++ { - p.buf[i] = byte(0) - } - salsa20.XORKeyStream( - p.buf[:S20BS+p.size-NonceSize-poly1305.TagSize], - p.buf[:S20BS+p.size-NonceSize-poly1305.TagSize], - p.frame[p.size-NonceSize-poly1305.TagSize:p.size-poly1305.TagSize], - p.Key, - ) - copy(p.tag[:], p.frame[p.size-poly1305.TagSize:]) - copy(p.keyAuth[:], p.buf[:SSize]) - if !poly1305.Verify(p.tag, p.frame[:p.size-poly1305.TagSize], p.keyAuth) { - p.FramesUnauth++ - return false - } - - // Check if received nonce is known to us in either of two buckets. - // If yes, then this is ignored duplicate. - // Check from the oldest bucket, as in most cases this will result - // in constant time check. - // If Bucket0 is filled, then it becomes Bucket1. - p.NonceCipher.Decrypt( - p.nonce, - p.frame[p.size-NonceSize-poly1305.TagSize:p.size-poly1305.TagSize], - ) - - p.nonceRecv = binary.BigEndian.Uint64(p.nonce) - if p.Conn.Reorderable() { - if _, p.nonceFound = p.nonceBucket1[p.NonceRecv]; p.nonceFound { - p.FramesDup++ - return false - } - if _, p.nonceFound = p.nonceBucket0[p.NonceRecv]; p.nonceFound { - p.FramesDup++ - return false - } - p.nonceBucket0[p.NonceRecv] = struct{}{} - p.nonceBucketN++ - if p.nonceBucketN == NonceBucketSize { - p.nonceBucket1 = p.nonceBucket0 - p.nonceBucket0 = make(map[uint64]struct{}, NonceBucketSize) - p.nonceBucketN = 0 - } - } else { - if p.nonceRecv != p.nonceExpect { - p.FramesDup++ - return false - } - p.nonceExpect += 2 - } - - p.FramesIn++ - p.BytesIn += int64(p.size) - p.LastPing = time.Now() - p.NonceRecv = p.nonceRecv - p.pktSize = binary.BigEndian.Uint16(p.buf[S20BS : S20BS+PktSizeSize]) - if p.pktSize == 0 { - p.HeartbeatRecv++ - return true - } - p.BytesPayloadIn += int64(p.pktSize) - tap.Write(p.buf[S20BS+PktSizeSize : S20BS+PktSizeSize+p.pktSize]) - return true -} - -// 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, ready chan struct{}) { - p.now = time.Now() - p.size = len(data) - // If this heartbeat is necessary - if p.size == 0 && !p.LastSent.Add(p.Timeout).Before(p.now) { - return - } - if p.size > 0 { - copy(p.buf[S20BS+PktSizeSize:], data) - ready <- struct{}{} - binary.BigEndian.PutUint16(p.buf[S20BS:S20BS+PktSizeSize], uint16(p.size)) - p.BytesPayloadOut += int64(p.size) - } else { - p.buf[S20BS+0] = byte(0) - p.buf[S20BS+1] = byte(0) - p.HeartbeatSent++ - } - - p.NonceOur += 2 - binary.BigEndian.PutUint64(p.nonce, p.NonceOur) - p.NonceCipher.Encrypt(p.nonce, p.nonce) - - for i := 0; i < S20BS; i++ { - p.buf[i] = byte(0) - } - salsa20.XORKeyStream(p.buf, p.buf, p.nonce, p.Key) - if p.NoiseEnable { - p.frame = append(p.buf[S20BS:S20BS+MTU-NonceSize-poly1305.TagSize], p.nonce...) - } else { - p.frame = append(p.buf[S20BS:S20BS+PktSizeSize+p.size], p.nonce...) - } - copy(p.keyAuth[:], p.buf[:SSize]) - poly1305.Sum(p.tag, p.frame, p.keyAuth) - - p.BytesOut += int64(len(p.frame) + poly1305.TagSize) - p.FramesOut++ - - if p.CPRCycle != time.Duration(0) { - p.willSentCycle = p.LastSent.Add(p.CPRCycle) - if p.willSentCycle.After(p.now) { - time.Sleep(p.willSentCycle.Sub(p.now)) - p.now = p.willSentCycle - } - } - p.LastSent = p.now - p.Conn.Write(append(p.frame, p.tag[:]...)) -}