config files, macOS support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
Justfile
|
Justfile
|
||||||
|
*.conf
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -5,18 +5,52 @@ A hub-and-spoke VPN for IPv6
|
|||||||
## Features
|
## Features
|
||||||
* **Post-quantum security:** Uses [HPKE](https://datatracker.ietf.org/doc/html/rfc9180/)([XWingMLKEM768X25519](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem/), [HKDF-SHA-256](https://en.wikipedia.org/wiki/HKDF)) + [XChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305#XChaCha20-Poly1305_%E2%80%93_extended_nonce_variant) for encryption and [ML-DSA-44](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf) for signatures.
|
* **Post-quantum security:** Uses [HPKE](https://datatracker.ietf.org/doc/html/rfc9180/)([XWingMLKEM768X25519](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem/), [HKDF-SHA-256](https://en.wikipedia.org/wiki/HKDF)) + [XChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305#XChaCha20-Poly1305_%E2%80%93_extended_nonce_variant) for encryption and [ML-DSA-44](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf) for signatures.
|
||||||
* **NAT-resistant architecture:** Any device with Internet access can easily join the network.
|
* **NAT-resistant architecture:** Any device with Internet access can easily join the network.
|
||||||
* **Cross-platform-ish:** Tested on Linux and Windows. macOS should probably work too.
|
* **Cross-platform:** Tested on Linux, Windows and macOS.
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
See [proto.go](https://git.ton1.dev/toni/baalvpn/src/branch/main/shared/proto.go) for details
|
See [proto.go](https://git.ton1.dev/toni/baalvpn/src/branch/main/shared/proto.go) for details
|
||||||
|
|
||||||
### Known limitations
|
### Known limitations
|
||||||
* Compromised server could give out fake pubkeys allowing a MITM attack and decrypting the peer-to-peer traffic
|
* A compromised server could give out fake pubkeys allowing a MITM attack and decrypting the peer-to-peer traffic
|
||||||
* The keys are rotated only on client and server restart
|
* The keys are rotated only on client and server restart
|
||||||
* The protocol is vulnerable to replay attacks
|
* The protocol is vulnerable to replay attacks
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
* [Go](https://go.dev/dl/) 1.26.3+
|
||||||
|
|
||||||
|
### Building
|
||||||
|
```sh
|
||||||
|
go build -o baalvpn-server ./server
|
||||||
|
|
||||||
|
CGO_ENABLED=0 go build -o baalvpn-client ./client
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
1. Generate a keypair:
|
||||||
|
```
|
||||||
|
./baalvpn-server keygen
|
||||||
|
```
|
||||||
|
This creates `server.conf` and `client.conf`.
|
||||||
|
|
||||||
|
2. Run:
|
||||||
|
```
|
||||||
|
sudo ./baalvpn-server server.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client
|
||||||
|
1. Copy `client.conf` (generated by keygen) and ensure `SERVER_ADDR` points to the server.
|
||||||
|
|
||||||
|
2. Run:
|
||||||
|
```
|
||||||
|
sudo ./baalvpn-client client.conf
|
||||||
|
```
|
||||||
|
The client will be assigned an IPv6 address from the `fd00:baa1::/32` range.
|
||||||
|
|
||||||
## Third-party dependencies
|
## Third-party dependencies
|
||||||
* [`github.com/songgao/water`](https://github.com/songgao/water) - wrapper around TUN interfaces for \*nix
|
* [`github.com/songgao/water`](https://github.com/songgao/water) - wrapper around TUN interfaces for \*nix
|
||||||
* [`golang.zx2c4.com/wintun`](https://git.zx2c4.com/wintun-go) - wrapper around `wintun.dll`
|
* [`golang.zx2c4.com/wintun`](https://git.zx2c4.com/wintun-go) - wrapper around `wintun.dll`
|
||||||
* [`github.com/cloudflare/circl`](https://github.com/cloudflare/circl) - used for ML-DSA, hopefully [not for long](https://github.com/golang/go/issues/77626)
|
* [`github.com/cloudflare/circl`](https://github.com/cloudflare/circl) - used for ML-DSA, but [only until next Go release](https://go.dev/doc/go1.27#crypto_mldsa)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,21 +18,22 @@ import (
|
|||||||
"github.com/cloudflare/circl/sign/mldsa/mldsa44"
|
"github.com/cloudflare/circl/sign/mldsa/mldsa44"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: parse some sort of config+key file
|
type Config struct {
|
||||||
const (
|
ListenAddr *net.UDPAddr
|
||||||
SERVER_IP = "172.20.12.47"
|
ServerAddr *net.UDPAddr
|
||||||
SERVER_PUBKEY = "b137ffedfaf45cb4dc5339373cdc935fbcc7597dde71b86112d282c2bda4c252e6676490acd3f3fd71a7ab7ce2edbe69521a966fa1e91056c8cd9f98443d49505c4a3663b90c322da0942e7511f9c600e0ebbeac071ba3763f623bfd2c2092123c0b2807518d70b790e2612b1b2daa32aa8fa14be8aef9072771e7c4e8c21b64173a504f92378804dcef63430b5c2d4a504d95a766b23ff04e804baf36b515d8c4116e9ffe5fa9afb1c2d005936b3e7222de604e8bae04fd5c56520d8e9dc428e6f6107fd735fcf54a7a324d8236d831b89b4fcda426d701add2b1f4122ba29021d263b74eea6ce1a546daab62b31b12c5b2f14a57ed1160af8630e1d2fa551e7609022a240ad487c76ef1c5b3632c7ca9d5c3648e8b0eaab2256ff5245b0dc25decd9271126b4834b13ddb365e75ad4411e73058460499c9a5522182007378aca990da89f87c90df379112586c90f42a2fe7ec1ab119fb95641abe61f7a04f95f2bd2a849cf01f561bcd4d5f4c57fff30e34e317bdbcba57126b068ded038a7947b6fd8501f2362fba7a695e628647ff28123b8a75df59da7b7b6a8064f6781fd70e62e86208942dc2fbe3a08999bc6b7e493dcf247d3679804c21ac0e64afd906d50b433a858ba6b55cfbb6668c5a4a7814345659756560a4e5678201a2cde4ca3c101520e0d1a0b06af5de5a596793e7bd68cdc45d2234e79b646fbb305096c193f053c6f158a0b1d2d590f43ba9236bb77000e1de75b2eb9693185bd82ac414db63450ea4808752f14946b8609d4b54c909b6854c8b2e822897e2497671102f41366808cd962802a128a551d6c56cccafb133c12294c14d97f02bc808c4a6777f1f602fe248d6d7ec6e850431d593a0510b3e5f75e19cafd3317b9951a12870d3ffdb6f4d0449ff63cce9a2acfd7eafc47befdd506b8b781133e4e4d364b816a072fa05b350368a62a61e4f63377897f579e21c67520c0edf949033210d7785a2435de37a0d609661b71d442bb11f8f9322f2df8dc52662de48299c8e3d82ab75124cb91e03f2d776ae33a6aed94d2eefa7a20d1fb9932c2511aa48304f703316e6397373418c1496b8be06cc97018af7bc43bf94cf5012c7486e251bfff3e275e759f7e1ca15afe8c1934c2a151d8cdd8c028fc67d3c6a22514d1aea6b907f130eda14860c559dee0dd88202ba2e00f5848777c2767bf96817c6b195c2556b4ce223397649c35f311de79c8687e3eec956a054bb36eb3024e520ec6d6455b12d44434ec4c41f7bcdd5133dc884de56ab77a8b1f50a613a5fbf90de87d30469dbc6d3c79b87e9a04a986bd00aa213e9cf270a37b50be919dcb999776f7928a7b43d700eeb6c92eaf901047607cb1d05839374aaa4eb49e8e7e4a69dc70a9b5b006284d72afa9a8f6a96473db5aff3c2f2ae2377a80570d2cd052cb2302d66c5115277673c9634117748c0ddf08315b17229a2ddaf238feb46270278de8d45b0c603a07b890d0f443f96c97a66bb2c783f43807e466e90aab185bcfc9e2036d1d745402c05913ee5bf380a99e5912612b10f39fa0dadefd2e2c47c67e4c118ea2d0579d35793fc2151bcbc40ebaa4e42b86231da9e952238d55327c8f9b96aec5bb1c4b283a6c67d0e0e715e754d8df84740920c002060781ac61662645ae3c92b4d79c28e7cc5e5103ff3b760e9d1feabed95b606275a57e1a1dc91a88505b41d1a4df434891d8e3f8531820a6397a5815c17dfde6e115061898f13611ecede2a1db94d2900359015e675244493f08228f95e36d9c004c0cb0405c2caed5cb31bd88800a6bdac7b53de67b36c2ea92ddd8496bafb0c304f458b28ef51217"
|
ServerPubKey mldsa44.PublicKey
|
||||||
)
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
config Config
|
||||||
|
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
serverAddr *net.UDPAddr
|
|
||||||
internalIP net.IP
|
internalIP net.IP
|
||||||
privKey hpke.PrivateKey
|
privKey hpke.PrivateKey
|
||||||
multicastKey []byte
|
multicastKey []byte
|
||||||
challengeCh = make(chan []byte, 1)
|
|
||||||
authSecret []byte
|
authSecret []byte
|
||||||
|
|
||||||
|
challengeCh = make(chan []byte, 1)
|
||||||
establishMutex sync.Mutex
|
establishMutex sync.Mutex
|
||||||
peerPubKeys = shared.NewTMap[string, hpke.PublicKey]()
|
peerPubKeys = shared.NewTMap[string, hpke.PublicKey]()
|
||||||
peerSessionKeys = shared.NewTMap[string, []byte]()
|
peerSessionKeys = shared.NewTMap[string, []byte]()
|
||||||
@@ -38,31 +41,59 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: baalvpn-client <configPath>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseConfig()
|
||||||
|
|
||||||
if !isAdmin() {
|
if !isAdmin() {
|
||||||
panic("this program must be ran with administrative privileges")
|
panic("this program must be ran with administrative privileges")
|
||||||
}
|
}
|
||||||
|
|
||||||
listenAddr, err := net.ResolveUDPAddr("udp", ":38000")
|
var err error
|
||||||
if err != nil {
|
conn, err = net.ListenUDP("udp", config.ListenAddr)
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
conn, err = net.ListenUDP("udp", listenAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
serverAddr, err = net.ResolveUDPAddr("udp", SERVER_IP+":38000")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go receivePackets()
|
go receivePackets()
|
||||||
register()
|
register()
|
||||||
|
|
||||||
<-make(chan int) // block forever
|
<-make(chan int) // block forever
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseConfig() {
|
||||||
|
conf, err := shared.ParseConfFile(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pubKeyBytes, err := hex.DecodeString(conf["SERVER_PUBKEY"])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var serverPubKey mldsa44.PublicKey
|
||||||
|
if err := serverPubKey.UnmarshalBinary(pubKeyBytes); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
listenAddr, err := net.ResolveUDPAddr("udp", conf["LISTEN_ADDR"])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverAddr, err := net.ResolveUDPAddr("udp", conf["SERVER_ADDR"])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
config = Config{
|
||||||
|
ListenAddr: listenAddr,
|
||||||
|
ServerAddr: serverAddr,
|
||||||
|
ServerPubKey: serverPubKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func register() {
|
func register() {
|
||||||
var err error
|
var err error
|
||||||
privKey, err = hpke.MLKEM768X25519().GenerateKey()
|
privKey, err = hpke.MLKEM768X25519().GenerateKey()
|
||||||
@@ -101,7 +132,7 @@ func receivePackets() {
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if addr.String() != serverAddr.String() {
|
if addr.String() != config.ServerAddr.String() {
|
||||||
log.Println("non-server connection rejected")
|
log.Println("non-server connection rejected")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -338,21 +369,12 @@ func buildAuthPkt(pktType uint16, parts ...[]byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func send(req []byte) {
|
func send(req []byte) {
|
||||||
if _, err := conn.WriteToUDP(req, serverAddr); err != nil {
|
if _, err := conn.WriteToUDP(req, config.ServerAddr); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyPkt(pkt []byte) bool {
|
func verifyPkt(pkt []byte) bool {
|
||||||
pubKeyBytes, err := hex.DecodeString(SERVER_PUBKEY)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // fatal misconfiguration
|
|
||||||
}
|
|
||||||
var pubKey mldsa44.PublicKey
|
|
||||||
if err := pubKey.UnmarshalBinary(pubKeyBytes); err != nil {
|
|
||||||
panic(err) // fatal misconfiguration
|
|
||||||
}
|
|
||||||
|
|
||||||
// this ignores version and packet type, fine for now
|
// this ignores version and packet type, fine for now
|
||||||
return mldsa44.Verify(&pubKey, pkt[2424:], nil, pkt[4:2424])
|
return mldsa44.Verify(&config.ServerPubKey, pkt[2424:], nil, pkt[4:2424])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ func setupInterface() {
|
|||||||
config := water.Config{DeviceType: water.TUN}
|
config := water.Config{DeviceType: water.TUN}
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
config.Name = "baalvpn"
|
config.Name = "baalvpn"
|
||||||
} else {
|
|
||||||
panic("TODO")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -41,6 +39,9 @@ func setupInterface() {
|
|||||||
if err := exec.Command("ifconfig", iface.Name(), "inet6", internalIP.String(), internalIP.String(), "prefixlen", "128", "up").Run(); err != nil {
|
if err := exec.Command("ifconfig", iface.Name(), "inet6", internalIP.String(), internalIP.String(), "prefixlen", "128", "up").Run(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if err := exec.Command("route", "-n", "add", "-inet6", "fd00:baa1::/32", "-interface", iface.Name()).Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
31
shared/confparser.go
Normal file
31
shared/confparser.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseConfFile(path string) (map[string]string, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := map[string]string{}
|
||||||
|
|
||||||
|
lines := strings.SplitSeq(strings.TrimSpace(string(data)), "\n")
|
||||||
|
for line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("expected '=' when parsing %s", path)
|
||||||
|
}
|
||||||
|
out[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ const PROTO_VERSION uint16 = 1
|
|||||||
// SYM = XChaCha20-Poly1305 with nonce prepended
|
// SYM = XChaCha20-Poly1305 with nonce prepended
|
||||||
// HMAC = HMAC-SHA-256
|
// HMAC = HMAC-SHA-256
|
||||||
// hpke = HPKE(MLKEM768X25519, ChaCha20Poly1305, HKDF-SHA-256)
|
// hpke = HPKE(MLKEM768X25519, ChaCha20Poly1305, HKDF-SHA-256)
|
||||||
// ds = ML-DSA
|
// ds = ML-DSA-44
|
||||||
|
|
||||||
// packet format
|
// packet format
|
||||||
// [ version - 2 bytes ] [ type - 2 bytes ] [ payload - see below ]
|
// [ version - 2 bytes ] [ type - 2 bytes ] [ payload - see below ]
|
||||||
|
|||||||
Reference in New Issue
Block a user