package main
import (
- "bufio"
"bytes"
"crypto/subtle"
- "crypto/tls"
- "crypto/x509"
"encoding/binary"
"encoding/hex"
- "errors"
"flag"
- "fmt"
"io"
"log"
"net"
- "net/netip"
"os"
"os/exec"
"strconv"
"strings"
"time"
+ "github.com/flynn/noise"
"github.com/jroimartin/gocui"
vors "go.stargrave.org/vors/internal"
"golang.org/x/crypto/blake2s"
panic("overflow")
}
+const soxParams = "--no-show-progress --buffer 1920 --channels 1 --endian little --encoding signed --rate 48000 --bits 16 --type raw -"
+
func main() {
+ srvAddr := flag.String("srv", "vors.home.arpa:"+strconv.Itoa(vors.DefaultPort),
+ "Host:TCP/UDP port to connect to")
+ srvPubHex := flag.String("pub", "", "Server's public key, hex")
+ recCmd := flag.String("rec", "rec "+soxParams, "rec command")
+ playCmd := flag.String("play", "play "+soxParams, "play command")
vadRaw := flag.Uint("vad", 0, "VAD threshold")
- hostport := flag.String("srv", "[::1]:12345", "TCP/UDP port to connect to")
- spkiHash := flag.String("spki", "FILL-ME", "SHA256 hash of server's certificate SPKI")
- passwd := flag.String("passwd", "", "Password")
- recCmd := flag.String("rec", "rec --no-show-progress --buffer 1920 --channels 1 --endian little --encoding signed --rate 48000 --bits 16 --type raw -", "rec command")
- playCmd := flag.String("play", "play --no-show-progress --buffer 1920 --channels 1 --endian little --encoding signed --rate 48000 --bits 16 --type raw -", "play command")
flag.Parse()
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
+ srvPub, err := hex.DecodeString(*srvPubHex)
+ if err != nil {
+ log.Fatal(err)
+ }
+
vad := uint64(*vadRaw)
opusEnc := newOpusEnc()
var mic io.ReadCloser
- var err error
if *recCmd != "" {
cmd := makeCmd(*recCmd)
mic, err = cmd.StdoutPipe()
}
}
- addrTCP, err := net.ResolveTCPAddr("tcp", *hostport)
+ ctrl, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
if err != nil {
- log.Fatal(err)
+ log.Fatalln("dial server:", err)
}
- addrUDP, err := net.ResolveUDPAddr("udp", *hostport)
+ defer ctrl.Close()
+ if err = ctrl.SetNoDelay(true); err != nil {
+ log.Fatalln("nodelay:", err)
+ }
+ if _, err = io.Copy(ctrl, strings.NewReader(vors.NoisePrologue)); err != nil {
+ log.Fatalln("handshake: write prologue", err)
+ return
+ }
+
+ hs, err := noise.NewHandshakeState(noise.Config{
+ CipherSuite: vors.NoiseCipherSuite,
+ Pattern: noise.HandshakeNK,
+ Initiator: true,
+ PeerStatic: srvPub,
+ Prologue: []byte(vors.NoisePrologue),
+ })
if err != nil {
- log.Fatal(err)
+ log.Fatalln("noise.NewHandshakeState:", err)
}
- ctrlRaw, err := net.DialTCP("tcp", nil, addrTCP)
+ buf, _, _, err := hs.WriteMessage(nil, []byte(*Name))
if err != nil {
- log.Fatalln("dial server:", err)
+ log.Fatalln("handshake encrypt:", err)
}
- defer ctrlRaw.Close()
- ourAddr := net.UDPAddrFromAddrPort(
- netip.MustParseAddrPort(ctrlRaw.LocalAddr().String()))
- ln, err := net.ListenUDP("udp", ourAddr)
+ if err = vors.PktWrite(ctrl, buf); err != nil {
+ log.Fatalln("write handshake:", err)
+ return
+ }
+ buf, err = vors.PktRead(ctrl)
if err != nil {
- log.Fatal(err)
+ log.Fatalln("read handshake:", err)
}
- ctrl := tls.Client(ctrlRaw, &tls.Config{
- MinVersion: tls.VersionTLS13,
- CurvePreferences: []tls.CurveID{tls.X25519},
- ServerName: vors.CN,
- InsecureSkipVerify: true,
- VerifyPeerCertificate: func(
- rawCerts [][]byte, verifiedChains [][]*x509.Certificate,
- ) error {
- cer, err := x509.ParseCertificate(rawCerts[0])
+ buf, txCS, rxCS, err := hs.ReadMessage(nil, buf)
+ if err != nil {
+ log.Fatalln("handshake decrypt:", err)
+ }
+
+ rx := make(chan []byte)
+ go func() {
+ for {
+ buf, err := vors.PktRead(ctrl)
if err != nil {
- return err
+ log.Fatalln("rx", err)
}
- if *spkiHash != vors.SPKIHash(cer) {
- return errors.New("server certificate's SPKI hash mismatch")
+ buf, err = rxCS.Decrypt(buf[:0], nil, buf)
+ if err != nil {
+ log.Fatalln("rx decrypt", err)
}
- return nil
- },
- })
- err = ctrl.Handshake()
- if err != nil {
- log.Println("TLS handshake:", err)
- return
- }
- defer ctrl.Close()
+ rx <- buf
+ }
+ }()
- scanner := bufio.NewScanner(ctrl)
- if !scanner.Scan() {
- log.Println("read challenge:", scanner.Err())
- return
+ srvAddrUDP := vors.MustResolveUDP(*srvAddr)
+ conn, err := net.DialUDP("udp", nil, srvAddrUDP)
+ if err != nil {
+ log.Fatalln("connect:", err)
}
+ var sid byte
{
- h, err := blake2s.New256([]byte(*passwd))
+ cols := strings.Fields(string(buf))
+ if cols[0] != "OK" || len(cols) != 2 {
+ log.Fatalln("handshake failed:", cols)
+ }
+ var cookie vors.Cookie
+ cookieRaw, err := hex.DecodeString(cols[1])
if err != nil {
log.Fatal(err)
}
- h.Write(scanner.Bytes())
- if _, err = io.Copy(ctrl, strings.NewReader(fmt.Sprintf(
- "%s %s\n", hex.EncodeToString(h.Sum(nil)), *Name))); err != nil {
- log.Println("write password:", err)
- return
+ copy(cookie[:], cookieRaw)
+ timeout := time.NewTimer(vors.PingTime)
+ defer func() {
+ if !timeout.Stop() {
+ <-timeout.C
+ }
+ }()
+ ticker := time.NewTicker(time.Second)
+ if _, err = conn.Write(cookie[:]); err != nil {
+ log.Fatalln("write:", err)
+ }
+ WaitForCookieAcceptance:
+ for {
+ select {
+ case <-timeout.C:
+ log.Fatalln("cookie acceptance timeout")
+ case <-ticker.C:
+ if _, err = conn.Write(cookie[:]); err != nil {
+ 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))
+ }
+ sid = parseSID(cols[1])
+ Streams[sid] = &Stream{name: *Name, stats: OurStats}
+ break WaitForCookieAcceptance
+ }
+ }
+ if !timeout.Stop() {
+ <-timeout.C
}
}
- if !scanner.Scan() {
- log.Println("auth", scanner.Err())
- return
- }
- cols := strings.Fields(scanner.Text())
- if cols[0] != "OK" {
- log.Println("auth failed:", scanner.Text())
- return
- }
- sid := parseSID(cols[1])
- Streams[sid] = &Stream{name: *Name, stats: OurStats}
- tlsState := ctrl.ConnectionState()
- keyOur, err := tlsState.ExportKeyingMaterial(cols[1], nil, chacha20.KeySize)
- if err != nil {
- log.Fatal(err)
+ var keyOur []byte
+ {
+ h, err := blake2s.New256(hs.ChannelBinding())
+ if err != nil {
+ log.Fatalln(err)
+ }
+ h.Write([]byte(vors.NoisePrologue))
+ keyOur = h.Sum(nil)
}
+
seen := time.Now()
LoggerReady := make(chan struct{})
log.Fatal(err)
}
log.SetOutput(v)
- log.Println("connected")
+ log.Println("connected", "sid:", sid,
+ "addr:", conn.LocalAddr().String())
close(LoggerReady)
for {
time.Sleep(vors.ScreenRefresh)
}()
go func() {
- var err error
for {
time.Sleep(vors.PingTime)
- if _, err = ctrl.Write([]byte(vors.CmdPing + "\n")); err != nil {
- log.Println("ping:", err)
- Finish <- struct{}{}
- break
+ buf, err := txCS.Encrypt(nil, nil, []byte(vors.CmdPing))
+ if err != nil {
+ log.Fatalln("tx encrypt:", err)
+ }
+ if err = vors.PktWrite(ctrl, buf); err != nil {
+ log.Fatalln("tx:", err)
}
}
}()
go func(seen *time.Time) {
- var t string
var now time.Time
- for scanner.Scan() {
- t = scanner.Text()
- if t == vors.CmdPong {
+ for buf := range rx {
+ if string(buf) == vors.CmdPong {
now = time.Now()
*seen = now
continue
}
- cols := strings.Fields(t)
+ cols := strings.Fields(string(buf))
switch cols[0] {
case vors.CmdAdd:
sidRaw, name, keyHex := cols[1], cols[2], cols[3]
- log.Println("add", name)
+ log.Println("add", name, "sid:", sidRaw)
sid := parseSID(sidRaw)
key, err := hex.DecodeString(keyHex)
if err != nil {
log.Println("unknown sid:", sid)
continue
}
+ log.Println("del", s.name, "sid:", cols[1])
delete(Streams, sid)
close(s.in)
close(s.stats.dead)
- log.Println("del", s.name)
default:
log.Fatal("unknown cmd:", cols[0])
}
}
- if scanner.Err() != nil {
- log.Print("scanner:", err)
- Finish <- struct{}{}
- }
}(&seen)
go func(seen *time.Time) {
var ctr uint32
for {
buf := make([]byte, 2*vors.FrameLen)
- n, from, err = ln.ReadFromUDP(buf)
+ n, from, err = conn.ReadFromUDP(buf)
if err != nil {
log.Println("recvfrom:", err)
Finish <- struct{}{}
break
}
- if from.Port != addrUDP.Port || !from.IP.Equal(addrUDP.IP) {
+ if from.Port != srvAddrUDP.Port || !from.IP.Equal(srvAddrUDP.IP) {
log.Println("wrong addr:", from)
continue
}
}
stream = Streams[buf[0]]
if stream == nil {
- log.Println("unknown stream:", buf[0])
+ // log.Println("unknown stream:", buf[0])
continue
}
stream.stats.pkts++
for {
OurStats.pkts++
OurStats.bytes += 1
- if _, err = ln.WriteTo([]byte{sid}, addrUDP); err != nil {
+ if _, err = conn.Write([]byte{sid}); err != nil {
log.Println("send:", err)
Finish <- struct{}{}
}
OurStats.bytes += uint64(len(pkt))
OurStats.last = time.Now()
OurStats.AddRMS(pcm)
- if _, err = ln.WriteTo(pkt, addrUDP); err != nil {
+ if _, err = conn.Write(pkt); err != nil {
log.Println("send:", err)
break
}
-// VoRS -- Vo(IP) Really Simple
-// Copyright (C) 2024 Sergey Matveev <stargrave@stargrave.org>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, version 3 of the License.
-//
-// 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 main
import (
- "crypto/ed25519"
"crypto/rand"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/pem"
+ "encoding/hex"
+ "flag"
"fmt"
+ "io"
"log"
- "math/big"
"os"
- "time"
- vors "go.stargrave.org/vors/internal"
+ "github.com/flynn/noise"
)
func main() {
- log.SetFlags(log.Lmicroseconds | log.Lshortfile)
- pub, prv, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- log.Fatal(err)
- }
- notBefore := time.Now()
- tmpl := x509.Certificate{
- SerialNumber: big.NewInt(1),
- Subject: pkix.Name{CommonName: vors.CN},
- NotBefore: notBefore,
- NotAfter: notBefore.Add(365 * 24 * time.Hour),
- KeyUsage: x509.KeyUsageDigitalSignature,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
- BasicConstraintsValid: true,
- DNSNames: []string{vors.CN},
- }
- der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, pub, prv)
- if err != nil {
- log.Fatal(err)
- }
- cer, err := x509.ParseCertificate(der)
- if err != nil {
- log.Fatal(err)
- }
- fmt.Fprintln(os.Stderr, "SPKI hash:", vors.SPKIHash(cer))
- key, err := x509.MarshalPKCS8PrivateKey(prv)
- if err != nil {
- log.Fatal(err)
- }
- err = pem.Encode(os.Stdout, &pem.Block{Type: "PRIVATE KEY", Bytes: key})
- if err != nil {
- log.Fatal(err)
+ pub := flag.Bool("pub", false, "Print hexadecimal public key")
+ flag.Parse()
+ if *pub {
+ data, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if len(data) != 2*32 {
+ log.Fatal("wrong length")
+ }
+ fmt.Printf("%s\n", hex.EncodeToString(data[32:]))
+ return
}
- err = pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: der})
+ kp, err := noise.DH25519.GenerateKeypair(rand.Reader)
if err != nil {
log.Fatal(err)
}
+ os.Stdout.Write(kp.Private[:])
+ os.Stdout.Write(kp.Public[:])
}
package main
import (
- "bufio"
"crypto/rand"
"crypto/subtle"
"crypto/tls"
"net/netip"
"os"
"strconv"
- "strings"
"sync"
"time"
"github.com/dustin/go-humanize"
+ "github.com/flynn/noise"
"github.com/jroimartin/gocui"
vors "go.stargrave.org/vors/internal"
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20"
- "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
)
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519},
}
- SPKI string
- Passwd = flag.String("passwd", "", "Shared password")
- Peers = map[byte]*Peer{}
- PeersM sync.Mutex
+ Peers = map[byte]*Peer{}
+ PeersM sync.Mutex
+ Prv, Pub []byte
+ Cookies = map[vors.Cookie]chan *net.UDPAddr{}
)
-type Peer struct {
- name string
- sid byte
- addr *net.UDPAddr
- conn net.Conn
- key []byte
- stats *Stats
-}
-
-func newPeer(connRaw net.Conn) {
- logger := slog.With("remote", connRaw.RemoteAddr().String())
+func newPeer(conn *net.TCPConn) {
+ logger := slog.With("remote", conn.RemoteAddr().String())
logger.Info("connected")
- defer connRaw.Close()
- if len(Peers) == 256 {
+ defer conn.Close()
+ if len(Peers) == 1<<8 {
logger.Error("too many peers")
return
}
- conn := tls.Server(connRaw, TLSCfg)
- err := conn.Handshake()
+ err := conn.SetNoDelay(true)
if err != nil {
- logger.Error("handshake:", "err", err)
+ log.Fatalln("nodelay:", err)
+ }
+ buf := make([]byte, len(vors.NoisePrologue))
+
+ if _, err = io.ReadFull(conn, buf); err != nil {
+ logger.Error("handshake: read prologue", "err", err)
+ return
+ }
+ if string(buf) != vors.NoisePrologue {
+ logger.Error("handshake: wrong prologue", "err", err)
return
}
- defer conn.Close()
- scanner := bufio.NewScanner(conn)
- peer := Peer{conn: conn, stats: &Stats{dead: make(chan struct{})}}
- peer.addr = net.UDPAddrFromAddrPort(
- netip.MustParseAddrPort(conn.RemoteAddr().String()))
+ hs, err := noise.NewHandshakeState(noise.Config{
+ CipherSuite: vors.NoiseCipherSuite,
+ Pattern: noise.HandshakeNK,
+ Initiator: false,
+ StaticKeypair: noise.DHKey{Private: Prv, Public: Pub},
+ Prologue: []byte(vors.NoisePrologue),
+ })
if err != nil {
- log.Fatal(err)
+ log.Fatalln("noise.NewHandshakeState:", err)
+ }
+ buf, err = vors.PktRead(conn)
+ if err != nil {
+ logger.Error("read handshake", "err", err)
+ return
+ }
+ peer := Peer{
+ logger: logger,
+ conn: conn,
+ stats: &Stats{alive: make(chan struct{})},
+ rx: make(chan []byte),
+ tx: make(chan []byte, 10),
+ alive: make(chan struct{}),
}
{
- chlng := make([]byte, 16)
- if _, err = io.ReadFull(rand.Reader, chlng); err != nil {
- log.Fatal(err)
+ name, _, _, err := hs.ReadMessage(nil, buf)
+ if err != nil {
+ logger.Error("handshake: decrypt", "err", err)
return
}
- chlngHex := hex.EncodeToString(chlng)
- if _, err = io.Copy(conn, strings.NewReader(chlngHex+"\n")); err != nil {
- logger.Error("write challenge:", "err", err)
- return
+ peer.name = string(name)
+ }
+ logger = logger.With("name", peer.name)
+
+ for _, p := range Peers {
+ if p.name != peer.name {
+ continue
}
- h, err := blake2s.New256([]byte(*Passwd))
+ logger.Error("name already taken")
+ buf, _, _, err = hs.WriteMessage(nil, []byte("name already taken"))
if err != nil {
log.Fatal(err)
}
- h.Write([]byte(chlngHex))
- if !scanner.Scan() {
- logger.Error("read password:", "err", scanner.Err())
- return
- }
- cols := strings.Fields(scanner.Text())
- if len(cols) == 1 {
- logger.Error("no name")
- io.Copy(conn, strings.NewReader("no name\n"))
- return
- }
- peer.name = cols[1]
- if peer.name == "myself" {
- logger.Error("reserved name")
- io.Copy(conn, strings.NewReader("reserved name\n"))
- return
- }
- logger = logger.With("name", cols[1])
- if hex.EncodeToString(h.Sum(nil)) != cols[0] {
- logger.Error("wrong password")
- io.Copy(conn, strings.NewReader("wrong password\n"))
- return
- }
- for _, p := range Peers {
- if p.name == peer.name {
- logger.Error("name already taken")
- io.Copy(conn, strings.NewReader("name already taken\n"))
- return
- }
- }
+ vors.PktWrite(conn, buf)
+ return
+ }
+
+ {
var i byte
var ok bool
+ var found bool
PeersM.Lock()
- for i = 0; i <= 255; i++ {
+ for i = 0; i <= (1<<8)-1; i++ {
if _, ok = Peers[i]; !ok {
peer.sid = i
+ found = true
break
}
}
- Peers[peer.sid] = &peer
+ if found {
+ Peers[peer.sid] = &peer
+ go peer.Tx()
+ }
PeersM.Unlock()
- logger = logger.With("sid", peer.sid)
- logger.Info("authenticated")
- defer func() {
- logger.Info("removing")
- PeersM.Lock()
- delete(Peers, peer.sid)
- close(peer.stats.dead)
- s := fmt.Sprintf("%s %d\n", vors.CmdDel, peer.sid)
- for _, p := range Peers {
- go io.Copy(p.conn, strings.NewReader(s))
+ if !found {
+ buf, _, _, err = hs.WriteMessage(nil, []byte("too many users"))
+ if err != nil {
+ log.Fatal(err)
}
- PeersM.Unlock()
- }()
- if _, err = io.Copy(conn, strings.NewReader(
- fmt.Sprintf("OK %d\n", peer.sid))); err != nil {
- logger.Error("write ok:", "err", err)
+ vors.PktWrite(conn, buf)
return
}
+ }
+ logger = logger.With("sid", peer.sid)
+ logger.Info("logged in")
+
+ defer func() {
+ logger.Info("removing")
+ PeersM.Lock()
+ delete(Peers, peer.sid)
+ PeersM.Unlock()
+ close(peer.stats.alive)
+ s := []byte(fmt.Sprintf("%s %d", vors.CmdDel, peer.sid))
for _, p := range Peers {
- if p.sid == peer.sid {
- continue
- }
- if _, err = io.Copy(conn, strings.NewReader(fmt.Sprintf(
- "%s %d %s %s\n", vors.CmdAdd, p.sid, p.name, hex.EncodeToString(p.key),
- ))); err != nil {
- logger.Error("write ADD:", "err", err)
- return
- }
+ go func(tx chan []byte) { tx <- s }(p.tx)
+ }
+ }()
+
+ {
+ var cookie vors.Cookie
+ if _, err = io.ReadFull(rand.Reader, cookie[:]); err != nil {
+ log.Fatalln("cookie:", err)
+ }
+ gotCookie := make(chan *net.UDPAddr)
+ Cookies[cookie] = gotCookie
+
+ var txCS, rxCS *noise.CipherState
+ buf, txCS, rxCS, err := hs.WriteMessage(nil,
+ []byte(fmt.Sprintf("OK %s", hex.EncodeToString(cookie[:]))))
+ if err = vors.PktWrite(conn, buf); err != nil {
+ logger.Error("handshake write", "err", err)
+ delete(Cookies, cookie)
+ return
+ }
+ peer.rxCS, peer.txCS = txCS, rxCS
+
+ timeout := time.NewTimer(vors.PingTime)
+ select {
+ case peer.addr = <-gotCookie:
+ case <-timeout.C:
+ logger.Error("cookie timeout")
+ delete(Cookies, cookie)
+ return
}
- tlsState := conn.ConnectionState()
- peer.key, err = tlsState.ExportKeyingMaterial(
- strconv.Itoa(int(peer.sid)), nil, chacha20poly1305.KeySize)
+ 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))
+
+ for _, p := range 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)))
+ }
+
+ {
+ h, err := blake2s.New256(hs.ChannelBinding())
if err != nil {
- log.Fatal(err)
+ log.Fatalln(err)
}
- {
- // assume atomic write
- s := fmt.Sprintf("%s %d %s %s\n",
- vors.CmdAdd, peer.sid, peer.name, hex.EncodeToString(peer.key))
- for _, p := range Peers {
- if p.sid == peer.sid {
- continue
- }
- go io.Copy(p.conn, strings.NewReader(s))
+ h.Write([]byte(vors.NoisePrologue))
+ peer.key = h.Sum(nil)
+ }
+
+ {
+ s := []byte(fmt.Sprintf("%s %d %s %s",
+ vors.CmdAdd, peer.sid, peer.name, hex.EncodeToString(peer.key)))
+ for _, p := range Peers {
+ if p.sid != peer.sid {
+ p.tx <- s
}
}
- seen := time.Now()
- go func(seen *time.Time) {
- for now := range time.Tick(vors.PingTime) {
+ }
+
+ seen := time.Now()
+ go func(seen *time.Time) {
+ ticker := time.Tick(vors.PingTime)
+ var now time.Time
+ for {
+ select {
+ case now = <-ticker:
if seen.Add(2 * vors.PingTime).Before(now) {
logger.Error("timeout:", "seen", seen)
- conn.Close()
- break
+ peer.Close()
+ return
}
- }
- }(&seen)
- go func(stats *Stats) {
- if *NoGUI {
+ case <-peer.alive:
return
}
- tick := time.Tick(vors.ScreenRefresh)
- var now time.Time
- var v *gocui.View
- for {
- select {
- case <-stats.dead:
- GUI.DeleteView(peer.name)
- return
- case now = <-tick:
- s := fmt.Sprintf(
- "Rx/Tx: %s / %s | %s / %s",
- humanize.Comma(stats.pktsRx),
- humanize.Comma(stats.pktsTx),
- humanize.IBytes(stats.bytesRx),
- humanize.IBytes(stats.bytesTx),
- )
- if stats.last.Add(vors.ScreenRefresh).After(now) {
- s += " | " + vors.CGreen + "TALK" + vors.CReset
- }
- v, err = GUI.View(peer.name)
- if err == nil {
- v.Clear()
- v.Write([]byte(s))
- }
+ }
+ }(&seen)
+
+ go func(stats *Stats) {
+ if *NoGUI {
+ return
+ }
+ tick := time.Tick(vors.ScreenRefresh)
+ var now time.Time
+ var v *gocui.View
+ for {
+ select {
+ case <-stats.alive:
+ GUI.DeleteView(peer.name)
+ return
+ case now = <-tick:
+ s := fmt.Sprintf(
+ "%s | Rx/Tx: %s / %s | %s / %s",
+ peer.addr,
+ humanize.Comma(stats.pktsRx),
+ humanize.Comma(stats.pktsTx),
+ humanize.IBytes(stats.bytesRx),
+ humanize.IBytes(stats.bytesTx),
+ )
+ if stats.last.Add(vors.ScreenRefresh).After(now) {
+ s += " | " + vors.CGreen + "TALK" + vors.CReset
}
- }
- }(peer.stats)
- for scanner.Scan() {
- if scanner.Text() == vors.CmdPing {
- if _, err = io.Copy(conn,
- strings.NewReader(vors.CmdPong+"\n")); err != nil {
- logger.Error("write ok:", "err", err)
- return
+ v, err = GUI.View(peer.name)
+ if err == nil {
+ v.Clear()
+ v.Write([]byte(s))
}
- seen = time.Now()
}
}
- if scanner.Err() != nil {
- logger.Error(scanner.Err().Error())
+ }(peer.stats)
+
+ for buf := range peer.rx {
+ if string(buf) == vors.CmdPing {
+ seen = time.Now()
+ peer.tx <- []byte(vors.CmdPong)
}
}
}
func main() {
- bind := flag.String("bind", "[::1]:12345", "TCP/UDP port to listen on")
- pemFile := flag.String("pem", "keypair.pem", "PEM with keypair")
+ bind := flag.String("bind", "[::1]:"+strconv.Itoa(vors.DefaultPort),
+ "Host:TCP/UDP port to listen on")
+ kpFile := flag.String("key", "key", "Path to keypair file")
flag.Parse()
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
- if *Passwd == "" {
- log.Fatal("no -passwd specified")
- }
- if err := parsePEM(*pemFile); err != nil {
- log.Fatal(err)
- }
- addrTCP, err := net.ResolveTCPAddr("tcp", *bind)
- if err != nil {
- log.Fatal(err)
- }
- addrUDP, err := net.ResolveUDPAddr("udp", *bind)
- if err != nil {
- log.Fatal(err)
+ {
+ data, err := os.ReadFile(*kpFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ Prv, Pub = data[:len(data)/2], data[len(data)/2:]
}
- lnTCP, err := net.ListenTCP("tcp", addrTCP)
+
+ lnTCP, err := net.ListenTCP("tcp",
+ net.TCPAddrFromAddrPort(netip.MustParseAddrPort(*bind)))
if err != nil {
log.Fatal(err)
}
- lnUDP, err := net.ListenUDP("udp", addrUDP)
+ lnUDP, err := net.ListenUDP("udp",
+ net.UDPAddrFromAddrPort(netip.MustParseAddrPort(*bind)))
if err != nil {
log.Fatal(err)
}
if err != nil {
log.Fatalln("recvfrom:", err)
}
+
+ if n == vors.CookieLen {
+ var cookie vors.Cookie
+ copy(cookie[:], buf)
+ if c, ok := Cookies[cookie]; ok {
+ c <- from
+ close(c)
+ } else {
+ slog.Info("unknown cookie", "cookie", cookie)
+ }
+ continue
+ }
+
sid = buf[0]
peer = Peers[sid]
if peer == nil {
- slog.Info("unknown:", "sid", sid, "from", from)
+ slog.Info("unknown", "sid", sid, "from", from)
continue
}
+
if from.Port != peer.addr.Port || !from.IP.Equal(peer.addr.IP) {
- slog.Info("wrong addr:",
+ slog.Info("wrong addr",
"peer", peer.name,
"our", peer.addr,
"got", from)
continue
}
+
peer.stats.pktsRx++
peer.stats.bytesRx += uint64(n)
if n == 1 {
continue
}
if n <= 4+vors.TagLen {
- slog.Info("too small:", "peer", peer.name, "len", n)
+ slog.Info("too small", "peer", peer.name, "len", n)
continue
}
buf[n-vors.TagLen:n],
) != 1 {
log.Println("decrypt:", peer.name, "tag differs")
- slog.Info("MAC failed:", "peer", peer.name, "len", n)
+ slog.Info("MAC failed", "peer", peer.name, "len", n)
continue
}
p.stats.pktsTx++
p.stats.bytesTx += uint64(n)
if _, err = lnUDP.WriteToUDP(buf[:n], p.addr); err != nil {
- slog.Warn("sendto:", "peer", peer.name, "err", err)
+ slog.Warn("sendto", "peer", peer.name, "err", err)
}
}
}
go func() {
<-LoggerReady
- slog.Info("listening", "bind", *bind, "spki", SPKI)
+ slog.Info("listening", "bind", *bind, "pub", hex.EncodeToString(Pub))
for {
- conn, err := lnTCP.Accept()
+ conn, err := lnTCP.AcceptTCP()
if err != nil {
log.Fatalln("accept:", err)
}
}()
if *NoGUI {
- dummy := make(chan struct{})
- <-dummy
- } else {
- err = GUI.MainLoop()
- if err != nil && err != gocui.ErrQuit {
- log.Fatal(err)
- }
+ <-make(chan struct{})
+ }
+ err = GUI.MainLoop()
+ if err != nil && err != gocui.ErrQuit {
+ log.Fatal(err)
}
}
--- /dev/null
+package main
+
+import (
+ "log/slog"
+ "net"
+ "sync"
+
+ "github.com/flynn/noise"
+ vors "go.stargrave.org/vors/internal"
+)
+
+type Peer struct {
+ name string
+ sid byte
+ addr *net.UDPAddr
+ key []byte
+ stats *Stats
+
+ logger *slog.Logger
+ conn net.Conn
+ rx, tx chan []byte
+ rxCS, txCS *noise.CipherState
+ alive chan struct{}
+ aliveOnce sync.Once
+}
+
+func (peer *Peer) Close() {
+ peer.aliveOnce.Do(func() {
+ close(peer.rx)
+ close(peer.tx)
+ close(peer.alive)
+ peer.conn.Close()
+ })
+}
+
+func (peer *Peer) Rx() {
+ for {
+ buf, err := vors.PktRead(peer.conn)
+ if err != nil {
+ peer.logger.Error("rx", "err", err)
+ break
+ }
+ buf, err = peer.rxCS.Decrypt(buf[:0], nil, buf)
+ if err != nil {
+ peer.logger.Error("rx decrypt", "err", err)
+ break
+ }
+ peer.rx <- buf
+ }
+ peer.Close()
+}
+
+func (peer *Peer) Tx() {
+ for buf := range peer.tx {
+ if peer.txCS == nil {
+ continue
+ }
+ 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.Close()
+}
bytesRx uint64
bytesTx uint64
last time.Time
- dead chan struct{}
+ alive chan struct{}
}
+++ /dev/null
-// VoRS -- Vo(IP) Really Simple
-// Copyright (C) 2024 Sergey Matveev <stargrave@stargrave.org>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, version 3 of the License.
-//
-// 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 main
-
-import (
- "crypto/tls"
- "crypto/x509"
- "encoding/pem"
- "os"
-
- vors "go.stargrave.org/vors/internal"
-)
-
-func parsePEM(pth string) error {
- data, err := os.ReadFile(pth)
- if err != nil {
- return err
- }
- cert := tls.Certificate{}
- var b *pem.Block
- for len(data) > 0 {
- b, data = pem.Decode(data)
- if b == nil {
- continue
- }
- switch b.Type {
- case "CERTIFICATE":
- cert.Certificate = append(cert.Certificate, b.Bytes)
- cer, err := x509.ParseCertificate(b.Bytes)
- if err != nil {
- return err
- }
- SPKI = vors.SPKIHash(cer)
- case "PRIVATE KEY":
- prv, err := x509.ParsePKCS8PrivateKey(b.Bytes)
- if err != nil {
- return err
- }
- cert.PrivateKey = prv
- }
- }
- TLSCfg.Certificates = append(TLSCfg.Certificates, cert)
- return nil
-}
@end itemize
-TODO: Look at latest Opus'es neural network abilities.
-
@include install.texi
@include usage.texi
@include vad.texi
VoRS is written on @url{https://go.dev/, Go}, but depends on
@url{https://github.com/hraban/opus, gopkg.in/hraban/opus.v2}
-library, that links it with C-written @code{libopus} library.
+library, that links it with C-written
+@url{https://opus-codec.org/, libopus} library.
So you will need its development headers.
@example
VoRS uses Opus codec with 20ms frames with 48kHz 1ch 16-bit S-LE sound.
It uses native @code{libopus}'es Packet Loss Concealment (PLC) feature
-if number of lost frame does not excess 32 count.
+if number of lost frame does not exceed 32 count.
Each frame has single byte stream identifier (unique identifier of the
participant) and 24-bit big-endian packet counter. Reordered packets are
identifier with the counter, +8B of UDP header for 50pps means also
24Kbps of bandwidth only for overhead transmission.
-Each client handshakes with the server over TCP protocol using TLS 1.3
-with curve25519 key-agreement protocol. @command{vors-keygen} generates
-ed25519-based certificates -- so everything here is nearly completely
-NIST-free.
+Each client handshakes with the server over TCP connection using
+@url{http://noiseprotocol.org/, Noise}-NK protocol pattern with
+curve25519, ChaCha20-Poly1305 and BLAKE2s algorithms.
-After TLS session is established, simple text-based protocol is run:
+@itemize
-@example
-TLS 1.3:
- S <- C : ClientHello
- S -> C : ServerHello+ServerFinished
- S <- C : ClientFinished
+@item Client sends @code{VoRS v1} to the socket. Just a magic number.
+
+@item All next messages are prepended with 16-bit big-endian length.
+
+@item Client sends initial Noise handshake message with his username as
+a payload.
+
+@item Server answers with final Noise handshake message with the payload
+of @code{OK HEX(COOKIE)}, or any other failure message. It may reject
+client if there are too many peers or its name is already taken.
+
+@item That 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
+after timeout server drop TCP connection.
+
+@item Otherwise it replies with @code{SID XXX}, where XXX is ASCII
+decimal stream number client must use.
-S -> C : HEX(128-bit random CHALLENGE)
-S <- C : HEX(BLAKE2s-256(PASSWORD, CHALLENGE)) USERNAME
-S -> C : OK SID
+@item @code{PING} and @code{PONG} messages are then sent every ten
+seconds as a heartbeat.
-S <- C : PING
-S -> C : PONG
+@end itemize
+
+@example
+S <- C : e, es, "username"
+S -> C : e, ee, "OK COOKIE"
+S <- C : UDP(COOKIE)
+S -> C : "SID XXX"
+
+S <- C : "PING"
+S -> C : "PONG"
S <> C : ...
-S -> C : ADD SID USERNAME HEX(KEY)
+S -> C : "ADD SID USERNAME HEX(KEY)"
S -> C : ...
-S -> C : DEL SID
+S -> C : "DEL SID"
S -> C : ...
@end example
-Client is authenticated by hashing the challenge with keyed hash. Every
-ten seconds it PINGs server, awaiting for PONG in return. Server may
-acknowledge client about new peer appearing, sending its SID (stream
-identifier) in ASCII decimal form, username and encryption key. Also it
-may notify about peer disappearing.
-
-If client did not get @code{OK SID} reply, then it disconnects.
-@code{SID} is our stream identifier. When we are successfully
-authenticated, we both derive our encryption key for UDP packets by
-"exporting keying material" (EKM) from TLS session context.
-
Every second client sends UDP packet with his single-byte stream
identifier, even if it is muted. That may help punching holes in
stateful firewalls.
+
+Clients are notified about new peers appearance with @code{ADD}
+commands, telling their SIDs, usernames and keys. @code{DEL} notifies
+about leaving peers.
@node Usage
@unnumbered Usage
-Server is required to authenticate clients, give them unique stream
-numbers and relay their voice traffic. Except for address to bind to, it
-requires only password and keypair specification. Clients authenticate
-server by its X.509 certificate's SubjectPublicKeyInfo's SHA2-256 hash.
-Clients are authenticate by challenge-response protocol based on
-provided password.
+@itemize
-Generate server's keypair with @command{vors-keygen} and run the server.
-Its SPKI hash will also be printed in the logs.
+@item
+ Generate server's keypair. And share its public key among users.
+ Fact of server's public key knowledge means ability to connect to it.
@example
-$ umask 077
-$ vors-keygen > keypair.pem
-$ vors-server -bind "[2001:db8::1234]:12345" -passwd PASSWORD -pem keypair.pem
+$ vors-keygen | tee key | vors-keygen -pub | read pub
+$ vors-server -key key -bind [2001:db8::1]:12978
@end example
-Client uses external commands for reading from microphone and playing it
-back. By default it uses SoX'es @command{rec} and @command{play}
-commands. Pay attention that VoRS expects @strong{ONLY} one channel,
-48kHz, 16-bit signed little-endian audio format. Empty strings in
-@option{-rec}/@option{-play} options mean no recording/playback attempts.
+@item
+ Client uses external commands for reading from microphone and
+ playing it back. By default it uses SoX'es @command{rec} and
+ @command{play} commands.
-@command{-play} command is spawned for each participant. Your OS should
-mix their output together.
+ Pay attention that VoRS expects @strong{ONLY} one channel, 48kHz,
+ 16-bit signed little-endian audio format. Empty strings in
+ @option{-rec}/@option{-play} options mean no recording/playback
+ attempts.
-Why no audio libraries solutions? OpenAL, PulseAudio, PortAudio,
-PipeWire, OSS, sndio, libao, JACK. Too much to choose from. None of them
-present by default in every distribution. All of them have problems,
-issues, and libao offers only playback capability for example. And pay
-attention that we have to use them from Go. Luckily SoX can use any of
-OS'es backend and we can use it transparently. And we do not have to
-create complex interface to configure in/out audio resources.
+ @command{-play} command is spawned for each participant. Your OS
+ should mix their output together.
-Start the client, providing server's SPKI hash, password and our username:
+@item
+ Start the client, providing server's public key and our username:
@example
-$ vors-client -spki SPKI -passwd PASSWORD -name NAME \
- -srv "[2001:db8::1234]:12345"
+$ vors-client -srv "[2001:db8::1]:12978" -pub $pub -name NAME
@end example
-Pressing F10 in server/client TUIs means quitting. Pressing Enter in
-client means "mute" toggling.
+ Pressing F10 in server/client TUIs means quitting. Pressing Enter in
+ client means "mute" toggling.
+
+@end itemize
$ rec [...] | vors-vad 100
[talk and see if threshold is low/high enough]
[it is too sensible, let's try higher one]
+
$ rec [...] | vors-vad 200
[perfect!]
+
$ vors-client -vad 200 [...]
@end example
require (
github.com/dustin/go-humanize v1.0.1
+ github.com/flynn/noise v1.1.0
github.com/jroimartin/gocui v0.5.0
golang.org/x/term v0.19.0
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
+github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+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=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
--- /dev/null
+package internal
+
+const (
+ Rate = 48000
+ FrameMs = 20
+ Bitrate = 32000
+ FrameLen = FrameMs * Rate / 1000
+ MaxLost = 32
+)
--- /dev/null
+package internal
+
+import "encoding/hex"
+
+const CookieLen = 16
+
+type Cookie [CookieLen]byte
+
+func (c Cookie) String() string {
+ return hex.EncodeToString(c[:])
+}
--- /dev/null
+package internal
+
+import (
+ "bytes"
+ "io"
+ "net"
+
+ "github.com/flynn/noise"
+)
+
+const NoisePrologue = "VoRS v1"
+
+var NoiseCipherSuite = noise.NewCipherSuite(
+ noise.DH25519,
+ 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 "net"
+
+const DefaultPort = 12978
+
+func MustResolveTCP(s string) (addr *net.TCPAddr) {
+ var err error
+ addr, err = net.ResolveTCPAddr("tcp6", s)
+ if err == nil {
+ return addr
+ }
+ addr, err = net.ResolveTCPAddr("tcp4", s)
+ if err != nil {
+ panic(err)
+ }
+ return
+}
+
+func MustResolveUDP(s string) (addr *net.UDPAddr) {
+ var err error
+ addr, err = net.ResolveUDPAddr("udp6", s)
+ if err == nil {
+ return addr
+ }
+ addr, err = net.ResolveUDPAddr("udp4", s)
+ if err != nil {
+ panic(err)
+ }
+ return
+}
import "time"
const (
- Rate = 48000
- FrameMs = 20
- Bitrate = 32000
- FrameLen = FrameMs * Rate / 1000
-
- CN = "vors"
- MaxLost = 32
- TagLen = 8
+ TagLen = 8
CmdPing = "PING"
CmdPong = "PONG"
+++ /dev/null
-package internal
-
-import (
- "crypto/sha256"
- "crypto/x509"
- "encoding/hex"
-)
-
-func SPKIHash(c *x509.Certificate) string {
- spki := c.RawSubjectPublicKeyInfo
- hsh := sha256.Sum256(spki)
- return hex.EncodeToString(hsh[:])
-}