diff --git a/.gitignore b/.gitignore index 8c3970a..da82313 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ Justfile +*.conf diff --git a/README.md b/README.md index 0151a3c..94a7775 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,52 @@ A hub-and-spoke VPN for IPv6 ## 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. * **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 See [proto.go](https://git.ton1.dev/toni/baalvpn/src/branch/main/shared/proto.go) for details ### 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 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 * [`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` -* [`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) diff --git a/client/main.go b/client/main.go index 87e8301..934507e 100644 --- a/client/main.go +++ b/client/main.go @@ -6,8 +6,10 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "fmt" "log" "net" + "os" "sync" "time" @@ -16,21 +18,22 @@ import ( "github.com/cloudflare/circl/sign/mldsa/mldsa44" ) -// TODO: parse some sort of config+key file -const ( - SERVER_IP = "172.20.12.47" - SERVER_PUBKEY = "b137ffedfaf45cb4dc5339373cdc935fbcc7597dde71b86112d282c2bda4c252e6676490acd3f3fd71a7ab7ce2edbe69521a966fa1e91056c8cd9f98443d49505c4a3663b90c322da0942e7511f9c600e0ebbeac071ba3763f623bfd2c2092123c0b2807518d70b790e2612b1b2daa32aa8fa14be8aef9072771e7c4e8c21b64173a504f92378804dcef63430b5c2d4a504d95a766b23ff04e804baf36b515d8c4116e9ffe5fa9afb1c2d005936b3e7222de604e8bae04fd5c56520d8e9dc428e6f6107fd735fcf54a7a324d8236d831b89b4fcda426d701add2b1f4122ba29021d263b74eea6ce1a546daab62b31b12c5b2f14a57ed1160af8630e1d2fa551e7609022a240ad487c76ef1c5b3632c7ca9d5c3648e8b0eaab2256ff5245b0dc25decd9271126b4834b13ddb365e75ad4411e73058460499c9a5522182007378aca990da89f87c90df379112586c90f42a2fe7ec1ab119fb95641abe61f7a04f95f2bd2a849cf01f561bcd4d5f4c57fff30e34e317bdbcba57126b068ded038a7947b6fd8501f2362fba7a695e628647ff28123b8a75df59da7b7b6a8064f6781fd70e62e86208942dc2fbe3a08999bc6b7e493dcf247d3679804c21ac0e64afd906d50b433a858ba6b55cfbb6668c5a4a7814345659756560a4e5678201a2cde4ca3c101520e0d1a0b06af5de5a596793e7bd68cdc45d2234e79b646fbb305096c193f053c6f158a0b1d2d590f43ba9236bb77000e1de75b2eb9693185bd82ac414db63450ea4808752f14946b8609d4b54c909b6854c8b2e822897e2497671102f41366808cd962802a128a551d6c56cccafb133c12294c14d97f02bc808c4a6777f1f602fe248d6d7ec6e850431d593a0510b3e5f75e19cafd3317b9951a12870d3ffdb6f4d0449ff63cce9a2acfd7eafc47befdd506b8b781133e4e4d364b816a072fa05b350368a62a61e4f63377897f579e21c67520c0edf949033210d7785a2435de37a0d609661b71d442bb11f8f9322f2df8dc52662de48299c8e3d82ab75124cb91e03f2d776ae33a6aed94d2eefa7a20d1fb9932c2511aa48304f703316e6397373418c1496b8be06cc97018af7bc43bf94cf5012c7486e251bfff3e275e759f7e1ca15afe8c1934c2a151d8cdd8c028fc67d3c6a22514d1aea6b907f130eda14860c559dee0dd88202ba2e00f5848777c2767bf96817c6b195c2556b4ce223397649c35f311de79c8687e3eec956a054bb36eb3024e520ec6d6455b12d44434ec4c41f7bcdd5133dc884de56ab77a8b1f50a613a5fbf90de87d30469dbc6d3c79b87e9a04a986bd00aa213e9cf270a37b50be919dcb999776f7928a7b43d700eeb6c92eaf901047607cb1d05839374aaa4eb49e8e7e4a69dc70a9b5b006284d72afa9a8f6a96473db5aff3c2f2ae2377a80570d2cd052cb2302d66c5115277673c9634117748c0ddf08315b17229a2ddaf238feb46270278de8d45b0c603a07b890d0f443f96c97a66bb2c783f43807e466e90aab185bcfc9e2036d1d745402c05913ee5bf380a99e5912612b10f39fa0dadefd2e2c47c67e4c118ea2d0579d35793fc2151bcbc40ebaa4e42b86231da9e952238d55327c8f9b96aec5bb1c4b283a6c67d0e0e715e754d8df84740920c002060781ac61662645ae3c92b4d79c28e7cc5e5103ff3b760e9d1feabed95b606275a57e1a1dc91a88505b41d1a4df434891d8e3f8531820a6397a5815c17dfde6e115061898f13611ecede2a1db94d2900359015e675244493f08228f95e36d9c004c0cb0405c2caed5cb31bd88800a6bdac7b53de67b36c2ea92ddd8496bafb0c304f458b28ef51217" -) +type Config struct { + ListenAddr *net.UDPAddr + ServerAddr *net.UDPAddr + ServerPubKey mldsa44.PublicKey +} var ( + config Config + conn *net.UDPConn - serverAddr *net.UDPAddr internalIP net.IP privKey hpke.PrivateKey multicastKey []byte - challengeCh = make(chan []byte, 1) authSecret []byte + challengeCh = make(chan []byte, 1) establishMutex sync.Mutex peerPubKeys = shared.NewTMap[string, hpke.PublicKey]() peerSessionKeys = shared.NewTMap[string, []byte]() @@ -38,31 +41,59 @@ var ( ) func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "Usage: baalvpn-client ") + os.Exit(1) + } + + parseConfig() + if !isAdmin() { panic("this program must be ran with administrative privileges") } - listenAddr, err := net.ResolveUDPAddr("udp", ":38000") - if err != nil { - panic(err) - } - conn, err = net.ListenUDP("udp", listenAddr) + var err error + conn, err = net.ListenUDP("udp", config.ListenAddr) if err != nil { panic(err) } defer conn.Close() - serverAddr, err = net.ResolveUDPAddr("udp", SERVER_IP+":38000") - if err != nil { - panic(err) - } - go receivePackets() register() <-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() { var err error privKey, err = hpke.MLKEM768X25519().GenerateKey() @@ -101,7 +132,7 @@ func receivePackets() { if n == 0 { continue } - if addr.String() != serverAddr.String() { + if addr.String() != config.ServerAddr.String() { log.Println("non-server connection rejected") continue } @@ -338,21 +369,12 @@ func buildAuthPkt(pktType uint16, parts ...[]byte) []byte { } func send(req []byte) { - if _, err := conn.WriteToUDP(req, serverAddr); err != nil { + if _, err := conn.WriteToUDP(req, config.ServerAddr); err != nil { panic(err) } } 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 - return mldsa44.Verify(&pubKey, pkt[2424:], nil, pkt[4:2424]) + return mldsa44.Verify(&config.ServerPubKey, pkt[2424:], nil, pkt[4:2424]) } diff --git a/client/os.go b/client/os.go index 6f4c748..e2ab2e2 100644 --- a/client/os.go +++ b/client/os.go @@ -20,8 +20,6 @@ func setupInterface() { config := water.Config{DeviceType: water.TUN} if runtime.GOOS == "linux" { config.Name = "baalvpn" - } else { - panic("TODO") } 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 { panic(err) } + if err := exec.Command("route", "-n", "add", "-inet6", "fd00:baa1::/32", "-interface", iface.Name()).Run(); err != nil { + panic(err) + } } } diff --git a/server/main.go b/server/main.go index 37ee3c9..5d18e8f 100644 --- a/server/main.go +++ b/server/main.go @@ -1,6 +1,5 @@ // TODO: somehow persist internal IPs // TODO: key rotation -// TODO: dont trust claimed IP at all // TODO: replay attacks are still a thing package main @@ -28,12 +27,11 @@ type Peer struct { AuthSecret []byte } -const SERVER_PRIVKEY = "b137ffedfaf45cb4dc5339373cdc935fbcc7597dde71b86112d282c2bda4c252c95edb48c5bd92159c992dd601b3294e59fda2b605c176232b760e545f28d3cd8a6ba045d4ad630481521182c5aab0d045f68f12de2a6cdbaea9245576b715cf1329c19e90b009b33708da77f4836cbe0b722ee325e1fe01c5b12f62a86802621829329024600243115b8680c0929108a1285428240017710046819c46264a4825c8b425d818248b2411111204e28481a3248e40c671081988020280d93800180688e4a864e28030c9080c4c800158a46524c390db0426d3b22424c845d11810109211e4c270a348415234610a304d89202590a480c4006c91240e12352021b131014540db320c2104116342648c86681c8744d924089cc845e24002544231021109d48069431290490048e13466031421511068d980404c32909b404112826c8b16061a132e19295054280cd1c08d92444109188de34046db42441323460c3741e4144080b62154184618b744019520210860d1a86820a11014c04004320803c14184b86c0297852185200ac1508ca25194c69182b0715934260018450a115008b52d1009619a922c200430c89491c312520a312488a00d8b16521c13904434411b434504830110120410c2444c284210b22003338de0228e01306448b60844060c19294618830000832400a780a1469240422440a864d1184dd9c084cb108a08363090464e049364899670a3b2400c018820282988b81018b7891c39465a84700c134811b48dda464981c20103044cc30045d9320de44880e226822239214c840c04274213c8251b08910205202116299b380400138424046808352219411091306a528080a22686c1444ed3002ecb18621a1410daa8205290315928661a37911b188889002014242a419268204189591641d1b664d128054c2664030645cab26484a0690a478c19b5812282695132409c344610b448ccc80518a545224489a48229d0a80d214249c4a66dda8228d4022d9b0441c820119a88215b1029944484d1b29110c90d14125013c4818bc228921070514070c922698a2890014620db98702334020b468d21014912a4901325519906095c3460dcb0911c37418a422dc3224a2211698110401282810b4670a3060c61a64843427053404ca2a68048486440246153966c90b86d1bb50863a029d0384964422d10258c59a285c8c89193860823b748032185d0b810a428524ca64c1a29490284815b008c0ca969c1a6409c36b1659c04f7e6f1cc28e6944fafdf06980608ec937a242945a9949704a47ae87cb6aa3b6d85d65f3f9cbcd301505fe0b19607d67f82ad539580c667927fe53c13af2e1a5177fd6f6f8a30829b363f586b92e5e9979c122393a0415e5b29fcf9da93196c10cad9c5038dd227961bc42f3f663d3647f9b811ff9ccaaf17d0a59b1fed877448412c254e3e7936023d4ef9b6b489080eaa04ca14e90e6947ff1d6320fc37acfa3d44b2710fda267e824513a056d7f5666bfac613008b1fba4f2fa12316b16ef3258ff9df5e1301835e76537318626d1d995637892b81d75ab7ed59ac0fd94af24bae94f457c88645b7de7f87faff893bda4bc55a54205b34eea275d02503eb3275b200f6b5fc2cdd82a2f46027e29b6a070c0dbe008933943861ed5e4566e7d1757084ef614f58b4237211dda05ef06bacf6ddc67d8f293fc874cf8eaaba7f4cea273e23fddfcbd40ee1ea7cf2bba94b35cded5682db8e451f5596082775055200da0c4de55834aa5e330e8d797d0d882243cca9eaa04452cbfdc50ebf5bdbacc1eee49c679eac90d13867b91e95a15a9a822ce61691f8a7aecd438d0d701534332688a7047734cc5c9ed538f7f8476c1fb8cdb44eb6db972815dc60464eb1931e1b8102838fb83b6a26458381001a70a61a7f68dd0ddbb077c5a86b48badac51271b01d5efbf019053bf6911ae8bf18fd77e70a2d0288d87af5a6035d52d3328262d72b11e1a597911ad7ec2349c106b25fd0207c31bd6f261104c1ec81f662031def52edce9eafb5cd30a4a77d1d3f4c030dcbcd13a110e1934c91323d2e8bcf57a4bd0f5f86fdd6f322af7791f65fa1af298ef6069863057847758c9285a214c0d264070a870c24a8d0a859d704a89cd0bd448c2175540259d92bae09ccf8e1ef5ba4f2ab8e409ab2027f17103ebe734d11984c8c3d4f337036cb3066413bcd488426ab17128829e88a73171d7f507e3405ba2b5b5bba6334cd51bc8861eff8443dad08670ff91bf45e918c832bfae923daae2c5425f032372257ba12d1b70d4eb38e96707270b5b3b5dff36c4f39526adfcfd96ec806828f431230d4637147e5a20ed646c6e5d81b759db34bde3e7fd39e2b16b774c5d8aa960df7cac49ba1b456ee9f3a0c8a177a6aea0f5d639e9efda6976da073b87b5e02d1a63da02d3b2e5710d4f2b76c7f01c747f328b3ccee5daf30a1a780ecd8896d376082468f1ec08b46a1533a6681c05b02d4a66327728d9014a3f81431f49b3aa82936d8224827606ef7faf12e4975832abd63513a02ffd72499cb33d305dcf1fe37006f156b04a02c3973c5e014f4733c1539f84921e7e1da5ee5d5cca7eaf2462bb4fdea2c91affea2eedfd96b6e706e718d1ff5b4a42b04a8fd20eade15c604bacd169508f3b06143e27e8bf817bf5f1fbd56a8a900ea45eb0c0c62447f9f964ae7b63d354270d02d58fa45b1f461fdceb7fa6bae89798ff7358de44adb58ca19aa989a34b3bb165c1b39054a7e2c1ec67d2fe7f80fc929f70116c41463e85b0e666709652a635eb6b2f1fc021e2e2903631a699b05015e905c68bcfc66dad19e87cb1c4dfb513d2bc473e72410cb3028cfb80295adde6aaa86737bea8b2ad135941d3ef2e17f0de620bd80e3d2aab1272bb7a44f86bcc5ea234910ab3cc15a300c48019d5035f6caf06b9602cc238fb1061a77067464ff0c4f0fed3d8abfccaed26f6e3945e6ec8c31dc92e9f04d1af749705af3612c73ed33e2f93f53eef4c29d48fd66cdb92cfadb6167ede617e71beff15cd231fefb95dfe9affcad1e67086474bfc49a3d3bdc34dd2017a3fc4d45bb2324cd31df6086efe84bc58891ce54bb24ca390297d0600ffc9e112c22860b58a32c21e5961538e30f835b2b0043579441877d858d1d44e1dc9902438bc3a1af68a52de945d7b857587bf37f85992f67e2388380bc3e3387262f9f1dde51491f7fee5202eba070bf82caa395ffeed4eb435073cdd52f40f48259e3c274588820359f4275dd0a097e5faae86aa29c399953b0e20261e3360f7a539d4eb60ccf2c677441e1215ec3fc0777ba613464ec48ea33238606d15e9c544d60d87aa39bd0b80f24a27fea01872eb18a0b38b374d609a14b057a8c6abba736e16ab9fb8a941df040883f9e0270a40e483664168f6c6f57071d06215b081313a221e75818ad80bdf59c84cf59f75af3026c190782b9a386e3c46479561ab413569fc1ca198f39b2b77fb9f21fd9de72998e5183f3006ac0ad36cdf3568aef4346608dc6a05f5cb7fed731cc731796704ed5c009e8fa7749625f8bdd78c28332104bcb9f8b73b786ef36472557f2231ca4ae" - var ( conn *net.UDPConn multicastKey = make([]byte, 32) authSecretsByPubKey = shared.NewTMap[string, []byte]() + serverPrivKey mldsa44.PrivateKey peers = shared.NewTMap[string, *Peer]() peersByInternal = shared.NewTMap[string, *Peer]() @@ -41,16 +39,32 @@ var ( ) func main() { - if len(os.Args) > 1 && os.Args[1] == "keygen" { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "Usage: baalvpn-server ") + fmt.Fprintln(os.Stderr, " baalvpn-server keygen") + os.Exit(1) + } + + if os.Args[1] == "keygen" { pubKey, privKey, err := mldsa44.GenerateKey(rand.Reader) if err != nil { - panic(err) // cant continue + panic(err) } - fmt.Println("pubKey: " + hex.EncodeToString(pubKey.Bytes())) - fmt.Println("privKey: " + hex.EncodeToString(privKey.Bytes())) + if err := os.WriteFile("client.conf", fmt.Appendf(nil, + "LISTEN_ADDR=:38000\nSERVER_ADDR=CHANGEME:38000\nSERVER_PUBKEY=%s", + hex.EncodeToString(pubKey.Bytes())), 0600); err != nil { + panic(err) + } + if err := os.WriteFile("server.conf", fmt.Appendf(nil, + "SERVER_PRIVKEY=%s", hex.EncodeToString(privKey.Bytes())), 0600); err != nil { + panic(err) + } + fmt.Println("client.conf and server.conf generated") return } + parseConfig() + if os.Geteuid() != 0 { panic("root permissions needed") } @@ -62,6 +76,21 @@ func main() { receivePackets() } +func parseConfig() { + conf, err := shared.ParseConfFile(os.Args[1]) + if err != nil { + panic(err) + } + + privKeyBytes, err := hex.DecodeString(conf["SERVER_PRIVKEY"]) + if err != nil { + panic(err) + } + if err := serverPrivKey.UnmarshalBinary(privKeyBytes); err != nil { + panic(err) + } +} + func receivePackets() { log.Println("Listening for packets...") @@ -308,16 +337,7 @@ func handleReq(req []byte, addr *net.UDPAddr) { func buildSignedPkt(pktType uint16, parts ...[]byte) []byte { pkt := shared.BuildPkt(pktType, parts...) - privKeyBytes, err := hex.DecodeString(SERVER_PRIVKEY) - if err != nil { - panic(err) // fatal misconfiguration - } - var privKey mldsa44.PrivateKey - if err := privKey.UnmarshalBinary(privKeyBytes); err != nil { - panic(err) // fatal misconfiguration - } - - sig, err := privKey.Sign(rand.Reader, pkt[4:], nil) + sig, err := serverPrivKey.Sign(rand.Reader, pkt[4:], nil) if err != nil { panic(err) // should never happen } diff --git a/shared/confparser.go b/shared/confparser.go new file mode 100644 index 0000000..264346e --- /dev/null +++ b/shared/confparser.go @@ -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 +} diff --git a/shared/proto.go b/shared/proto.go index 0f52166..9561515 100644 --- a/shared/proto.go +++ b/shared/proto.go @@ -15,7 +15,7 @@ const PROTO_VERSION uint16 = 1 // SYM = XChaCha20-Poly1305 with nonce prepended // HMAC = HMAC-SHA-256 // hpke = HPKE(MLKEM768X25519, ChaCha20Poly1305, HKDF-SHA-256) -// ds = ML-DSA +// ds = ML-DSA-44 // packet format // [ version - 2 bytes ] [ type - 2 bytes ] [ payload - see below ]