]> Cypherpunks.ru repositories - govpn.git/commitdiff
Texinfo documentation, client ID, simultaneous clients 2.0
authorSergey Matveev <stargrave@stargrave.org>
Thu, 5 Mar 2015 15:52:02 +0000 (18:52 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 12 Mar 2015 19:30:21 +0000 (22:30 +0300)
Signed-off-by: Sergey Matveev <stargrave@stargrave.org>
22 files changed:
INSTALL [new file with mode: 0644]
NEWS [new file with mode: 0644]
README
THANKS [new file with mode: 0644]
TODO [new file with mode: 0644]
cmd/govpn-client/main.go [new file with mode: 0644]
cmd/govpn-server/main.go [new file with mode: 0644]
common.go [new file with mode: 0644]
doc/.gitignore [new file with mode: 0644]
doc/download.texi [new file with mode: 0644]
doc/govpn.texi [new file with mode: 0644]
doc/handshake.txt [new file with mode: 0644]
doc/makefile [new file with mode: 0644]
govpn.go
handshake.go
identify.go [new file with mode: 0644]
makedist.sh [new file with mode: 0755]
makefile [new file with mode: 0644]
tap.go [new file with mode: 0644]
tap_freebsd.go
tap_linux.go
transport.go [new file with mode: 0644]

diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..c1f1dc3
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,5 @@
+GoVPN is a program written on Go programming language. If you have set
+up $GOPATH environment, then simple "make all" should build govpn-client
+and govpn-server executable binaries.
+
+For usage documentation see either doc/govpn.info or doc/govpn.texi.
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..bf44317
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,27 @@
+Release 2.0
+-----------
+* Added clients identification.
+* Simultaneous several clients support by server.
+* Per-client up/down scripts.
+
+Release 1.5
+-----------
+* Nonce obfuscation/encryption.
+
+Release 1.4
+-----------
+* Performance optimizations.
+
+Release 1.3
+-----------
+* Heartbeat feature.
+* Rehandshake feature.
+* up- and down- optinal scripts.
+
+Release 1.1
+-----------
+* FreeBSD support.
+
+Release 1.0
+-----------
+* Initial stable release.
diff --git a/README b/README
index dfef9cbda7c98c12f5d9931a350b2ab87eca9cf5..bdb348941c9ca5ab1d62b86b5cfe3f13ae6c00e8 100644 (file)
--- a/README
+++ b/README
-                                 GoVPN
-                                 =====
-SYNOPSIS
+GoVPN is simple secure free software virtual private network daemon. It
+uses Diffie-Hellman Encrypted Key Exchange (DH-EKE) for mutual
+zero-knowledge peers authentication and authenticated encrypted data
+transport.
 
-govpn is simple secure virtual private network daemon.
-It uses DH-EKE for mutual zero-knowledge authentication and
-authenticated encrypted transport. It runs under GNU/Linux and FreeBSD.
+Home page: http://www.cypherpunks.ru/govpn/
 
-FEATURES
+Send bug reports, questions and patches to govpn-devel@lists.cypherpunks.ru
+mailing list. Visit https://lists.cypherpunks.ru/mailman/listinfo/govpn-devel
+for subscription and archive access information.
 
-* GNU/Linux and FreeBSD support
-* IPv6 compatible
-* Encrypted and authenticated transport
-* Relatively fast handshake
-* Replay attack protection
-* Perfect forward secrecy (if long-term pre-shared keys are compromised,
-  no captured traffic can be decrypted anyway)
-* Mutual two-side authentication (noone will send real network interface
-  data unless the other side is authenticated)
-* Zero knowledge authentication (pre-shared key is not transmitted in
-  any form between the peers, not even it's hash value)
-* Built-in rehandshake and heartbeat features
+Development Git source code repository currently is located on:
+https://github.com/stargrave/govpn
 
-DESCRIPTION
+GoVPN is free software: see the file COPYING for copying conditions.
 
-All packets captured on network interface are encrypted, authenticated
-and sent to remote server, that writes them to his interface, and vice
-versa. Client and server use pre-shared authentication key (PSK).
-Because of stateless UDP nature, after some timeout of inactivity peers
-forget about each other and have to retry handshake process again,
-therefore background heartbeat process will be ran.
-
-Handshake is used to mutually authenticate peers, exchange common secret
-per-session encryption key and checks UDP transport availability.
-
-Because of UDP and authentication overhead: each packet grows in size
-during transmission, so you have to lower you maximum transmission unit
-(MTU) on network interface.
-
-High security is the goal for that daemon. It uses fast cryptography
-algorithms with 128bit security margin, strong mutual zero-knowledge
-authentication and perfect-forward secrecy property. An attacker can not
-know anything from captured traffic, even if pre-shared key is
-compromised. Rehandshake is performed by client every 4 GiB of
-transfered data.
-
-Also you can provide up and down scripts that will be executed after
-either connection is initiated (up-script in background), or is went
-down. The first argument for them is an interface name.
-
-CONSOLE OUTPUT LEGEND
-
-B -- bad or timeouted UDP packet (maybe network is inactive)
-T -- bad tag on packet (MiTM, unordered packet)
-R -- invalid sequence number (MiTM, unordered packet)
-[HS?] -- unknown handshake message
-w -- successful write to remote peer
-r -- successful read from remote peer
-[HS1], [HS2], [HS3], [HS4] -- handshake packet stage
-[rS?] -- invalid server's random authentication number received (MiTM, bad PSK)
-[rC?] -- invalid client's random authentication number received (MiTM, bad PSK)
-[S?] -- invalid handshake stage is trying to perform (MiTM, duplicate packet)
-[OK] -- handshake's stage passed
-
-EXAMPLE USAGE
-
-Let's assume that there is some insecure link between your computer and
-WiFi-reachable gateway. You have got preconfigured wlan0 network
-interface with 192.168.0/24 network. You want to create virtual
-encrypted and authenticated 172.16.0/24 network and use it as a default
-transport. MTU for that wlan0 is 1500 bytes. GoVPN will say that maximum
-MTU for the link is 1476, however it does not take in account TAP's
-Ethernet frame header length, that in my case is 14 bytes long (1476 - 14).
-
-    common% umask 066
-    common% echo MYLONG64HEXKEY > key.txt
-
-GNU/Linux IPv4 client-server example:
-
-    server% ip addr add 192.168.0.1/24 dev wlan0
-    server% tunctl -t tap10
-    server% ip link set mtu 1462 dev tap10
-    server% ip addr add 172.16.0.1/24 dev tap10
-    server% ip link set up dev tap10
-    server% govpn -key key.txt -iface tap10 -bind 192.168.0.1:1194
-
-    client% ip addr add 192.168.0.2/24 dev wlan0
-    client% tunctl -t tap10
-    client% ip link set mtu 1462 dev tap10
-    client% ip addr add 172.16.0.2/24 dev tap10
-    client% ip link set up dev tap10
-    client% ip route add default via 172.16.0.1
-    client% while :; do govpn -key key.txt -iface tap10 -remote 192.168.0.1:1194; done
-
-FreeBSD IPv6 client-server example:
-
-    server% ifconfig em0 inet6 fe80::1/64
-    server% ifconfig tap10 create
-    server% ifconfig tap10 inet6 fc00::1/96 mtu 1462 up
-    server% govpn -key key.txt -face tap10 -bind fe80::1%em0
-
-    client% ifconfig me0 inet6 -ifdisabled auto_linklocal
-    client% ifconfig tap10
-    client% ifconfig tap10 inet6 fc00::2/96 mtu 1462 up
-    client% route -6 add default fc00::1
-    client% while :; do govpn -key key.txt -iface tap10 -remote [fe80::1%me0]:1194; done
-
-Example up-script:
-
-    client% cat > up.sh <<EOF
-    #!/bin/sh
-    dhclient $1
-    rtsol $1
-    EOF
-    client% chmod +x up.sh
-    client% govpn -key key.txt -iface tap10 -remote [fe80::1%me0]:1194 -up ./up.sh
-
-If client won't finish handshake during -timeout, then it will exit.
-If no packets are received from remote side during timeout, then daemon
-will stop sending packets to the client and client will exit. In all
-cases you have to rehandshake again.
-
-TECHNICAL INTERNALS
-
-Nonce encryption: XTEA
-Encryption: Salsa20
-Message authentication: Poly1305
-Password authenticated key agreement: Curve25519 based DH-EKE
-Packet overhead: 24 bytes per packet
-Handshake overhead: 4 UDP (2 from client, 2 from server) packets,
-                    232 bytes total payload
-
-                           Transport protocol
-
-    ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA) + AUTH(ENCn(SERIAL) + ENC_DATA)
-
-Each transport message is indistinguishable from pseudo random noise.
-
-SERIAL is an encrypted message serial number. Odds are reserved for
-client(→server) messages, evens for server(→client) messages.
-
-ENCn is XTEA block cipher algorithm used here as PRP (pseudo random
-permutation) to randomize, obfuscate SERIAL. Plaintext 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. 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).
-
-Encrypted SERIAL is used as a nonce for DATA encryption: encryption key
-is different during each handshake, so (key, nonce) pair is always used
-only once.
-
-We generate Salsa20's output using this key and nonce for each message:
-* first 256 bits are used as a one-time key for Poly1305 authentication
-* next 256 bits of output are ignored
-* and all remaining ones XORed with the data, encrypting it
-
-                           Handshake protocol
-     ┌──────┐                                  ┌──────┐
-     │Client│                                  │Server│
-     └──┬───┘                                  └──┬───┘
-        │────┐
-        │    │ R=rand(64bit); CPrivKey=rand(256bit)
-        │<───┘
-        │                                         │
-        │         R, enc(PSK, R, CPubKey)         │
-        │ ────────────────────────────────────────>
-        │                                         │
-        │                                         │────┐
-        │                                         │    │ SPrivKey=rand(256bit)
-        │                                         │<───┘
-        │                                         │
-        │                                         │────┐
-        │                                         │    │ K=DH(SPrivKey, CPubKey)
-        │                                         │<───┘
-        │                                         │
-        │                                         │────┐
-        │                                         │    │ RS=rand(64bit); SS=rand(256bit)
-        │                                         │<───┘
-        │                                         │
-        │ enc(PSK, R+1, SPubKey); enc(K, R, RS+SS)│
-        │ <────────────────────────────────────────
-        │                                         │
-        │────┐                                    │
-        │    │ K=DH(CPrivKey, SPubKey)            │
-        │<───┘                                    │
-        │                                         │
-        │────┐                                    │
-        │    │ RC=rand(64bit); SC=rand(256bit)    │
-        │<───┘                                    │
-        │                                         │
-        │          enc(K, R+1, RS+RC+SC)          │
-        │ ────────────────────────────────────────>
-        │                                         │
-        │                                         │────┐
-        │                                         │    │ compare(RS)
-        │                                         │<───┘
-        │                                         │
-        │                                         │────┐
-        │                                         │    │ MasterKey=SS XOR SC
-        │                                         │<───┘
-        │                                         │
-        │             enc(K, 0x00, RC)            │
-        │ <────────────────────────────────────────
-        │                                         │
-        │────┐                                    │
-        │    │ compare(RC)                        │
-        │<───┘                                    │
-        │                                         │
-        │────┐                                    │
-        │    │ MasterKey=SS XOR SC                │
-        │<───┘                                    │
-     ┌──┴───┐                                  ┌──┴───┐
-     │Client│                                  │Server│
-     └──────┘                                  └──────┘
-
-* client generates CPubKey, random 64bit R that is used as a nonce
-  for encryption
-* R + enc(PSK, R, CPubKey) + NULLs -> Server [56 bytes]
-* server remembers clients address, decrypt CPubKey, generates
-  SPrivKey/SPubKey, computes common shared key K (based on
-  CPubKey and SPrivKey), generates 64bit random number RS and
-  256bit random SS. PSK-encryption uses incremented R (from previous
-  message) for nonce
-* enc(PSK, SPubKey) + enc(K, RS + SS) + NULLs -> Client [88 bytes]
-* client decrypt SPubKey, computes K, decrypts RS, SS with key K,
-  remembers SS, generates 64bit random number RC and 256bit random SC,
-* enc(K, RS + RC + SC) + NULLs -> Server [64 bytes]
-* server decrypt RS, RC, SC with key K, compares RS with it's own one
-  send before, computes final main encryption key S = SS XOR SC
-* ENC(K, RC) + NULLs -> Client [24 bytes]
-* server switches to the new client
-* client decrypts RC and compares with it's own generated one, computes
-  final main encryption key S
-
-Where PSK is 256bit pre-shared key, NULLs are 16 null-bytes. R* are
-required for handshake randomization and two-way authentication. K key
-is used only during handshake. NULLs are required to differentiate
-common transport protocol messages from handshake ones. DH public keys
-can be trivially derived from private ones.
-
-
-RELATED DOCUMENTS
-
-* http://cr.yp.to/ecdh.html
-* http://cr.yp.to/snuffle.html
-* http://cr.yp.to/mac.html
-* http://grouper.ieee.org/groups/1363/passwdPK/contributions/jablon.pdf
-* Applied Cryptography (C) 1996 Bruce Schneier
-
-TODO
-
-* Move decryption and encryption processes into goroutines
-* Add identity management (client can send it's identification, server has
-  on-disk id↔key plaintext database)
-* Implement alternative Secure Remote Password protocol (it is much slower,
-  technically has more code, but human memorized passwords can be used
-  instead of keys)
-
-LICENCE
-
-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
-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.
+For futher information please read either doc/govpn.info or doc/govpn.texi.
diff --git a/THANKS b/THANKS
new file mode 100644 (file)
index 0000000..c317254
--- /dev/null
+++ b/THANKS
@@ -0,0 +1,5 @@
+* Applied Cryptography (C) 1996 Bruce Schneier
+* http://grouper.ieee.org/groups/1363/passwdPK/contributions/jablon.pdf
+* http://cr.yp.to/ecdh.html
+* http://cr.yp.to/snuffle.html
+* http://cr.yp.to/mac.html
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..63d6ef3
--- /dev/null
+++ b/TODO
@@ -0,0 +1,6 @@
+* Increase performance
+* Implement rate-control
+* Probably implement alternative Secure Remote Password protocol to allow
+  human memorized passphrases to be used
+* Randomize ports usage
+* Fragmentize packets, noise the traffic
diff --git a/cmd/govpn-client/main.go b/cmd/govpn-client/main.go
new file mode 100644 (file)
index 0000000..e358d43
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2015 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, 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 <http://www.gnu.org/licenses/>.
+*/
+
+// Simple secure free software virtual private network daemon.
+package main
+
+import (
+       "flag"
+       "log"
+       "net"
+       "os"
+       "os/signal"
+
+       "govpn"
+)
+
+var (
+       remoteAddr = flag.String("remote", "", "Remote server address")
+       ifaceName  = flag.String("iface", "tap0", "TAP network interface")
+       IDRaw      = flag.String("id", "", "Client identification")
+       keyPath    = flag.String("key", "", "Path to authentication key file")
+       upPath     = flag.String("up", "", "Path to up-script")
+       downPath   = flag.String("down", "", "Path to down-script")
+       mtu        = flag.Int("mtu", 1500, "MTU")
+       nonceDiff  = flag.Int("noncediff", 1, "Allow nonce difference")
+       timeoutP   = flag.Int("timeout", 60, "Timeout seconds")
+)
+
+func main() {
+       flag.Parse()
+       timeout := *timeoutP
+       var err error
+       log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
+
+       govpn.MTU = *mtu
+       govpn.Timeout = timeout
+       govpn.Noncediff = *nonceDiff
+
+       id := govpn.IDDecode(*IDRaw)
+       key := govpn.KeyRead(*keyPath)
+       if id == nil {
+               panic("ID is not specified")
+       }
+
+       bind, err := net.ResolveUDPAddr("udp", "0.0.0.0:0")
+       if err != nil {
+               panic(err)
+       }
+       conn, err := net.ListenUDP("udp", bind)
+       if err != nil {
+               panic(err)
+       }
+       remote, err := net.ResolveUDPAddr("udp", *remoteAddr)
+       if err != nil {
+               panic(err)
+       }
+
+       tap, ethSink, ethReady, _, err := govpn.TAPListen(*ifaceName)
+       if err != nil {
+               panic(err)
+       }
+       udpSink, udpBuf, udpReady := govpn.ConnListen(conn)
+
+       timeouts := 0
+       firstUpCall := true
+       var peer *govpn.Peer
+       var ethPkt []byte
+       var udpPkt *govpn.UDPPkt
+       var udpPktData []byte
+
+       termSignal := make(chan os.Signal, 1)
+       signal.Notify(termSignal, os.Interrupt, os.Kill)
+
+       log.Println("Client version", govpn.Version)
+       log.Println("Starting handshake")
+       handshake := govpn.HandshakeStart(conn, remote, id, key)
+
+MainCycle:
+       for {
+               if peer != nil && peer.Bytes > govpn.MaxBytesPerKey {
+                       peer = nil
+                       handshake = govpn.HandshakeStart(conn, remote, id, key)
+                       log.Println("Rehandshaking")
+               }
+               select {
+               case <-termSignal:
+                       break MainCycle
+               case ethPkt = <-ethSink:
+                       if peer == nil {
+                               ethReady <- struct{}{}
+                               continue
+                       }
+                       peer.EthProcess(ethPkt, conn, ethReady)
+               case udpPkt = <-udpSink:
+                       timeouts++
+                       if timeouts >= timeout {
+                               break MainCycle
+                       }
+                       if udpPkt == nil {
+                               udpReady <- struct{}{}
+                               continue
+                       }
+
+                       udpPktData = udpBuf[:udpPkt.Size]
+                       if govpn.IsValidHandshakePkt(udpPktData) {
+                               if udpPkt.Addr.String() != remote.String() {
+                                       udpReady <- struct{}{}
+                                       log.Println("Unknown handshake message")
+                                       continue
+                               }
+                               if p := handshake.Client(conn, key, udpPktData); p != nil {
+                                       log.Println("Handshake completed")
+                                       if firstUpCall {
+                                               go govpn.ScriptCall(*upPath, *ifaceName)
+                                               firstUpCall = false
+                                       }
+                                       peer = p
+                                       handshake = nil
+                               }
+                               udpReady <- struct{}{}
+                               continue
+                       }
+                       if peer == nil {
+                               udpReady <- struct{}{}
+                               continue
+                       }
+                       if peer.UDPProcess(udpPktData, tap, udpReady) {
+                               timeouts = 0
+                       }
+               }
+       }
+       govpn.ScriptCall(*downPath, *ifaceName)
+}
diff --git a/cmd/govpn-server/main.go b/cmd/govpn-server/main.go
new file mode 100644 (file)
index 0000000..f628244
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2015 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, 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 <http://www.gnu.org/licenses/>.
+*/
+
+// Simple secure free software virtual private network daemon.
+package main
+
+import (
+       "bytes"
+       "flag"
+       "log"
+       "net"
+       "os"
+       "os/signal"
+       "path"
+       "time"
+
+       "govpn"
+)
+
+var (
+       bindAddr  = flag.String("bind", "[::]:1194", "Bind to address")
+       peersPath = flag.String("peers", "peers", "Path to peers keys directory")
+       mtu       = flag.Int("mtu", 1500, "MTU")
+       nonceDiff = flag.Int("noncediff", 1, "Allow nonce difference")
+       timeoutP  = flag.Int("timeout", 60, "Timeout seconds")
+)
+
+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)
+       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(*timeoutP)
+       var err error
+       log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
+
+       govpn.MTU = *mtu
+       govpn.Timeout = *timeoutP
+       govpn.Noncediff = *nonceDiff
+       govpn.PeersInit(*peersPath)
+
+       bind, err := net.ResolveUDPAddr("udp", *bindAddr)
+       if err != nil {
+               panic(err)
+       }
+       conn, err := net.ListenUDP("udp", bind)
+       if err != nil {
+               panic(err)
+       }
+       udpSink, udpBuf, udpReady := govpn.ConnListen(conn)
+
+       termSignal := make(chan os.Signal, 1)
+       signal.Notify(termSignal, os.Interrupt, os.Kill)
+
+       hsHeartbeat := time.Tick(timeout)
+       go func() { <-hsHeartbeat }()
+
+       var addr string
+       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)
+       var peerReady PeerReadyEvent
+       var udpPkt *govpn.UDPPkt
+       var udpPktData []byte
+       var ethEvent EthEvent
+       ethSink := make(chan EthEvent)
+
+       log.Println("Server version", govpn.Version)
+       log.Println("Server started")
+
+MainCycle:
+       for {
+               select {
+               case <-termSignal:
+                       break MainCycle
+               case <-hsHeartbeat:
+                       now := time.Now()
+                       for addr, hs := range states {
+                               if hs.LastPing.Add(timeout).Before(now) {
+                                       log.Println("Deleting handshake state", addr)
+                                       delete(states, addr)
+                               }
+                       }
+                       for addr, state := range peers {
+                               if state.peer.LastPing.Add(timeout).Before(now) {
+                                       log.Println("Deleting peer", state.peer)
+                                       delete(peers, addr)
+                                       downPath := path.Join(
+                                               govpn.PeersPath,
+                                               state.peer.Id.String(),
+                                               "down.sh",
+                                       )
+                                       go govpn.ScriptCall(downPath, state.tap.Name)
+                                       state.terminate <- struct{}{}
+                               }
+                       }
+               case peerReady = <-peerReadySink:
+                       for addr, state := range peers {
+                               if state.tap.Name != peerReady.iface {
+                                       continue
+                               }
+                               delete(peers, addr)
+                               state.terminate <- struct{}{}
+                               break
+                       }
+                       addr = peerReady.peer.Addr.String()
+                       state := NewPeerState(peerReady.peer, peerReady.iface)
+                       if state == nil {
+                               continue
+                       }
+                       peers[addr] = state
+                       delete(states, 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 _, exists := peers[ethEvent.peer.Addr.String()]; !exists {
+                               continue
+                       }
+                       ethEvent.peer.EthProcess(ethEvent.data, conn, ethEvent.ready)
+               case udpPkt = <-udpSink:
+                       if udpPkt == nil {
+                               udpReady <- struct{}{}
+                               continue
+                       }
+                       udpPktData = udpBuf[:udpPkt.Size]
+                       addr = udpPkt.Addr.String()
+                       if govpn.IsValidHandshakePkt(udpPktData) {
+                               state, exists = states[addr]
+                               if !exists {
+                                       state = govpn.HandshakeNew(udpPkt.Addr)
+                                       states[addr] = state
+                               }
+                               peer = state.Server(conn, udpPktData)
+                               if peer != nil {
+                                       log.Println("Peer handshake finished", peer)
+                                       if _, exists = peers[addr]; exists {
+                                               go func() {
+                                                       peerReadySink <- PeerReadyEvent{peer, peers[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}
+                                               }()
+                                       }
+                               }
+                               udpReady <- struct{}{}
+                               continue
+                       }
+                       peerState, exists = peers[addr]
+                       if !exists {
+                               udpReady <- struct{}{}
+                               continue
+                       }
+                       peerState.peer.UDPProcess(udpPktData, peerState.tap, udpReady)
+               }
+       }
+}
diff --git a/common.go b/common.go
new file mode 100644 (file)
index 0000000..b38f52b
--- /dev/null
+++ b/common.go
@@ -0,0 +1,72 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2015 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, 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 <http://www.gnu.org/licenses/>.
+*/
+
+package govpn
+
+import (
+       "bytes"
+       "encoding/hex"
+       "io/ioutil"
+       "log"
+       "os/exec"
+)
+
+var (
+       MTU       int
+       Timeout   int
+       Noncediff int
+       Version   string
+)
+
+// Call external program/script.
+// You have to specify path to it and (inteface name as a rule) something
+// that will be the first argument when calling it. Function will return
+// it's output and possible error.
+func ScriptCall(path, ifaceName string) ([]byte, error) {
+       if path == "" {
+               return nil, nil
+       }
+       cmd := exec.Command(path, ifaceName)
+       var out bytes.Buffer
+       cmd.Stdout = &out
+       err := cmd.Run()
+       result := out.Bytes()
+       if err != nil {
+               log.Println("Script error", path, err, string(result))
+       }
+       return result, err
+}
+
+// Read authentication key from the file.
+// Key is 64 hexadecimal chars long.
+func KeyRead(path string) *[KeySize]byte {
+       keyData, err := ioutil.ReadFile(path)
+       if err != nil {
+               panic("Unable to read keyfile: " + err.Error())
+       }
+       if len(keyData) < 64 {
+               panic("Key must be 64 hex characters long")
+       }
+       keyDecoded, err := hex.DecodeString(string(keyData[0:64]))
+       if err != nil {
+               panic("Unable to decode the key: " + err.Error())
+       }
+       key := new([KeySize]byte)
+       copy(key[:], keyDecoded)
+       return key
+}
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644 (file)
index 0000000..37a161d
--- /dev/null
@@ -0,0 +1,3 @@
+handshake.utxt
+govpn.info
+govpn.html
diff --git a/doc/download.texi b/doc/download.texi
new file mode 100644 (file)
index 0000000..3a72cdc
--- /dev/null
@@ -0,0 +1,11 @@
+You can obtain it's source code either by cloning development branches
+from Git repository: @code{git clone https://github.com/stargrave/govpn.git},
+or by downloading prepared tarballs below.
+
+@multitable {XXXXX} {XXXX KiB} {link sign}
+@headitem Version @tab Size @tab Tarball
+@item 1.5 @tab 19 KiB
+@tab @url{download/govpn-1.5.tar.xz, link} @url{download/govpn-1.5.tar.xz.sig, sign}
+@item 2.0 @tab 31 KiB
+@tab @url{download/govpn-2.0.tar.xz, link} @url{download/govpn-2.0.tar.xz.sig, sign}
+@end multitable
diff --git a/doc/govpn.texi b/doc/govpn.texi
new file mode 100644 (file)
index 0000000..e3aed5a
--- /dev/null
@@ -0,0 +1,410 @@
+\input texinfo
+@setfilename govpn.info
+@documentencoding UTF-8
+@settitle GoVPN
+
+@copying
+@quotation
+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
+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.
+@end quotation
+@end copying
+
+@ifnottex
+@node Top
+@top GoVPN
+
+This manual is for GoVPN -- simple secure free software
+virtual private network (VPN) daemon.
+@end ifnottex
+
+@menu
+* Overview::
+* News::
+* Getting source code::
+* User manual::
+* Developer manual::
+* Reporting bugs::
+* Copying conditions::
+@end menu
+
+@node Overview
+@unnumbered Overview
+
+GoVPN is simple secure virtual private network daemon. It uses
+Diffie-Hellman Encrypted Key Exchange (DH-EKE) for mutual zero-knowledge
+peers authentication and authenticated encrypted data transport.
+
+All packets captured on network interface are encrypted, authenticated
+and sent to remote server, that writes them to his interface, and vice
+versa. Client and server use pre-shared authentication key (PSK) and
+128-bit identification key.
+
+Because of stateless UDP nature, after some timeout of inactivity peers
+forget about each other and have to retry handshake process again,
+therefore background heartbeat process will be ran.
+
+Handshake is used to mutually authenticate peers, exchange common secret
+per-session encryption key and check UDP transport availability.
+
+Because of UDP and authentication overhead: each packet grows in size
+during transmission, so you have to lower you maximum transmission unit
+(MTU) on virtual network interface.
+
+High security is the goal for that daemon. It uses fast cryptography
+algorithms with 128bit security margin, strong mutual zero-knowledge
+authentication and perfect-forward secrecy property. An attacker can not
+know anything about payload (except it's size and time) from captured
+traffic, even if pre-shared key is compromised. Rehandshake is performed
+by client every 4 GiB of transfered data.
+
+Each client also has it's own identification key and server works with
+all of them independently. Identification key is not secret, but it is
+encrypted (obfuscated) during transmission.
+
+@itemize @bullet
+@item GNU/Linux and FreeBSD support
+@item IPv6 compatible
+@item Encrypted and authenticated transport
+@item Relatively fast handshake
+@item Replay attack protection
+@item
+Perfect forward secrecy (if long-term pre-shared keys are compromised,
+no captured traffic can be decrypted anyway)
+@item
+Mutual two-side authentication (noone will send real network interface
+data unless the other side is authenticated)
+@item
+Zero knowledge authentication (pre-shared key is not transmitted in
+any form between the peers, not even it's hash value)
+@item Built-in rehandshake and heartbeat features
+@item Several simultaneous clients support
+@end itemize
+
+@node News
+@unnumbered News
+
+@verbatiminclude ../NEWS
+
+@node Getting source code
+@unnumbered Getting source code
+
+GoVPN is written on Go programming language and depends on
+@code{golang.org/x/crypto} libraries.
+
+@include download.texi
+
+You @strong{have to} verify downloaded archives integrity and check
+their signature to be sure that you have got trusted, untampered
+software. For integrity and authentication of downloaded binaries
+@url{https://www.gnupg.org/, The GNU Privacy Guard} is used. You must
+download signature provided with the tarball and run for example:
+
+@example
+gpg --verify govpn-1.5.tar.xz.sig govpn-1.5.tar.xz
+@end example
+
+For the very first time you must also import signing public keys. They
+are provided below, but be sure that you are reading them from the
+trusted source. Alternatively check this page from other sources and
+look for the mailing list announcements.
+
+@verbatim
+pub   rsa2048/FFE2F4A1 2015-03-10
+uid       [ultimate] Sergey Matveev (GoVPN release signing key) <stargrave@stargrave.org>
+sub   rsa2048/8A6C750A 2015-03-10
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFT/H6cBCADTf/oqoTTBAA/CCQuYtzg8vrXxyjXj9yy4lTWqMSwgLXMm8br/
+kG0Jnk63oP3hggI3hm2mpuiNwpwrJiORLBZCe8JgZW71zG4LfhVpQeWd7fu8WxDx
+0uUZWByz5KcK8c/kNWNDpSkMmmqdE/8v0YDFbsz5U+ytp/Kki/gj3BCeIX3jYOL1
+fxczkv2okoU+aGYXt9z50VzheLUSRLzkkX8yNSpszqfB0LEEmUk8HO2fSS/bXwaY
+ZXX5//suH8V5hwq8vB8dHHCquZW6blyzcTa2KGIh6g2CmpypIQp/i5QAbzOCHKTM
+A1F7A1r0kYF2WfZOrycCfjUx3GA5B7sytuA3ABEBAAG0RFNlcmdleSBNYXR2ZWV2
+IChHb1ZQTiByZWxlYXNlIHNpZ25pbmcga2V5KSA8c3RhcmdyYXZlQHN0YXJncmF2
+ZS5vcmc+iQE8BBMBCAAmBQJU/x+nAhsDCAsKCQgHBAMCBxUKCQgLAwIFFgIBAwAC
+HgECF4AACgkQ8vWQRf/i9KEZ/AgAqYF/RRNwwhgLgFqTLfw3ha0FeiSso7H9ITDo
+cdJ/domLHaFvmwFIDQQKV8Zd1Rnj6xTCs2bq2O5hYMLrFZg85A9i5tLwkgFc9J5G
++8K3K/dh9Y4pArbM+craO+xydrwLyg1zlXCezthWbL0iXO/CuGiuBBCZJqRJ9HV4
+cZr4TRA3Znm5nt96rRsR86XqOgr0iOEDtYKfKW/IzDqOEgXUN5o2bUwuQawe9Y8d
+CngXzJcfb2eJ/TqSP9CxVWscjz4sAmD3/ECrHSjX7xsusIs46F2+VMlEXFuST52r
+zamfiGKlol8XvimUjKhlMWjqfdcJ0+jvFftsa7HXQUwRoQ1vJYheBBARCAAGBQJU
+/x/VAAoJEK4agQnkmFfvqn8A/ReK2ZZrnI9s0rzTsF1jrTZ1o5YowuINOzVMmLbE
+aYuGAP4iGwPgwVbANu4dWaP2N03oL4xFtmdaeNn3sB9ZqJOOyrkBDQRU/x+nAQgA
+uYBRyJVwhlE2SRIEmMggwr4gq1JBM2Ge5O46usf+YPUjCJKWoAj+MpQoq7r+oA/s
+E/6kGvWgngwV9prCdNkvcdwEWbb+n9PcMc2ZuIGRV3iOKYlYEBFV0bfM9zEV2jar
+1YQ+J/48UX7R00cYJuXel7Dy77V9eNd+Ukyowm93fggFlBDBGBjVbNtfIorHNYjB
+01CCu3i/8yxrMyFRvMKyAVEGp3obgmlam4DNkNIhFMv3du0tFnDFBsZf7N0kbLWI
+xEEJoc/jxaezDytQpUr3RhlMsLV6N/jjIZuy36QO1sbFeOe2to0E7ixaFzNCWsqY
+cxUfnJ3wi7hOiOwE2PF3tQARAQABiQEfBBgBCAAJBQJU/x+nAhsMAAoJEPL1kEX/
+4vShrVcIAKLUwMn7WgK6thmwPjdwP5V/jTlsWLWk2O/LEN4W/R0mw2hRsgRG/8Sz
+qlAP6vfl7ERaWuyL+fp72rKnGTGU9CEvn6PKmaG7bi4tGEvWXscNc10r0leIAP63
+pkQOa6Nyx2axJlJdSuTsYetd1ZgNpHNng+lxSUBlkPMOhPd/P/Ok7DShZjd2jhQ1
+jUbjWn+P7ARGEvgdd5utNjy/RaSwrLG8NXj3I+XuksG0/TPeG0zu9NOPzWZq9sCc
+5VbDNJTYtsMFs1etHE95Efmx6yUquQyB+g/HgvkH/LzthBawVVHxZNzzHgc6KN5w
+E0itJPXMaQL+juUfiNM0i2R1O8nJo14=
+=LJzj
+-----END PGP PUBLIC KEY BLOCK-----
+@end verbatim
+
+@node User manual
+@unnumbered User manual
+
+GoVPN is split into two pieces: client and server. Each of them work on
+top of UDP and TAP virtual network interfaces. Client and server have
+several common configuration command line options:
+
+@table @asis
+@item Timeout
+Because of stateless UDP nature there is no way to know if
+remote peer is dead, but after some timeout. Client and server
+heartbeats each other every third part of heartbeat. Also this timeout
+is the time when server purge his obsolete handshake and peers states.
+@item Allowable nonce difference
+To prevent replay attacks we just remembers
+latest received nonce number from the remote peer and drops those who
+has lower ones. Because UDP packets can be reordered during: that
+behaviour can lead to dropping of not replayed ones. This options gives
+ability to create some window of allows difference. That opens the door
+for replay attacks for narrow time interval.
+@item MTU
+Maximum transmission unit.
+@end table
+
+Client needs to know his identification, path to the authentication key,
+remote server's address, TAP interface name, and optional path to up and
+down scripts, that will be executed after connection is either initiated
+or terminated.
+
+Server needs to know only the address to listen on and path to directory
+containing peers information. This directory must contain subdirectories
+with the names equal to client's identifications. Each of them must have
+key file with corresponding authentication key, up.sh script that has to
+print interface's name on the first line and optional down.sh.
+
+@menu
+* Example usage::
+@end menu
+
+@node Example usage
+@section Example usage
+
+Let's assume that there is some insecure link between your computer and
+WiFi-reachable gateway. You have got preconfigured @code{wlan0} network
+interface with 192.168.0/24 network. You want to create virtual
+encrypted and authenticated 172.16.0/24 network and use it as a default
+transport. MTU for that wlan0 is 1500 bytes. GoVPN will say that maximum
+MTU for the link is 1476, however it does not take in account TAP's
+Ethernet frame header length, that in my case is 14 bytes long (1476 - 14).
+
+GNU/Linux IPv4 client-server example:
+
+@example
+server% mkdir -p peers/CLIENTID
+server% umask 066
+server% echo MYLONG64HEXKEY > peers/CLIENTID/key
+server% echo "#!/bin/sh" > peers/CLIENTID/up.sh
+server% echo "echo tap10" > peers/CLIENTID/up.sh
+server% chmod 500 peers/CLIENTID/up.sh
+server% ip addr add 192.168.0.1/24 dev wlan0
+server% tunctl -t tap10
+server% ip link set mtu 1462 dev tap10
+server% ip addr add 172.16.0.1/24 dev tap10
+server% ip link set up dev tap10
+server% govpn -bind 192.168.0.1:1194
+@end example
+
+@example
+client% umask 066
+client% echo MYLONG64HEXKEY > key.txt
+client% ip addr add 192.168.0.2/24 dev wlan0
+client% tunctl -t tap10
+client% ip link set mtu 1462 dev tap10
+client% ip addr add 172.16.0.2/24 dev tap10
+client% ip link set up dev tap10
+client% ip route add default via 172.16.0.1
+client% while :; do
+    govpn -key key.txt -id CLIENTID -iface tap10 -remote 192.168.0.1:1194
+done
+@end example
+
+FreeBSD IPv6 client-server example:
+
+@example
+server% mkdir -p peers/CLIENTID
+server% umask 066
+server% echo MYLONG64HEXKEY > peers/CLIENTID/key
+server% echo "#!/bin/sh" > 
+server% cat > peers/CLIENTID/up.sh <<EOF
+#!/bin/sh
+$tap=$(ifconfig tap create)
+ifconfig $tap inet6 fc00::1/96 mtu 1462 up
+echo $tap
+EOF
+server% chmod 500 peers/CLIENTID/up.sh
+server% ifconfig em0 inet6 fe80::1/64
+server% govpn -bind fe80::1%em0
+@end example
+
+@example
+client% ifconfig me0 inet6 -ifdisabled auto_linklocal
+client% ifconfig tap10
+client% ifconfig tap10 inet6 fc00::2/96 mtu 1462 up
+client% route -6 add default fc00::1
+client% while :; do
+    govpn -key key.txt -id CLIENTID -iface tap10 -remote [fe80::1%me0]:1194
+done
+@end example
+
+Example up-script:
+
+@example
+client% cat > up.sh <<EOF
+#!/bin/sh
+dhclient $1
+rtsol $1
+EOF
+client% chmod +x up.sh
+client% govpn -id CLIENTID -key key.txt -iface tap10 -remote [fe80::1%me0]:1194 -up ./up.sh
+@end example
+
+Client will exit if won't finish handshake during @code{-timeout}.
+If no packets are received from remote side during timeout, then daemon
+will stop sending packets to the client and client will exit. In all
+cases you have to rehandshake again.
+
+@node Developer manual
+@unnumbered Developer manual
+
+@table @asis
+@item Nonce and identification encryption
+XTEA
+@item Data encryption
+Salsa20
+@item Message authentication
+Poly1305
+@item Password authenticated key agreement
+Curve25519 based DH-EKE
+@item Packet overhead
+24 bytes per packet
+@item Handshake overhead
+4 UDP (2 from client, 2 from server) packets, 240 bytes total payload
+@end table
+
+@menu
+* Transport protocol::
+* Handshake protocol::
+@end menu
+
+@node Transport protocol
+@section Transport protocol
+
+@verbatim
+ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA) +
+    AUTH(ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA))
+@end verbatim
+
+Each transport message is indistinguishable from pseudo random noise.
+
+@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
+random permutation) to randomize, 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. 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).
+
+Encrypted @code{SERIAL} is used as a nonce for @code{DATA} encryption:
+encryption key is different during each handshake, so (key, nonce) pair
+is always used only once. @code{ENC} is Salsa20 cipher, with established
+session @code{KEY} and encrypted @code{SERIAL} used as a nonce.
+
+@code{AUTH} is Poly1305 authentication function. First 256 bits of
+Salsa20 output are used as a one-time key for @code{AUTH}. Next 256 bits
+of Salsa20 are ignored. All remaining output is XORed with the data,
+encrypting it.
+
+To prevent replay attacks we remember latest @code{SERIAL} from the
+remote peer. If received message's @code{SERIAL} is not greater that the
+saved one, then drop it. Optionally, because some UDP packets can be
+reordered during transmission, we can allow some window for valid
+serials with the @code{-noncediff} option. @code{-noncediff 10} with
+current saved serial state equals to 78 allows messages with 68…78
+serials. That time window can be used by attacker to replay packets, so
+by default it equals to 1. However it can improve performance because of
+rearranged UDP packets.
+
+@node Handshake protocol
+@section Handshake protocol
+
+@verbatiminclude handshake.utxt
+
+@enumerate
+@item
+client generates @code{CPubKey}, random 64bit @code{R} that is used as a
+nonce for encryption, and an encrypted @code{R} with XTEA, where the key
+equals to client's identity
+@item
+@verb{|R + enc(PSK, R, CPubKey) + xtea(ID, R) + NULL + NULLs -> Server|} [65 bytes]
+@item
+server remembers clients address, decrypt @code{CPubKey}, generates
+@code{SPrivKey}/@code{SPubKey}, computes common shared key @code{K}
+(based on @code{CPubKey} and @code{SPrivKey}), generates 64bit random
+number @code{RS} and 256bit random @code{SS}. PSK-encryption uses
+incremented @code{R} (from previous message) for nonce
+@item
+@verb{|enc(PSK, SPubKey) + enc(K, RS + SS) + NULLs -> Client|} [88 bytes]
+@item
+client decrypt @code{SPubKey}, computes @code{K}, decrypts @code{RS},
+@code{SS} with key @code{K}, remembers @code{SS}, generates 64bit random
+number @code{RC} and 256bit random @code{SC},
+@item
+@verb{|enc(K, RS + RC + SC) + NULLs -> Server|} [64 bytes]
+@item
+server decrypt @code{RS}, @code{RC}, @code{SC} with key @code{K},
+compares @code{RS} with it's own one send before, computes final main
+encryption key @code{S = SS XOR SC}
+@item
+@verb{|ENC(K, RC) + NULLs -> Client|} [24 bytes]
+@item
+server switches to the new client
+@item
+client decrypts @code{RC} and compares with it's own generated one,
+computes final main encryption key @code{S}
+@end enumerate
+
+Where PSK is 256bit pre-shared key, @code{NULLs} are 16 null-bytes.
+@code{R*} are required for handshake randomization and two-way
+authentication. K key is used only during handshake. @code{NULLs} are
+required to differentiate common transport protocol messages from
+handshake ones. DH public keys can be trivially derived from private
+ones.
+
+@node Reporting bugs
+@unnumbered Reporting bugs
+
+Please send all your bug requests, patches and related questions to
+@email{govpn-devel@@lists.cypherpunks.ru} mailing list.
+Visit @url{https://lists.cypherpunks.ru/mailman/listinfo/govpn-devel}
+for information about subscription options and archived messages access.
+
+Development Git source code repository currently is located on:
+@url{https://github.com/stargrave/govpn}.
+
+@node Copying conditions
+@unnumbered Copying conditions
+
+@insertcopying
+
+@bye
diff --git a/doc/handshake.txt b/doc/handshake.txt
new file mode 100644 (file)
index 0000000..d946833
--- /dev/null
@@ -0,0 +1,21 @@
+@startuml
+participant Client
+participant Server
+
+Client -> Client : R=rand(64bit)
+Client -> Client : CPrivKey=rand(256bit)
+Client -> Server : R, enc(PSK, R, CPubKey), xtea(ID, R)
+Server -> Server : SPrivKey=rand(256bit)
+Server -> Server : K=DH(SPrivKey, CPubKey)
+Server -> Server : RS=rand(64bit)
+Server -> Server : SS=rand(256bit)
+Server -> Client : enc(PSK, R+1, SPubKey); enc(K, R, RS+SS)
+Client -> Client : K=DH(CPrivKey, SPubKey)
+Client -> Client : RC=rand(64bit); SC=rand(256bit)
+Client -> Server : enc(K, R+1, RS+RC+SC)
+Server -> Server : compare(RS)
+Server -> Server : MasterKey=SS XOR SC
+Server -> Client : enc(K, 0x00, RC)
+Client -> Client : compare(RC)
+Client -> Client : MasterKey=SS XOR SC
+@enduml
diff --git a/doc/makefile b/doc/makefile
new file mode 100644 (file)
index 0000000..aa886a9
--- /dev/null
@@ -0,0 +1,10 @@
+all: govpn.info govpn.html
+
+govpn.info: govpn.texi handshake.utxt
+       makeinfo govpn.texi
+
+handshake.utxt: handshake.txt
+       plantuml -tutxt handshake.txt
+
+govpn.html: govpn.texi handshake.utxt
+       makeinfo --html -o govpn.html govpn.texi
index 6f662c60ca208d5d4a8bce9d69ce1dbc0382ae65..9a7b1143c97c1daac5fb9f2ef26ccadba3b344ca 100644 (file)
--- a/govpn.go
+++ b/govpn.go
@@ -1,5 +1,5 @@
 /*
-govpn -- simple secure virtual private network daemon
+GoVPN -- simple secure free software virtual private network daemon
 Copyright (C) 2014-2015 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -16,343 +16,5 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-// Simple secure virtual private network daemon
-package main
-
-import (
-       "bytes"
-       "encoding/binary"
-       "encoding/hex"
-       "flag"
-       "fmt"
-       "io"
-       "io/ioutil"
-       "log"
-       "net"
-       "os"
-       "os/exec"
-       "os/signal"
-       "time"
-
-       "golang.org/x/crypto/poly1305"
-       "golang.org/x/crypto/salsa20"
-       "golang.org/x/crypto/xtea"
-)
-
-var (
-       remoteAddr = flag.String("remote", "", "Remote server address")
-       bindAddr   = flag.String("bind", "", "Bind to address")
-       ifaceName  = flag.String("iface", "tap0", "TAP network interface")
-       keyPath    = flag.String("key", "", "Path to authentication key file")
-       upPath     = flag.String("up", "", "Path to up-script")
-       downPath   = flag.String("down", "", "Path to down-script")
-       mtu        = flag.Int("mtu", 1500, "MTU")
-       nonceDiff  = flag.Int("noncediff", 1, "Allow nonce difference")
-       timeoutP   = flag.Int("timeout", 60, "Timeout seconds")
-       verboseP   = flag.Bool("v", false, "Increase verbosity")
-)
-
-const (
-       NonceSize = 8
-       KeySize   = 32
-       // S20BS is Salsa20's internal blocksize in bytes
-       S20BS         = 64
-       HeartBeatSize = 12
-       HeartBeatMark = "\x00\x00\x00HEARTBEAT"
-       // Maximal amount of bytes transfered with single key (4 GiB)
-       MaxBytesPerKey = 4294967296
-)
-
-type TAP interface {
-       io.Reader
-       io.Writer
-}
-
-type Peer struct {
-       addr        *net.UDPAddr
-       key         *[KeySize]byte // encryption key
-       nonceOur    uint64         // nonce for our messages
-       nonceRecv   uint64         // latest received nonce from remote peer
-       nonceCipher *xtea.Cipher   // nonce cipher
-}
-
-type UDPPkt struct {
-       addr *net.UDPAddr
-       size int
-}
-
-func ScriptCall(path *string) {
-       if *path == "" {
-               return
-       }
-       cmd := exec.Command(*path, *ifaceName)
-       var out bytes.Buffer
-       cmd.Stdout = &out
-       if err := cmd.Run(); err != nil {
-               fmt.Println(time.Now(), "script error: ", err.Error(), string(out.Bytes()))
-       }
-}
-
-func main() {
-       flag.Parse()
-       timeout := *timeoutP
-       verbose := *verboseP
-       noncediff := uint64(*nonceDiff)
-       var err error
-       log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
-
-       // Key decoding
-       keyData, err := ioutil.ReadFile(*keyPath)
-       if err != nil {
-               panic("Unable to read keyfile: " + err.Error())
-       }
-       if len(keyData) < 64 {
-               panic("Key must be 64 hex characters long")
-       }
-       keyDecoded, err := hex.DecodeString(string(keyData[0:64]))
-       if err != nil {
-               panic("Unable to decode the key: " + err.Error())
-       }
-       key := new([KeySize]byte)
-       copy(key[:], keyDecoded)
-       keyDecoded = nil
-       keyData = nil
-
-       // Interface listening
-       maxIfacePktSize := *mtu - poly1305.TagSize - NonceSize
-       log.Println("Max MTU", maxIfacePktSize, "on interface", *ifaceName)
-       iface := NewTAP(*ifaceName)
-       ethBuf := make([]byte, maxIfacePktSize)
-       ethSink := make(chan int)
-       ethSinkReady := make(chan bool)
-       go func() {
-               var n int
-               var err error
-               for {
-                       <-ethSinkReady
-                       n, err = iface.Read(ethBuf)
-                       if err != nil {
-                               panic(err)
-                       }
-                       ethSink <- n
-               }
-       }()
-       ethSinkReady <- true
-
-       // Network address parsing
-       if (len(*bindAddr) > 1 && len(*remoteAddr) > 1) ||
-               (len(*bindAddr) == 0 && len(*remoteAddr) == 0) {
-               panic("Either -bind or -remote must be specified only")
-       }
-       var conn *net.UDPConn
-       var remote *net.UDPAddr
-       serverMode := false
-       bindTo := "0.0.0.0:0"
-
-       if len(*bindAddr) > 1 {
-               bindTo = *bindAddr
-               serverMode = true
-       }
-
-       bind, err := net.ResolveUDPAddr("udp", bindTo)
-       if err != nil {
-               panic(err)
-       }
-       conn, err = net.ListenUDP("udp", bind)
-       if err != nil {
-               panic(err)
-       }
-
-       if len(*remoteAddr) > 1 {
-               remote, err = net.ResolveUDPAddr("udp", *remoteAddr)
-               if err != nil {
-                       panic(err)
-               }
-       }
-
-       udpBuf := make([]byte, *mtu)
-       udpSink := make(chan *UDPPkt)
-       udpSinkReady := make(chan bool)
-       go func(conn *net.UDPConn) {
-               var n int
-               var addr *net.UDPAddr
-               var err error
-               for {
-                       <-udpSinkReady
-                       conn.SetReadDeadline(time.Now().Add(time.Second))
-                       n, addr, err = conn.ReadFromUDP(udpBuf)
-                       if err != nil {
-                               if verbose {
-                                       fmt.Print("B")
-                               }
-                               udpSink <- nil
-                       } else {
-                               udpSink <- &UDPPkt{addr, n}
-                       }
-               }
-       }(conn)
-       udpSinkReady <- true
-
-       // Process packets
-       var udpPkt *UDPPkt
-       var udpPktData []byte
-       var ethPktSize int
-       var frame []byte
-       var dataToSend []byte
-       var addr string
-       var peer *Peer
-       var p *Peer
-       var nonceRecv uint64
-
-       timeouts := 0
-       bytes := 0
-       states := make(map[string]*Handshake)
-       nonce := make([]byte, NonceSize)
-       keyAuth := new([KeySize]byte)
-       tag := new([poly1305.TagSize]byte)
-       buf := make([]byte, *mtu+S20BS)
-       emptyKey := make([]byte, KeySize)
-
-       if !serverMode {
-               states[remote.String()] = HandshakeStart(conn, remote, key)
-       }
-
-       heartbeat := time.Tick(time.Second * time.Duration(timeout/3))
-       go func() { <-heartbeat }()
-       heartbeatMark := []byte(HeartBeatMark)
-
-       termSignal := make(chan os.Signal, 1)
-       signal.Notify(termSignal, os.Interrupt, os.Kill)
-
-       finished := false
-       for {
-               if finished {
-                       break
-               }
-               if !serverMode && bytes > MaxBytesPerKey {
-                       states[remote.String()] = HandshakeStart(conn, remote, key)
-                       bytes = 0
-               }
-               select {
-               case <-termSignal:
-                       finished = true
-               case <-heartbeat:
-                       go func() { ethSink <- -1 }()
-               case udpPkt = <-udpSink:
-                       timeouts++
-                       if !serverMode && timeouts >= timeout {
-                               finished = true
-                       }
-                       if udpPkt == nil {
-                               udpSinkReady <- true
-                               continue
-                       }
-                       udpPktData = udpBuf[:udpPkt.size]
-                       if isValidHandshakePkt(udpPktData) {
-                               addr = udpPkt.addr.String()
-                               state, exists := states[addr]
-                               if serverMode {
-                                       if !exists {
-                                               state = &Handshake{addr: udpPkt.addr}
-                                               states[addr] = state
-                                       }
-                                       p = state.Server(noncediff, conn, key, udpPktData)
-                               } else {
-                                       if !exists {
-                                               fmt.Print("[HS?]")
-                                               udpSinkReady <- true
-                                               continue
-                                       }
-                                       p = state.Client(noncediff, conn, key, udpPktData)
-                               }
-                               if p != nil {
-                                       fmt.Print("[HS-OK]")
-                                       if peer == nil {
-                                               go ScriptCall(upPath)
-                                       }
-                                       peer = p
-                                       delete(states, addr)
-                               }
-                               udpSinkReady <- true
-                               continue
-                       }
-                       if peer == nil {
-                               udpSinkReady <- true
-                               continue
-                       }
-                       copy(buf[:KeySize], emptyKey)
-                       copy(tag[:], udpPktData[udpPkt.size-poly1305.TagSize:])
-                       copy(buf[S20BS:], udpPktData[NonceSize:udpPkt.size-poly1305.TagSize])
-                       salsa20.XORKeyStream(
-                               buf[:S20BS+udpPkt.size-poly1305.TagSize],
-                               buf[:S20BS+udpPkt.size-poly1305.TagSize],
-                               udpPktData[:NonceSize],
-                               peer.key,
-                       )
-                       copy(keyAuth[:], buf[:KeySize])
-                       if !poly1305.Verify(tag, udpPktData[:udpPkt.size-poly1305.TagSize], keyAuth) {
-                               udpSinkReady <- true
-                               fmt.Print("T")
-                               continue
-                       }
-                       peer.nonceCipher.Decrypt(buf, udpPktData[:NonceSize])
-                       nonceRecv, _ = binary.Uvarint(buf[:NonceSize])
-                       if nonceRecv < peer.nonceRecv-noncediff {
-                               fmt.Print("R")
-                               udpSinkReady <- true
-                               continue
-                       }
-                       udpSinkReady <- true
-                       peer.nonceRecv = nonceRecv
-                       timeouts = 0
-                       frame = buf[S20BS : S20BS+udpPkt.size-NonceSize-poly1305.TagSize]
-                       bytes += len(frame)
-                       if string(frame[0:HeartBeatSize]) == HeartBeatMark {
-                               continue
-                       }
-                       if _, err = iface.Write(frame); err != nil {
-                               log.Println("Error writing to iface: ", err)
-                       }
-                       if verbose {
-                               fmt.Print("r")
-                       }
-               case ethPktSize = <-ethSink:
-                       if ethPktSize > maxIfacePktSize {
-                               panic("Too large packet on interface")
-                       }
-                       if peer == nil {
-                               ethSinkReady <- true
-                               continue
-                       }
-
-                       peer.nonceOur = peer.nonceOur + 2
-                       for i := 0; i < NonceSize; i++ {
-                               nonce[i] = '\x00'
-                       }
-                       binary.PutUvarint(nonce, peer.nonceOur)
-                       peer.nonceCipher.Encrypt(nonce, nonce)
-
-                       copy(buf[:KeySize], emptyKey)
-                       if ethPktSize > -1 {
-                               copy(buf[S20BS:], ethBuf[:ethPktSize])
-                               ethSinkReady <- true
-                       } else {
-                               copy(buf[S20BS:], heartbeatMark)
-                               ethPktSize = HeartBeatSize
-                       }
-                       salsa20.XORKeyStream(buf, buf, nonce, peer.key)
-                       copy(buf[S20BS-NonceSize:S20BS], nonce)
-                       copy(keyAuth[:], buf[:KeySize])
-                       dataToSend = buf[S20BS-NonceSize : S20BS+ethPktSize]
-                       poly1305.Sum(tag, dataToSend, keyAuth)
-                       bytes += len(dataToSend)
-                       if _, err = conn.WriteTo(append(dataToSend, tag[:]...), peer.addr); err != nil {
-                               log.Println("Error sending UDP", err)
-                       }
-                       if verbose {
-                               fmt.Print("w")
-                       }
-               }
-       }
-       ScriptCall(downPath)
-}
+// Simple secure free software virtual private network daemon
+package govpn
index 9faf9ec9d3abfc91f6c5cfb5a4bf4c282c87c483..c458f5d41e90d25b6961d9d043b2d1ece7dbde87 100644 (file)
@@ -1,5 +1,5 @@
 /*
-govpn -- Simple secure virtual private network daemon
+GoVPN -- simple secure free software virtual private network daemon
 Copyright (C) 2014-2015 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -16,14 +16,15 @@ 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 main
+package govpn
 
 import (
        "crypto/rand"
        "crypto/subtle"
        "encoding/binary"
-       "fmt"
+       "log"
        "net"
+       "path"
        "time"
 
        "golang.org/x/crypto/curve25519"
@@ -35,17 +36,18 @@ import (
 
 type Handshake struct {
        addr     *net.UDPAddr
-       lastPing time.Time
+       LastPing time.Time
+       Id       PeerId
        rNonce   *[8]byte
-       dhPriv   *[32]byte // own private DH key
-       key      *[32]byte // handshake encryption key
-       rServer  *[8]byte  // random string for authentication
+       dhPriv   *[32]byte      // own private DH key
+       key      *[KeySize]byte // handshake encryption key
+       rServer  *[8]byte       // random string for authentication
        rClient  *[8]byte
        sServer  *[32]byte // secret string for main key calculation
        sClient  *[32]byte
 }
 
-func KeyFromSecrets(server, client []byte) *[32]byte {
+func keyFromSecrets(server, client []byte) *[KeySize]byte {
        k := new([32]byte)
        for i := 0; i < 32; i++ {
                k[i] = server[i] ^ client[i]
@@ -53,19 +55,9 @@ func KeyFromSecrets(server, client []byte) *[32]byte {
        return k
 }
 
-func NewNonceCipher(key *[32]byte) *xtea.Cipher {
-       nonceKey := make([]byte, 16)
-       salsa20.XORKeyStream(nonceKey, make([]byte, 32), make([]byte, 8), key)
-       ciph, err := xtea.NewCipher(nonceKey)
-       if err != nil {
-               panic(err)
-       }
-       return ciph
-}
-
-// Check if it is valid handshake-related message
-// Minimal size and last 16 zero bytes
-func isValidHandshakePkt(pkt []byte) bool {
+// Check if it is valid handshake-related message.
+// Minimal size and last 16 zero bytes.
+func IsValidHandshakePkt(pkt []byte) bool {
        if len(pkt) < 24 {
                return false
        }
@@ -99,10 +91,22 @@ func dhKeyGen(priv, pub *[32]byte) *[32]byte {
        return key
 }
 
-func HandshakeStart(conn *net.UDPConn, addr *net.UDPAddr, key *[32]byte) *Handshake {
-       state := Handshake{}
-       state.addr = addr
-       state.lastPing = time.Now()
+// Create new handshake state.
+func HandshakeNew(addr *net.UDPAddr) *Handshake {
+       state := Handshake{
+               addr:     addr,
+               LastPing: time.Now(),
+       }
+       return &state
+}
+
+// Start handshake's procedure from the client.
+// It is the entry point for starting the handshake procedure.
+// You have to specify outgoing conn address, remote's addr address,
+// our own identification and an encryption key. First handshake packet
+// will be sent immediately.
+func HandshakeStart(conn *net.UDPConn, addr *net.UDPAddr, id *PeerId, key *[32]byte) *Handshake {
+       state := HandshakeNew(addr)
 
        state.dhPriv = dhPrivGen()
        dhPub := new([32]byte)
@@ -115,23 +119,47 @@ func HandshakeStart(conn *net.UDPConn, addr *net.UDPAddr, key *[32]byte) *Handsh
        enc := make([]byte, 32)
        salsa20.XORKeyStream(enc, dhPub[:], state.rNonce[:], key)
 
-       if _, err := conn.WriteTo(
-               append(state.rNonce[:],
-                       append(enc, make([]byte, poly1305.TagSize)...)...), addr); err != nil {
+       ciph, err := xtea.NewCipher(id[:])
+       if err != nil {
+               panic(err)
+       }
+       rEnc := make([]byte, xtea.BlockSize)
+       ciph.Encrypt(rEnc, state.rNonce[:])
+
+       data := append(state.rNonce[:], enc...)
+       data = append(data, rEnc...)
+       data = append(data, '\x00')
+       data = append(data, make([]byte, poly1305.TagSize)...)
+
+       if _, err := conn.WriteTo(data, addr); err != nil {
                panic(err)
        }
-       return &state
+       return state
 }
 
-func (h *Handshake) Server(noncediff uint64, conn *net.UDPConn, key *[32]byte, data []byte) *Peer {
+// Process handshake message on the server side.
+// This function is intended to be called on server's side.
+// Our outgoing conn connection and received data are required.
+// If this is the final handshake message, then new Peer object
+// will be created and used as a transport. If no mutually
+// authenticated Peer is ready, then return nil.
+func (h *Handshake) Server(conn *net.UDPConn, data []byte) *Peer {
        switch len(data) {
-       case 56: // R + ENC(PSK, dh_client_pub) + NULLs
-               fmt.Print("[HS1]")
+       case 65: // R + ENC(PSK, dh_client_pub) + xtea(ID, R) + NULL + NULLs
                if h.rNonce != nil {
-                       fmt.Print("[S?]")
+                       log.Println("Invalid handshake stage from", h.addr)
                        return nil
                }
 
+               // Try to determine client's ID
+               id := IDsCache.Find(data[:8], data[8+32:8+32+8])
+               if id == nil {
+                       log.Println("Unknown identity from", h.addr)
+                       return nil
+               }
+               key := KeyRead(path.Join(PeersPath, id.String(), "key"))
+               h.Id = *id
+
                // Generate private DH key
                h.dhPriv = dhPrivGen()
                dhPub := new([32]byte)
@@ -167,19 +195,18 @@ func (h *Handshake) Server(noncediff uint64, conn *net.UDPConn, key *[32]byte, d
                                append(encRs, make([]byte, poly1305.TagSize)...)...), h.addr); err != nil {
                        panic(err)
                }
-               fmt.Print("[OK]")
+               h.LastPing = time.Now()
        case 64: // ENC(K, RS + RC + SC) + NULLs
-               fmt.Print("[HS3]")
                if (h.rNonce == nil) || (h.rClient != nil) {
-                       fmt.Print("[S?]")
+                       log.Println("Invalid handshake stage from", h.addr)
                        return nil
                }
 
-               // Decrypt Rs compare rServer
+               // Decrypted Rs compare rServer
                decRs := make([]byte, 8+8+32)
                salsa20.XORKeyStream(decRs, data[:8+8+32], h.rNonceNext(), h.key)
-               if res := subtle.ConstantTimeCompare(decRs[:8], h.rServer[:]); res != 1 {
-                       fmt.Print("[rS?]")
+               if subtle.ConstantTimeCompare(decRs[:8], h.rServer[:]) != 1 {
+                       log.Println("Invalid server's random number with", h.addr)
                        return nil
                }
 
@@ -191,27 +218,28 @@ func (h *Handshake) Server(noncediff uint64, conn *net.UDPConn, key *[32]byte, d
                }
 
                // Switch peer
-               peer := Peer{
-                       addr:      h.addr,
-                       nonceOur:  noncediff + 0,
-                       nonceRecv: noncediff + 0,
-                       key:       KeyFromSecrets(h.sServer[:], decRs[8+8:]),
-               }
-               peer.nonceCipher = NewNonceCipher(peer.key)
-               fmt.Print("[OK]")
-               return &peer
+               peer := newPeer(h.addr, h.Id, 0, keyFromSecrets(h.sServer[:], decRs[8+8:]))
+               h.LastPing = time.Now()
+               return peer
        default:
-               fmt.Print("[HS?]")
+               log.Println("Invalid handshake message from", h.addr)
        }
        return nil
 }
 
-func (h *Handshake) Client(noncediff uint64, conn *net.UDPConn, key *[32]byte, data []byte) *Peer {
+// Process handshake message on the client side.
+// This function is intended to be called on client's side.
+// Our outgoing conn connection, authentication key and received data
+// are required. Client does not work with identities, as he is the
+// only one, so key is a requirement.
+// If this is the final handshake message, then new Peer object
+// will be created and used as a transport. If no mutually
+// authenticated Peer is ready, then return nil.
+func (h *Handshake) Client(conn *net.UDPConn, key *[KeySize]byte, data []byte) *Peer {
        switch len(data) {
        case 88: // ENC(PSK, dh_server_pub) + ENC(K, RS + SS) + NULLs
-               fmt.Print("[HS2]")
                if h.key != nil {
-                       fmt.Print("[S?]")
+                       log.Println("Invalid handshake stage from", h.addr)
                        return nil
                }
 
@@ -246,34 +274,27 @@ func (h *Handshake) Client(noncediff uint64, conn *net.UDPConn, key *[32]byte, d
                if _, err := conn.WriteTo(append(encRs, make([]byte, poly1305.TagSize)...), h.addr); err != nil {
                        panic(err)
                }
-               fmt.Print("[OK]")
+               h.LastPing = time.Now()
        case 24: // ENC(K, RC) + NULLs
-               fmt.Print("[HS4]")
                if h.key == nil {
-                       fmt.Print("[S?]")
+                       log.Println("Invalid handshake stage from", h.addr)
                        return nil
                }
 
                // Decrypt rClient
                dec := make([]byte, 8)
                salsa20.XORKeyStream(dec, data[:8], make([]byte, 8), h.key)
-               if res := subtle.ConstantTimeCompare(dec, h.rClient[:]); res != 1 {
-                       fmt.Print("[rC?]")
+               if subtle.ConstantTimeCompare(dec, h.rClient[:]) != 1 {
+                       log.Println("Invalid client's random number with", h.addr)
                        return nil
                }
 
                // Switch peer
-               peer := Peer{
-                       addr:      h.addr,
-                       nonceOur:  noncediff + 1,
-                       nonceRecv: noncediff + 0,
-                       key:       KeyFromSecrets(h.sServer[:], h.sClient[:]),
-               }
-               peer.nonceCipher = NewNonceCipher(peer.key)
-               fmt.Print("[OK]")
-               return &peer
+               peer := newPeer(h.addr, h.Id, 1, keyFromSecrets(h.sServer[:], h.sClient[:]))
+               h.LastPing = time.Now()
+               return peer
        default:
-               fmt.Print("[HS?]")
+               log.Println("Invalid handshake message from", h.addr)
        }
        return nil
 }
diff --git a/identify.go b/identify.go
new file mode 100644 (file)
index 0000000..8547e60
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2015 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, 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 <http://www.gnu.org/licenses/>.
+*/
+
+package govpn
+
+import (
+       "crypto/subtle"
+       "encoding/hex"
+       "log"
+       "os"
+
+       "golang.org/x/crypto/xtea"
+)
+
+const (
+       IDSize = 128 / 8
+)
+
+type PeerId [IDSize]byte
+
+func (id PeerId) String() string {
+       return hex.EncodeToString(id[:])
+}
+
+type cipherCache map[PeerId]*xtea.Cipher
+
+var (
+       PeersPath string
+       IDsCache  cipherCache
+)
+
+// Initialize (pre-cache) available peers info.
+func PeersInit(path string) {
+       PeersPath = path
+       IDsCache = make(map[PeerId]*xtea.Cipher)
+       IDsCache.refresh()
+}
+
+// Refresh IDsCache: remove disappeared keys, add missing ones with
+// initialized ciphers.
+func (cc cipherCache) refresh() {
+       dir, err := os.Open(PeersPath)
+       if err != nil {
+               panic(err)
+       }
+       peerIds, err := dir.Readdirnames(0)
+       if err != nil {
+               panic(err)
+       }
+       available := make(map[PeerId]bool)
+       for _, peerId := range peerIds {
+               id := IDDecode(peerId)
+               if id == nil {
+                       continue
+               }
+               available[*id] = true
+       }
+       // Cleanup deleted ones from cache
+       for k, _ := range cc {
+               if _, exists := available[k]; !exists {
+                       delete(cc, k)
+                       log.Println("Cleaning key: ", k)
+               }
+       }
+       // Add missing ones
+       for peerId, _ := range available {
+               if _, exists := cc[peerId]; !exists {
+                       log.Println("Adding key", peerId)
+                       cipher, err := xtea.NewCipher(peerId[:])
+                       if err != nil {
+                               panic(err)
+                       }
+                       cc[peerId] = cipher
+               }
+       }
+}
+
+// Try to find peer's identity (that equals to an encryption key)
+// by providing cipher and plain texts.
+func (cc cipherCache) Find(plaintext, ciphertext []byte) *PeerId {
+       cc.refresh()
+       buf := make([]byte, xtea.BlockSize)
+       for pid, cipher := range cc {
+               cipher.Decrypt(buf, ciphertext)
+               if subtle.ConstantTimeCompare(buf, plaintext) == 1 {
+                       ppid := PeerId(pid)
+                       return &ppid
+               }
+       }
+       return nil
+}
+
+// Decode identification string.
+// It must be 32 hexadecimal characters long.
+// If it is not the valid one, then return nil.
+func IDDecode(raw string) *PeerId {
+       if len(raw) != IDSize*2 {
+               return nil
+       }
+       idDecoded, err := hex.DecodeString(raw)
+       if err != nil {
+               return nil
+       }
+       idP := new([IDSize]byte)
+       copy(idP[:], idDecoded)
+       id := PeerId(*idP)
+       return &id
+}
diff --git a/makedist.sh b/makedist.sh
new file mode 100755 (executable)
index 0000000..9b2ac21
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/sh -ex
+
+cur=$(pwd)
+tmp=$(mktemp -d)
+release=$1
+[ -n "$release" ]
+git clone . $tmp/govpn-$release
+cd $tmp/govpn-$release
+git checkout $release
+rm -fr .git
+find . -name .gitignore -delete
+echo > doc/download.texi
+make -C doc
+cd $tmp
+tar cvf govpn-"$release".tar govpn-"$release"
+xz -9 govpn-"$release".tar
+gpg --detach-sign --sign --local-user FFE2F4A1 govpn-"$release".tar.xz
+mv $tmp/govpn-"$release".tar.xz $tmp/govpn-"$release".tar.xz.sig $cur/doc/govpn.html/download
diff --git a/makefile b/makefile
new file mode 100644 (file)
index 0000000..d6bed1c
--- /dev/null
+++ b/makefile
@@ -0,0 +1,17 @@
+.PHONY: govpn-client govpn-server
+
+VERSION=2.0
+LDFLAGS=-X govpn.Version $(VERSION)
+
+all: govpn-client govpn-server
+
+dependencies:
+       go get golang.org/x/crypto/poly1305
+       go get golang.org/x/crypto/salsa20
+       go get golang.org/x/crypto/xtea
+
+govpn-client: dependencies
+       go build -ldflags "$(LDFLAGS)" govpn/cmd/govpn-client
+
+govpn-server: dependencies
+       go build -ldflags "$(LDFLAGS)" govpn/cmd/govpn-server
diff --git a/tap.go b/tap.go
new file mode 100644 (file)
index 0000000..eabc924
--- /dev/null
+++ b/tap.go
@@ -0,0 +1,68 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2015 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, 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 <http://www.gnu.org/licenses/>.
+*/
+
+package govpn
+
+import (
+       "io"
+       "log"
+
+       "golang.org/x/crypto/poly1305"
+)
+
+type TAP struct {
+       Name  string
+       dev   io.ReadWriteCloser
+       buf   []byte
+       sink  chan []byte
+       ready chan struct{}
+}
+
+func NewTAP(ifaceName string) (*TAP, error) {
+       maxIfacePktSize := MTU - poly1305.TagSize - NonceSize
+       tapRaw, err := newTAPer(ifaceName)
+       if err != nil {
+               return nil, err
+       }
+       tap := TAP{
+               Name:  ifaceName,
+               dev:   tapRaw,
+               buf:   make([]byte, maxIfacePktSize),
+               sink:  make(chan []byte),
+               ready: make(chan struct{}),
+       }
+       go func() {
+               var n int
+               var err error
+               for {
+                       <-tap.ready
+                       n, err = tap.dev.Read(tap.buf)
+                       if err != nil {
+                               panic(err)
+                       }
+                       tap.sink <- tap.buf[:n]
+               }
+       }()
+       return &tap, nil
+}
+
+func (t *TAP) Write(data []byte) {
+       if _, err := t.dev.Write(data); err != nil {
+               log.Println("Error writing to iface: ", err)
+       }
+}
index 197df484e397e0b6388a88bc04575a51706782a6..547fb06d44afafab9c5362aa5de7197fd80a7ed7 100644 (file)
@@ -1,21 +1,18 @@
 // +build freebsd
 
 /*
-govpn -- Simple secure virtual private network daemon
+GoVPN -- simple secure free software virtual private network daemon
 Copyright (C) 2014-2015 Sergey Matveev <stargrave@stargrave.org>
 */
 
-package main
+package govpn
 
 import (
+       "io"
        "os"
        "path"
 )
 
-func NewTAP(ifaceName string) TAP {
-       fd, err := os.OpenFile(path.Join("/dev/", ifaceName), os.O_RDWR, os.ModePerm)
-       if err != nil {
-               panic(err)
-       }
-       return fd
+func newTAPer(ifaceName string) (io.ReadWriteCloser, error) {
+       return os.OpenFile(path.Join("/dev/", ifaceName), os.O_RDWR, os.ModePerm)
 }
index 6fc4265a42919371ac965a8c984ca5ce199bb9d2..938a3b5056e73647ff46628e0b9acb1ad4b20321 100644 (file)
@@ -1,20 +1,18 @@
 // +build linux
 
 /*
-govpn -- Simple secure virtual private network daemon
+GoVPN -- simple secure free software virtual private network daemon
 Copyright (C) 2014-2015 Sergey Matveev <stargrave@stargrave.org>
 */
 
-package main
+package govpn
 
 import (
+       "io"
+
        "github.com/chon219/water"
 )
 
-func NewTAP(string ifaceName) TAP {
-       iface, err := water.NewTAP(ifaceName)
-       if err != nil {
-               panic(err)
-       }
-       return iface
+func newTAPer(string ifaceName) (io.ReadWriteCloser, error) {
+       return water.NewTAP(ifaceName)
 }
diff --git a/transport.go b/transport.go
new file mode 100644 (file)
index 0000000..3c460e6
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+GoVPN -- simple secure free software virtual private network daemon
+Copyright (C) 2014-2015 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, 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 <http://www.gnu.org/licenses/>.
+*/
+
+package govpn
+
+import (
+       "crypto/subtle"
+       "encoding/binary"
+       "log"
+       "net"
+       "time"
+
+       "golang.org/x/crypto/poly1305"
+       "golang.org/x/crypto/salsa20"
+       "golang.org/x/crypto/xtea"
+)
+
+const (
+       NonceSize = 8
+       KeySize   = 32
+       // S20BS is Salsa20's internal blocksize in bytes
+       S20BS         = 64
+       HeartbeatSize = 12
+       // Maximal amount of bytes transfered with single key (4 GiB)
+       MaxBytesPerKey = 4294967296
+)
+
+type UDPPkt struct {
+       Addr *net.UDPAddr
+       Size int
+}
+
+type Peer struct {
+       Addr        *net.UDPAddr
+       Id          PeerId
+       Key         *[KeySize]byte // encryption key
+       NonceOur    uint64         // nonce for our messages
+       NonceRecv   uint64         // latest received nonce from remote peer
+       NonceCipher *xtea.Cipher   // nonce cipher
+       LastPing    time.Time
+       LastSent    time.Time
+       buf         []byte
+       tag         *[poly1305.TagSize]byte
+       keyAuth     *[KeySize]byte
+       nonceRecv   uint64
+       Bytes       int
+       frame       []byte
+       nonce       []byte
+}
+
+func (p *Peer) String() string {
+       return p.Id.String() + ":" + p.Addr.String()
+}
+
+var (
+       HeartbeatMark   = []byte("\x00\x00\x00HEARTBEAT")
+       Emptiness       = make([]byte, KeySize)
+       taps            = make(map[string]*TAP)
+       heartbeatPeriod *time.Duration
+)
+
+func heartbeatPeriodGet() time.Duration {
+       if heartbeatPeriod == nil {
+               period := time.Second * time.Duration(Timeout/4)
+               heartbeatPeriod = &period
+       }
+       return *heartbeatPeriod
+}
+
+// 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) (*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{})
+
+       go func() {
+               heartbeat := time.Tick(heartbeatPeriodGet())
+               var pkt []byte
+       ListenCycle:
+               for {
+                       select {
+                       case <-sinkTerminate:
+                               break ListenCycle
+                       case <-heartbeat:
+                               sink <- make([]byte, 0)
+                               continue
+                       case <-sinkReady:
+                               if exists {
+                                       exists = false
+                                       break
+                               }
+                               tap.ready <- struct{}{}
+                       }
+               HeartbeatCatched:
+                       select {
+                       case <-heartbeat:
+                               sink <- make([]byte, 0)
+                               goto HeartbeatCatched
+                       case <-sinkTerminate:
+                               break ListenCycle
+                       case pkt = <-tap.sink:
+                               sink <- pkt
+                       }
+               }
+               close(sink)
+       }()
+       sinkReady <- struct{}{}
+       return tap, sink, sinkReady, sinkTerminate, nil
+}
+
+// Create UDP listening goroutine.
+// This function takes already listening UDP socket and a buffer where
+// all UDP packet data will be saved, channel where information about
+// remote address and number of written bytes are stored, and a channel
+// used to tell that buffer is ready to be overwritten.
+func ConnListen(conn *net.UDPConn) (chan *UDPPkt, []byte, chan struct{}) {
+       buf := make([]byte, MTU)
+       sink := make(chan *UDPPkt)
+       sinkReady := make(chan struct{})
+       go func(conn *net.UDPConn) {
+               var n int
+               var addr *net.UDPAddr
+               var err error
+               for {
+                       <-sinkReady
+                       conn.SetReadDeadline(time.Now().Add(time.Second))
+                       n, addr, err = conn.ReadFromUDP(buf)
+                       if err != nil {
+                               // This is needed for ticking the timeouts counter outside
+                               sink <- nil
+                               continue
+                       }
+                       sink <- &UDPPkt{addr, n}
+               }
+       }(conn)
+       sinkReady <- struct{}{}
+       return sink, buf, sinkReady
+}
+
+func newNonceCipher(key *[KeySize]byte) *xtea.Cipher {
+       nonceKey := make([]byte, 16)
+       salsa20.XORKeyStream(nonceKey, make([]byte, KeySize), make([]byte, xtea.BlockSize), key)
+       ciph, err := xtea.NewCipher(nonceKey)
+       if err != nil {
+               panic(err)
+       }
+       return ciph
+}
+
+func newPeer(addr *net.UDPAddr, id PeerId, nonce int, key *[KeySize]byte) *Peer {
+       peer := Peer{
+               Addr:        addr,
+               LastPing:    time.Now(),
+               Id:          id,
+               NonceOur:    uint64(Noncediff + nonce),
+               NonceRecv:   uint64(Noncediff + 0),
+               Key:         key,
+               NonceCipher: newNonceCipher(key),
+               Bytes:       0,
+               buf:         make([]byte, MTU+S20BS),
+               tag:         new([poly1305.TagSize]byte),
+               keyAuth:     new([KeySize]byte),
+               nonce:       make([]byte, NonceSize),
+       }
+       return &peer
+}
+
+// Process incoming UDP packet.
+// udpPkt is received data, related to the peer tap interface and
+// 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) UDPProcess(udpPkt []byte, tap *TAP, ready chan struct{}) bool {
+       size := len(udpPkt)
+       copy(p.buf[:KeySize], Emptiness)
+       copy(p.tag[:], udpPkt[size-poly1305.TagSize:])
+       copy(p.buf[S20BS:], udpPkt[NonceSize:size-poly1305.TagSize])
+       salsa20.XORKeyStream(
+               p.buf[:S20BS+size-poly1305.TagSize],
+               p.buf[:S20BS+size-poly1305.TagSize],
+               udpPkt[:NonceSize],
+               p.Key,
+       )
+       copy(p.keyAuth[:], p.buf[:KeySize])
+       if !poly1305.Verify(p.tag, udpPkt[:size-poly1305.TagSize], p.keyAuth) {
+               ready <- struct{}{}
+               return false
+       }
+       p.NonceCipher.Decrypt(p.buf, udpPkt[:NonceSize])
+       p.nonceRecv, _ = binary.Uvarint(p.buf[:NonceSize])
+       if int(p.NonceRecv)-Noncediff >= 0 && int(p.nonceRecv) < int(p.NonceRecv)-Noncediff {
+               ready <- struct{}{}
+               return false
+       }
+       ready <- struct{}{}
+       p.LastPing = time.Now()
+       p.NonceRecv = p.nonceRecv
+       p.frame = p.buf[S20BS : S20BS+size-NonceSize-poly1305.TagSize]
+       p.Bytes += len(p.frame)
+       if subtle.ConstantTimeCompare(p.frame[:HeartbeatSize], HeartbeatMark) == 1 {
+               return true
+       }
+       tap.Write(p.frame)
+       return true
+}
+
+// Process incoming Ethernet packet.
+// ethPkt is received data, conn is our outgoing connection.
+// 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(ethPkt []byte, conn *net.UDPConn, ready chan struct{}) {
+       now := time.Now()
+       size := len(ethPkt)
+       // If this heartbeat is necessary
+       if size == 0 && !p.LastSent.Add(heartbeatPeriodGet()).Before(now) {
+               return
+       }
+       copy(p.buf[:KeySize], Emptiness)
+       if size > 0 {
+               copy(p.buf[S20BS:], ethPkt)
+               ready <- struct{}{}
+       } else {
+               copy(p.buf[S20BS:], HeartbeatMark)
+               size = HeartbeatSize
+       }
+
+       p.NonceOur = p.NonceOur + 2
+       copy(p.nonce, Emptiness)
+       binary.PutUvarint(p.nonce, p.NonceOur)
+       p.NonceCipher.Encrypt(p.nonce, p.nonce)
+
+       salsa20.XORKeyStream(p.buf, p.buf, p.nonce, p.Key)
+       copy(p.buf[S20BS-NonceSize:S20BS], p.nonce)
+       copy(p.keyAuth[:], p.buf[:KeySize])
+       p.frame = p.buf[S20BS-NonceSize : S20BS+size]
+       poly1305.Sum(p.tag, p.frame, p.keyAuth)
+
+       p.Bytes += len(p.frame)
+       p.LastSent = now
+       if _, err := conn.WriteTo(append(p.frame, p.tag[:]...), p.Addr); err != nil {
+               log.Println("Error sending UDP", err)
+       }
+}