"crypto/subtle"
"encoding/base64"
"encoding/binary"
- "encoding/hex"
"flag"
"fmt"
"io"
"github.com/dchest/siphash"
"github.com/flynn/noise"
"github.com/jroimartin/gocui"
+ "go.cypherpunks.ru/netstring/v2"
"go.stargrave.org/opus/v2"
vors "go.stargrave.org/vors/v3/internal"
"golang.org/x/crypto/blake2s"
Muted bool
)
-func parseSID(s string) byte {
- n, err := strconv.Atoi(s)
- if err != nil {
- log.Fatal(err)
- }
- if n > 255 {
- log.Fatal("too big stream num")
- }
- return byte(n)
-}
-
func incr(data []byte) {
for i := len(data) - 1; i >= 0; i-- {
data[i]++
return
}
+ var passwdHsh []byte
+ if *passwd != "" {
+ hsh := blake2s.Sum256([]byte(*passwd))
+ passwdHsh = hsh[:]
+ }
+
srvPub, err := base64.RawURLEncoding.DecodeString(*srvPubB64)
if err != nil {
log.Fatal(err)
}
vors.PreferIPv4 = *prefer4
- ctrl, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
+ ctrlConn, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
if err != nil {
log.Fatalln("dial server:", err)
}
- defer ctrl.Close()
- if err = ctrl.SetNoDelay(true); err != nil {
+ defer ctrlConn.Close()
+ if err = ctrlConn.SetNoDelay(true); err != nil {
log.Fatalln("nodelay:", err)
}
+ ctrl := vors.NewNSConn(ctrlConn)
hs, err := noise.NewHandshakeState(noise.Config{
CipherSuite: vors.NoiseCipherSuite,
if err != nil {
log.Fatalln("noise.NewHandshakeState:", err)
}
- buf, _, _, err := hs.WriteMessage(nil, []byte(*Name+" "+*Room+" "+*passwd))
+ buf, _, _, err := hs.WriteMessage(nil, vors.ArgsEncode(
+ []byte(*Name), []byte(*Room), passwdHsh,
+ ))
if err != nil {
log.Fatalln("handshake encrypt:", err)
}
- buf = append(
- append(
- []byte(vors.NoisePrologue),
- byte((len(buf)&0xFF00)>>8),
- byte((len(buf)&0x00FF)>>0),
- ),
- buf...,
- )
- _, err = io.Copy(ctrl, bytes.NewReader(buf))
+ {
+ var w bytes.Buffer
+ w.WriteString(vors.NoisePrologue)
+ netstring.NewWriter(&w).WriteChunk(buf)
+ buf = w.Bytes()
+ }
+ _, err = io.Copy(ctrlConn, bytes.NewReader(buf))
if err != nil {
log.Fatalln("write handshake:", err)
return
}
- buf, err = vors.PktRead(ctrl)
- if err != nil {
- log.Fatalln("read handshake:", err)
+ buf = <-ctrl.Rx
+ if buf == nil {
+ log.Fatalln("read handshake:", ctrl.Err)
}
buf, txCS, rxCS, err := hs.ReadMessage(nil, buf)
if err != nil {
rx := make(chan []byte)
go func() {
- for {
- buf, err := vors.PktRead(ctrl)
- if err != nil {
- log.Println("rx", err)
- break
- }
+ for buf := range ctrl.Rx {
buf, err = rxCS.Decrypt(buf[:0], nil, buf)
if err != nil {
log.Println("rx decrypt", err)
}
var sid byte
{
- cols := strings.Fields(string(buf))
- if cols[0] != "OK" || len(cols) != 2 {
- log.Fatalln("handshake failed:", cols)
+ args, err := vors.ArgsDecode(buf)
+ if err != nil {
+ log.Fatalln("args decode:", err)
+ }
+ if len(args) < 2 {
+ log.Fatalln("empty args")
}
var cookie vors.Cookie
- cookieRaw, err := hex.DecodeString(cols[1])
- if err != nil {
- log.Fatal(err)
+ switch cmd := string(args[0]); cmd {
+ case vors.CmdErr:
+ log.Fatalln("handshake failed:", string(args[1]))
+ case vors.CmdCookie:
+ copy(cookie[:], args[1])
+ default:
+ log.Fatalln("unexpected post-handshake cmd:", cmd)
}
- copy(cookie[:], cookieRaw)
timeout := time.NewTimer(vors.PingTime)
defer func() {
if !timeout.Stop() {
log.Fatalln("write:", err)
}
case buf = <-rx:
- cols = strings.Fields(string(buf))
- if cols[0] != "SID" || len(cols) != 2 {
- log.Fatalln("cookie acceptance failed:", string(buf))
+ args, err := vors.ArgsDecode(buf)
+ if err != nil {
+ log.Fatalln("args decode:", err)
+ }
+ if len(args) < 2 {
+ log.Fatalln("empty args")
+ }
+ switch cmd := string(args[0]); cmd {
+ case vors.CmdErr:
+ log.Fatalln("cookie acceptance failed:", string(args[1]))
+ case vors.CmdSID:
+ sid = args[1][0]
+ Streams[sid] = &Stream{name: *Name, stats: OurStats}
+ default:
+ log.Fatalln("unexpected post-cookie cmd:", cmd)
}
- sid = parseSID(cols[1])
- Streams[sid] = &Stream{name: *Name, stats: OurStats}
break WaitForCookieAcceptance
}
}
go func() {
for {
time.Sleep(vors.PingTime)
- buf, err := txCS.Encrypt(nil, nil, []byte(vors.CmdPing))
+ buf, err := txCS.Encrypt(nil, nil, vors.ArgsEncode(
+ []byte(vors.CmdPing),
+ ))
if err != nil {
log.Fatalln("tx encrypt:", err)
}
- if err = vors.PktWrite(ctrl, buf); err != nil {
+ if err = ctrl.Tx(buf); err != nil {
log.Fatalln("tx:", err)
}
}
go func(seen *time.Time) {
var now time.Time
for buf := range rx {
- if string(buf) == vors.CmdPong {
+ args, err := vors.ArgsDecode(buf)
+ if err != nil {
+ log.Fatalln("args decode:", err)
+ }
+ if len(args) == 0 {
+ log.Fatalln("empty args")
+ }
+ switch cmd := string(args[0]); cmd {
+ case vors.CmdPong:
now = time.Now()
*seen = now
- continue
- }
- cols := strings.Fields(string(buf))
- switch cols[0] {
case vors.CmdAdd:
- sidRaw, name, keyHex := cols[1], cols[2], cols[3]
- log.Println("add", name, "sid:", sidRaw)
- sid := parseSID(sidRaw)
- key, err := hex.DecodeString(keyHex)
- if err != nil {
- log.Fatal(err)
- }
+ sidRaw, name, key := args[1], args[2], args[3]
+ sid := sidRaw[0]
+ log.Println("add", string(name), "sid:", sid)
keyCiph, keyMAC := key[:chacha20.KeySize], key[chacha20.KeySize:]
stream := &Stream{
- name: name,
+ name: string(name),
in: make(chan []byte, 1<<10),
stats: &Stats{dead: make(chan struct{})},
}
go statsDrawer(stream.stats, stream.name)
Streams[sid] = stream
case vors.CmdDel:
- sid := parseSID(cols[1])
+ sid := args[1][0]
s := Streams[sid]
if s == nil {
log.Println("unknown sid:", sid)
continue
}
- log.Println("del", s.name, "sid:", cols[1])
+ log.Println("del", s.name, "sid:", sid)
delete(Streams, sid)
close(s.in)
close(s.stats.dead)
default:
- log.Fatal("unknown cmd:", cols[0])
+ log.Fatal("unexpected cmd:", cmd)
}
}
}(&seen)
"crypto/rand"
"crypto/subtle"
"encoding/base64"
- "encoding/hex"
"flag"
"fmt"
"io"
if err != nil {
log.Fatalln("noise.NewHandshakeState:", err)
}
- buf, err = vors.PktRead(conn)
- if err != nil {
- logger.Error("read handshake", "err", err)
+ nsConn := vors.NewNSConn(conn)
+ buf = <-nsConn.Rx
+ if buf == nil {
+ logger.Error("read handshake", "err", nsConn.Err)
return
}
peer := &Peer{
logger: logger,
- conn: conn,
+ conn: nsConn,
stats: &Stats{},
rx: make(chan []byte),
tx: make(chan []byte, 10),
}
var room *Room
{
- nameAndRoom, _, _, err := hs.ReadMessage(nil, buf)
+ argsRaw, _, _, err := hs.ReadMessage(nil, buf)
if err != nil {
logger.Error("handshake: decrypt", "err", err)
return
}
- cols := strings.SplitN(string(nameAndRoom), " ", 3)
- roomName := "/"
- if len(cols) > 1 {
- roomName = cols[1]
- }
- var key string
- if len(cols) > 2 {
- key = cols[2]
+ args, err := vors.ArgsDecode(argsRaw)
+ if err != nil {
+ logger.Error("handshake: decode args", "err", err)
+ return
}
- peer.name = string(cols[0])
+ peer.name = string(args[0])
+ roomName := string(args[1])
+ key := string(args[2])
logger = logger.With("name", peer.name, "room", roomName)
RoomsM.Lock()
room = Rooms[roomName]
RoomsM.Unlock()
if room.key != key {
logger.Error("wrong password")
- buf, _, _, err = hs.WriteMessage(nil, []byte("wrong password"))
+ buf, _, _, err = hs.WriteMessage(nil, vors.ArgsEncode(
+ []byte(vors.CmdErr), []byte("wrong password"),
+ ))
if err != nil {
log.Fatal(err)
}
- vors.PktWrite(conn, buf)
+ nsConn.Tx(buf)
return
}
}
continue
}
logger.Error("name already taken")
- buf, _, _, err = hs.WriteMessage(nil, []byte("name already taken"))
+ buf, _, _, err = hs.WriteMessage(nil, vors.ArgsEncode(
+ []byte(vors.CmdErr), []byte("name already taken"),
+ ))
if err != nil {
log.Fatal(err)
}
- vors.PktWrite(conn, buf)
+ nsConn.Tx(buf)
return
}
}
PeersM.Unlock()
if !found {
- buf, _, _, err = hs.WriteMessage(nil, []byte("too many users"))
+ buf, _, _, err = hs.WriteMessage(nil, vors.ArgsEncode(
+ []byte(vors.CmdErr), []byte("too many users"),
+ ))
if err != nil {
log.Fatal(err)
}
- vors.PktWrite(conn, buf)
+ nsConn.Tx(buf)
return
}
}
delete(Peers, peer.sid)
delete(room.peers, peer.sid)
PeersM.Unlock()
- s := []byte(fmt.Sprintf("%s %d", vors.CmdDel, peer.sid))
+ s := vors.ArgsEncode([]byte(vors.CmdDel), []byte{peer.sid})
for _, p := range room.peers {
go func(tx chan []byte) { tx <- s }(p.tx)
}
var txCS, rxCS *noise.CipherState
buf, txCS, rxCS, err := hs.WriteMessage(nil,
- []byte(fmt.Sprintf("OK %s", hex.EncodeToString(cookie[:]))))
+ vors.ArgsEncode([]byte(vors.CmdCookie), cookie[:]))
if err != nil {
log.Fatalln("hs.WriteMessage:", err)
}
- if err = vors.PktWrite(conn, buf); err != nil {
+ if err = nsConn.Tx(buf); err != nil {
logger.Error("handshake write", "err", err)
delete(Cookies, cookie)
return
return
}
delete(Cookies, cookie)
- logger.Info("got cookie", "addr", peer.addr)
if !timeout.Stop() {
<-timeout.C
}
}
go peer.Rx()
- peer.tx <- []byte(fmt.Sprintf("SID %d", peer.sid))
+ peer.tx <- vors.ArgsEncode([]byte(vors.CmdSID), []byte{peer.sid})
for _, p := range room.peers {
if p.sid == peer.sid {
continue
}
- peer.tx <- []byte(fmt.Sprintf("%s %d %s %s",
- vors.CmdAdd, p.sid, p.name, hex.EncodeToString(p.key)))
+ peer.tx <- vors.ArgsEncode(
+ []byte(vors.CmdAdd), []byte{p.sid}, []byte(p.name), p.key)
}
{
}
{
- s := []byte(fmt.Sprintf("%s %d %s %s",
- vors.CmdAdd, peer.sid, peer.name, hex.EncodeToString(peer.key)))
+ s := vors.ArgsEncode(
+ []byte(vors.CmdAdd), []byte{peer.sid}, []byte(peer.name), peer.key)
for _, p := range room.peers {
if p.sid != peer.sid {
p.tx <- s
}(&seen)
for buf := range peer.rx {
- if string(buf) == vors.CmdPing {
+ args, err := vors.ArgsDecode(buf)
+ if err != nil {
+ logger.Error("decode args", "err", err)
+ break
+ }
+ if len(args) == 0 {
+ logger.Error("empty args")
+ break
+ }
+ switch cmd := string(args[0]); cmd {
+ case vors.CmdPing:
seen = time.Now()
- peer.tx <- []byte(vors.CmdPong)
+ peer.tx <- vors.ArgsEncode([]byte(vors.CmdPong))
+ default:
+ logger.Error("unknown", "cmd", cmd)
}
}
}
room *Room
logger *slog.Logger
- conn net.Conn
+ conn *vors.NSConn
rx, tx chan []byte
rxCS, txCS *noise.CipherState
alive chan struct{}
close(peer.rx)
close(peer.tx)
close(peer.alive)
- peer.conn.Close()
+ peer.conn.Conn.Close()
})
}
func (peer *Peer) Rx() {
- for {
- buf, err := vors.PktRead(peer.conn)
- if err != nil {
- peer.logger.Error("rx", "err", err)
- break
- }
+ var err error
+ for buf := range peer.conn.Rx {
buf, err = peer.rxCS.Decrypt(buf[:0], nil, buf)
if err != nil {
peer.logger.Error("rx decrypt", "err", err)
}
func (peer *Peer) Tx() {
+ var err error
for buf := range peer.tx {
if peer.txCS == nil {
continue
}
- buf, err := peer.txCS.Encrypt(buf[:0], nil, buf)
+ buf, err = peer.txCS.Encrypt(buf[:0], nil, buf)
if err != nil {
peer.logger.Error("tx encrypt", "err", err)
break
}
- err = vors.PktWrite(peer.conn, buf)
- if err != nil {
- peer.logger.Error("tx", "err", err)
- break
- }
+ peer.conn.Tx(buf)
}
peer.Close()
}
@item Client sends @code{VoRS v3} to the socket. Just a magic number.
-@item All next messages are prepended with 16-bit big-endian length.
+@item All next messages are @url{http://cr.yp.to/proto/netstrings.txt,
+Netstring} encoded strings. Most of them contain netstring encoded
+sequence of netstrings if multiple values are expected:
-@item Client sends initial Noise handshake message with his username as
-a payload.
+@example
+NS(NS(arg0) || NS(arg1) || ...)
+@end example
+
+@item Client sends initial Noise handshake message with his username,
+room name and optional BLAKE2s-256 hash of the room's password (or an
+empty string) as a payload: @code{"USERNAME", "ROOM", BLAKE2s(PASSWORD)}.
-@item Server answers with final noise handshake message with the payload
-of @code{OK HEX(COOKIE)}, or any other failure message. It may reject a
-client if there are too many peers or its name is already taken.
+@item Server answers with final noise handshake message with the
+@code{"COOKIE", COOKIE}, or @code{"ERR", MSG} failure message.
+It may reject a client if there are too many peers, its name is
+already taken or it provided an invalid room's password.
@item The 128-bit cookie is sent by client over UDP to the server every
second. If UDP packets are lost, then no connection is possible and
may differ from known to client one)
@end itemize
-@item Server replies with @code{SID XXX}, where XXX is ASCII decimal
+@item Server replies with @code{"SID", X}, where X is single byte
stream number client must use.
-@item @code{PING} and @code{PONG} messages are then sent every ten
+@item @code{"PING"} and @code{"PONG"} messages are then sent every ten
seconds as a heartbeat.
@end itemize
@example
-S <- C : e, es, "username"
-S -> C : e, ee, "OK COOKIE"
+S <- C : e, es, NS(NS("USERNAME") || NS("ROOM") || NS("PASSWORD"))
+S -> C : e, ee, NS(NS("COOKIE") || NS(COOKIE))
S <- C : UDP(COOKIE)
-S -> C : "SID XXX"
+S -> C : NS(NS("SID") || NS(X))
-S <- C : "PING"
-S -> C : "PONG"
+S <- C : NS(NS("PING"))
+S -> C : NS(NS("PONG"))
S <> C : ...
-S -> C : "ADD SID USERNAME HEX(KEY)"
+S -> C : NS(NS("ADD") || NS(SID) || NS(USERNAME) || NS(KEY))
S -> C : ...
-S -> C : "DEL SID"
+S -> C : NS(NS("DEL") || NS(SID))
S -> C : ...
@end example
github.com/dustin/go-humanize v1.0.1
github.com/flynn/noise v1.1.0
github.com/jroimartin/gocui v0.5.0
+ go.cypherpunks.ru/netstring/v2 v2.5.0
go.stargrave.org/opus/v2 v2.1.0
golang.org/x/term v0.19.0
)
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
+go.cypherpunks.ru/netstring/v2 v2.5.0 h1:WGo1RhjLkhMcvse9zIi+1Gqd1m3ssU3joslvwHKd4po=
+go.cypherpunks.ru/netstring/v2 v2.5.0/go.mod h1:/b95GfHHgJYdLVJEgektI+hJGsONiB/eXTNOAkJLO6I=
go.stargrave.org/opus/v2 v2.1.0 h1:WwyMf76wcIWEPIQlU2UI5V9YkqXRHQhq6wfZGslcMFc=
go.stargrave.org/opus/v2 v2.1.0/go.mod h1:Y57qgcaXH7jBvKW89fscWOT/Wd3MYfhXUbYUcOMV0A8=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
package internal
import (
- "bytes"
- "io"
- "net"
-
"github.com/flynn/noise"
)
noise.CipherChaChaPoly,
noise.HashBLAKE2s,
)
-
-func PktRead(conn net.Conn) (buf []byte, err error) {
- buf = make([]byte, 2)
- _, err = io.ReadFull(conn, buf[:2])
- if err != nil {
- return
- }
- buf = make([]byte, int(buf[0])<<8|int(buf[1]))
- _, err = io.ReadFull(conn, buf)
- return
-}
-
-func PktWrite(conn net.Conn, buf []byte) (err error) {
- _, err = io.Copy(conn, bytes.NewReader(append([]byte{
- byte((len(buf) & 0xFF00) >> 8),
- byte((len(buf) & 0x00FF) >> 0),
- }, buf...)))
- return
-}
--- /dev/null
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "log"
+ "net"
+ "sync"
+
+ "go.cypherpunks.ru/netstring/v2"
+)
+
+func ArgsEncode(datum ...[]byte) []byte {
+ var buf bytes.Buffer
+ w := netstring.NewWriter(&buf)
+ for _, data := range datum {
+ if _, err := w.WriteChunk(data); err != nil {
+ log.Fatal(err)
+ }
+ }
+ return buf.Bytes()
+}
+
+func ArgsDecode(buf []byte) (args [][]byte, err error) {
+ r := netstring.NewReader(bytes.NewReader(buf))
+ var n uint64
+ for {
+ n, err = r.Next()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ err = nil
+ break
+ }
+ return
+ }
+ arg := make([]byte, int(n))
+ _, err = io.ReadFull(r, arg)
+ if err != nil {
+ return
+ }
+ args = append(args, arg)
+ }
+ return
+}
+
+type NSConn struct {
+ Conn net.Conn
+ Rx chan []byte
+ Err error
+ sync.Mutex
+}
+
+func NewNSConn(conn net.Conn) *NSConn {
+ c := NSConn{Conn: conn, Rx: make(chan []byte)}
+ go func() {
+ r := netstring.NewReader(conn)
+ var n uint64
+ for {
+ n, c.Err = r.Next()
+ if c.Err != nil {
+ break
+ }
+ buf := make([]byte, int(n))
+ if _, c.Err = io.ReadFull(r, buf); c.Err != nil {
+ break
+ }
+ c.Rx <- buf
+ }
+ close(c.Rx)
+ }()
+ return &c
+}
+
+func (ns *NSConn) Tx(data []byte) (err error) {
+ ns.Lock()
+ _, err = netstring.NewWriter(ns.Conn).WriteChunk(data)
+ ns.Unlock()
+ return
+}
)
const (
- CmdPing = "PING"
- CmdPong = "PONG"
- CmdAdd = "ADD"
- CmdDel = "DEL"
+ CmdErr = "ERR"
+ CmdCookie = "COOKIE"
+ CmdSID = "SID"
+ CmdPing = "PING"
+ CmdPong = "PONG"
+ CmdAdd = "ADD"
+ CmdDel = "DEL"
)
var (