Speak protocol 3.0 and fall back to 2.0 only when that is the reflector's maximum.
A reflector that has moved to 3.0 will not accept a station that always announces 2.0, and a 2.x reflector still needs one explicit plaintext session. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+42
-13
@@ -45,17 +45,13 @@ func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []st
|
||||
t.Cleanup(cancel)
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || len(body) != 4 || body[1] != 2 {
|
||||
t.Fatalf("protocol frame type %d body %v", typ, body)
|
||||
}
|
||||
declineTo2(t, refl)
|
||||
challenge := make([]byte, 20)
|
||||
for i := range challenge {
|
||||
challenge[i] = byte(i + 1)
|
||||
}
|
||||
refl.writeFrame(t, 10, appendBytes(nil, challenge))
|
||||
typ, body = refl.readFrame(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 11 {
|
||||
t.Fatalf("auth type %d", typ)
|
||||
}
|
||||
@@ -88,12 +84,41 @@ func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []st
|
||||
if utyp != 1 || gotID != id || seq != 0 || len(ubody) != 0 {
|
||||
t.Fatalf("udp heartbeat type %d id %d seq %d len %d", utyp, gotID, seq, len(ubody))
|
||||
}
|
||||
if _, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connected" }); err != nil {
|
||||
got, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connected" })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Protocol != "2.0" {
|
||||
t.Fatalf("Protocol = %q, want 2.0", got.Protocol)
|
||||
}
|
||||
return loggedIn{st: st, clock: clock, refl: refl, cancel: cancel, id: id}
|
||||
}
|
||||
|
||||
func declineTo2(t *testing.T, refl *reflector) {
|
||||
t.Helper()
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("first version type %d body %v, want 3.0", typ, body)
|
||||
}
|
||||
refl.writeFrame(t, 6, versionWire(2, 0))
|
||||
refl.accept(t)
|
||||
typ, body = refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("fallback version type %d body %v, want 2.0", typ, body)
|
||||
}
|
||||
}
|
||||
|
||||
func versionWire(major, minor uint16) []byte {
|
||||
return appendU16(appendU16(nil, major), minor)
|
||||
}
|
||||
|
||||
func versionIs(body []byte, major, minor uint16) bool {
|
||||
if len(body) != 4 {
|
||||
return false
|
||||
}
|
||||
return binary.BigEndian.Uint16(body[0:2]) == major && binary.BigEndian.Uint16(body[2:4]) == minor
|
||||
}
|
||||
|
||||
func TestQuietSessionSendsHeartbeats(t *testing.T) {
|
||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||
|
||||
@@ -127,12 +152,15 @@ func TestDropRetriesUntilQuit(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := in.st.State(); got.Protocol != "" {
|
||||
t.Fatalf("Protocol = %q, want blank while disconnected", got.Protocol)
|
||||
}
|
||||
in.clock.BlockUntilWaiting(t)
|
||||
in.clock.Advance(2 * time.Second)
|
||||
in.refl.accept(t)
|
||||
typ, body := in.refl.readFrame(t)
|
||||
if typ != 5 || body[1] != 2 {
|
||||
t.Fatalf("retry protocol type %d body %v", typ, body)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry protocol type %d body %v, want 3.0", typ, body)
|
||||
}
|
||||
in.cancel()
|
||||
in.clock.Advance(2 * time.Second)
|
||||
@@ -240,7 +268,7 @@ func TestMissingOpusStopsRetries(t *testing.T) {
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
refl.readFrame(t)
|
||||
declineTo2(t, refl)
|
||||
challenge := make([]byte, 20)
|
||||
refl.writeFrame(t, 10, appendBytes(nil, challenge))
|
||||
refl.readFrame(t)
|
||||
@@ -508,14 +536,15 @@ func expectAudio(t *testing.T, refl *reflector, dec *opus.Decoder, id, seq uint1
|
||||
}
|
||||
|
||||
func tryFrame(r *reflector) (uint16, []byte, bool) {
|
||||
_ = r.tcp.SetReadDeadline(time.Now().Add(30 * time.Millisecond))
|
||||
conn := r.conn()
|
||||
_ = conn.SetReadDeadline(time.Now().Add(30 * time.Millisecond))
|
||||
var hdr [4]byte
|
||||
if _, err := io.ReadFull(r.tcp, hdr[:]); err != nil {
|
||||
if _, err := io.ReadFull(conn, hdr[:]); err != nil {
|
||||
return 0, nil, false
|
||||
}
|
||||
n := binary.BigEndian.Uint32(hdr[:])
|
||||
buf := make([]byte, n)
|
||||
if _, err := io.ReadFull(r.tcp, buf); err != nil {
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return 0, nil, false
|
||||
}
|
||||
if len(buf) < 2 {
|
||||
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package station
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
caFileName = "ca.pem"
|
||||
keyFileName = "key.pem"
|
||||
csrFileName = "csr.pem"
|
||||
certFileName = "cert.pem"
|
||||
)
|
||||
|
||||
var (
|
||||
oidBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19}
|
||||
oidKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15}
|
||||
oidExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
|
||||
oidClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2}
|
||||
)
|
||||
|
||||
func ensureCertDir(path string) error {
|
||||
err := os.Mkdir(path, 0o700)
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrExist) {
|
||||
return err
|
||||
}
|
||||
info, statErr := os.Stat(path)
|
||||
if statErr != nil {
|
||||
return statErr
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return os.Chmod(path, 0o700)
|
||||
}
|
||||
|
||||
func caDigest(dir string) (sum [32]byte, ok bool, err error) {
|
||||
body, err := os.ReadFile(filepath.Join(dir, caFileName))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return sum, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return sum, false, err
|
||||
}
|
||||
return sha256.Sum256(body), true, nil
|
||||
}
|
||||
|
||||
func verifyCABundle(caPEM string, sig []byte, certPEM string) error {
|
||||
block, _ := pem.Decode([]byte(certPEM))
|
||||
if block == nil {
|
||||
return errors.New("signing certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pub, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("signing certificate")
|
||||
}
|
||||
sum := sha256.Sum256([]byte(caPEM))
|
||||
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig)
|
||||
}
|
||||
|
||||
func writeCABundle(dir, caPEM string) error {
|
||||
return writeMode(filepath.Join(dir, caFileName), []byte(caPEM), 0o644)
|
||||
}
|
||||
|
||||
func writeClientCert(dir, certPEM string) error {
|
||||
return writeMode(filepath.Join(dir, certFileName), []byte(certPEM), 0o644)
|
||||
}
|
||||
|
||||
func csrNewerThanCert(dir string) bool {
|
||||
csr, err1 := os.Stat(filepath.Join(dir, csrFileName))
|
||||
cert, err2 := os.Stat(filepath.Join(dir, certFileName))
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
return csr.ModTime().After(cert.ModTime())
|
||||
}
|
||||
|
||||
func readCSR(dir string) (string, error) {
|
||||
body, err := os.ReadFile(filepath.Join(dir, csrFileName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// prepareIdentity checks a saved certificate or creates the key and signing
|
||||
// request. fatal is a window message when the station must stop.
|
||||
func prepareIdentity(dir, callsign string) (tls.Certificate, bool, string) {
|
||||
certPath := filepath.Join(dir, certFileName)
|
||||
keyPath := filepath.Join(dir, keyFileName)
|
||||
_, certErr := os.Stat(certPath)
|
||||
_, keyErr := os.Stat(keyPath)
|
||||
certExists := certErr == nil
|
||||
keyExists := keyErr == nil
|
||||
if certErr != nil && !errors.Is(certErr, fs.ErrNotExist) {
|
||||
return tls.Certificate{}, false, "Cannot create " + certPath
|
||||
}
|
||||
if keyErr != nil && !errors.Is(keyErr, fs.ErrNotExist) {
|
||||
return tls.Certificate{}, false, "Cannot create " + keyPath
|
||||
}
|
||||
if certExists && !keyExists {
|
||||
return tls.Certificate{}, false, "Certificate key is missing"
|
||||
}
|
||||
if certExists && keyExists {
|
||||
pair, msg := loadPair(certPath, keyPath, callsign)
|
||||
if msg != "" {
|
||||
return tls.Certificate{}, false, msg
|
||||
}
|
||||
return pair, true, ""
|
||||
}
|
||||
if msg := ensureKeyAndCSR(dir, callsign, !keyExists); msg != "" {
|
||||
return tls.Certificate{}, false, msg
|
||||
}
|
||||
return tls.Certificate{}, false, ""
|
||||
}
|
||||
|
||||
func loadPair(certPath, keyPath, callsign string) (tls.Certificate, string) {
|
||||
cert, err := loadCert(certPath)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, "Certificate does not match the key"
|
||||
}
|
||||
key, err := loadKey(keyPath)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, "Certificate does not match the key"
|
||||
}
|
||||
pub, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||
if !ok || !pub.Equal(&key.PublicKey) {
|
||||
return tls.Certificate{}, "Certificate does not match the key"
|
||||
}
|
||||
if cert.Subject.CommonName != callsign {
|
||||
return tls.Certificate{}, "Certificate callsign does not match"
|
||||
}
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
}, ""
|
||||
}
|
||||
|
||||
func ensureKeyAndCSR(dir, callsign string, created bool) string {
|
||||
keyPath := filepath.Join(dir, keyFileName)
|
||||
var key *rsa.PrivateKey
|
||||
var err error
|
||||
if created {
|
||||
key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return "Cannot create " + keyPath
|
||||
}
|
||||
der := x509.MarshalPKCS1PrivateKey(key)
|
||||
body := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der})
|
||||
if err := writeNew(keyPath, body, 0o600); err != nil {
|
||||
return "Cannot create " + keyPath
|
||||
}
|
||||
} else {
|
||||
key, err = loadKey(keyPath)
|
||||
if err != nil {
|
||||
return "Cannot create " + keyPath
|
||||
}
|
||||
}
|
||||
csrPath := filepath.Join(dir, csrFileName)
|
||||
_, csrErr := os.Stat(csrPath)
|
||||
if created || errors.Is(csrErr, fs.ErrNotExist) {
|
||||
if err := writeCSR(csrPath, key, callsign); err != nil {
|
||||
return "Cannot create " + csrPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeCSR(path string, key *rsa.PrivateKey, callsign string) error {
|
||||
bc, err := asn1.Marshal(struct {
|
||||
CA bool
|
||||
}{CA: false})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// digitalSignature, keyEncipherment, keyAgreement: bits 0, 2, and 4.
|
||||
ku, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0xA8}, BitLength: 5})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eku, err := asn1.Marshal([]asn1.ObjectIdentifier{oidClientAuth})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
der, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: callsign},
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{Id: oidBasicConstraints, Critical: true, Value: bc},
|
||||
{Id: oidKeyUsage, Critical: true, Value: ku},
|
||||
{Id: oidExtKeyUsage, Critical: false, Value: eku},
|
||||
},
|
||||
}, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: der})
|
||||
return writeMode(path, body, 0o644)
|
||||
}
|
||||
|
||||
func loadCert(path string) (*x509.Certificate, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(body)
|
||||
if block == nil {
|
||||
return nil, errors.New("certificate")
|
||||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
func loadKey(path string) (*rsa.PrivateKey, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(body)
|
||||
if block == nil {
|
||||
return nil, errors.New("key")
|
||||
}
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
case "PRIVATE KEY":
|
||||
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rk, ok := k.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("key")
|
||||
}
|
||||
return rk, nil
|
||||
default:
|
||||
return nil, errors.New("key")
|
||||
}
|
||||
}
|
||||
|
||||
func writeNew(path string, body []byte, mode os.FileMode) error {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, werr := f.Write(body)
|
||||
cerr := f.Close()
|
||||
if werr != nil {
|
||||
return werr
|
||||
}
|
||||
if cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
return os.Chmod(path, mode)
|
||||
}
|
||||
|
||||
func writeMode(path string, body []byte, mode os.FileMode) error {
|
||||
if err := os.WriteFile(path, body, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(path, mode)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package station
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func counterAAD(counter uint32) []byte {
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], counter)
|
||||
return b[:]
|
||||
}
|
||||
|
||||
func registrationAAD(clientID uint16) []byte {
|
||||
b := counterAAD(0)
|
||||
return appendU16(b, clientID)
|
||||
}
|
||||
|
||||
func ivFor(prefix []byte, clientID uint16, counter uint32) []byte {
|
||||
iv := make([]byte, 12)
|
||||
copy(iv, prefix)
|
||||
binary.BigEndian.PutUint16(iv[6:8], clientID)
|
||||
binary.BigEndian.PutUint32(iv[8:12], counter)
|
||||
return iv
|
||||
}
|
||||
|
||||
func sealDatagram(key, prefix []byte, clientID uint16, counter uint32, aad, plain []byte) ([]byte, error) {
|
||||
tag, ct, err := sealTrunc(key, ivFor(prefix, clientID, counter), plain, aad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wire := make([]byte, 0, len(aad)+len(tag)+len(ct))
|
||||
wire = append(wire, aad...)
|
||||
wire = append(wire, tag...)
|
||||
return append(wire, ct...), nil
|
||||
}
|
||||
|
||||
func openDatagram(key, prefix []byte, clientID uint16, counter uint32, aad, tag, ct []byte) ([]byte, error) {
|
||||
return openTrunc(key, ivFor(prefix, clientID, counter), tag, ct, aad)
|
||||
}
|
||||
|
||||
// The protocol's tag is 8 bytes. crypto/cipher only builds GCM tags of 12 to 16
|
||||
// bytes, and an 8-byte tag is the leading 8 bytes of the ordinary 16-byte tag.
|
||||
func sealTrunc(key, nonce, plain, aad []byte) (tag, ct []byte, err error) {
|
||||
g, err := gcmFull(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
out := g.Seal(nil, nonce, plain, aad)
|
||||
ct = append([]byte(nil), out[:len(plain)]...)
|
||||
tag = append([]byte(nil), out[len(plain):len(plain)+8]...)
|
||||
return tag, ct, nil
|
||||
}
|
||||
|
||||
func openTrunc(key, nonce, tag, ct, aad []byte) ([]byte, error) {
|
||||
if len(tag) != 8 {
|
||||
return nil, errors.New("tag")
|
||||
}
|
||||
g, err := gcmFull(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := g.Seal(nil, nonce, make([]byte, len(ct)), aad)
|
||||
plain := make([]byte, len(ct))
|
||||
subtle.XORBytes(plain, ct, stream[:len(ct)])
|
||||
check := g.Seal(nil, nonce, plain, aad)
|
||||
var got, want [8]byte
|
||||
copy(got[:], tag)
|
||||
copy(want[:], check[len(plain):len(plain)+8])
|
||||
if subtle.ConstantTimeCompare(got[:], want[:]) != 1 {
|
||||
return nil, errors.New("tag")
|
||||
}
|
||||
return plain, nil
|
||||
}
|
||||
|
||||
func gcmFull(key []byte) (cipher.AEAD, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewGCM(block)
|
||||
}
|
||||
+66
-27
@@ -15,34 +15,38 @@ import (
|
||||
)
|
||||
|
||||
type loaded struct {
|
||||
host string
|
||||
port int
|
||||
callsign string
|
||||
authKey string
|
||||
talkGroup int
|
||||
enableLogging bool
|
||||
logPath string
|
||||
logNotice string
|
||||
host string
|
||||
port int
|
||||
callsign string
|
||||
authKey string
|
||||
talkGroup int
|
||||
certificateDir string
|
||||
certFatal string
|
||||
enableLogging bool
|
||||
logPath string
|
||||
logNotice string
|
||||
}
|
||||
|
||||
type fileShape struct {
|
||||
Host jsontext.Value `json:"host"`
|
||||
Port jsontext.Value `json:"port"`
|
||||
Callsign jsontext.Value `json:"callsign"`
|
||||
AuthKey jsontext.Value `json:"authKey"`
|
||||
TalkGroup jsontext.Value `json:"talkGroup"`
|
||||
EnableLogging jsontext.Value `json:"enableLogging"`
|
||||
LogFile jsontext.Value `json:"logFile"`
|
||||
Host jsontext.Value `json:"host"`
|
||||
Port jsontext.Value `json:"port"`
|
||||
Callsign jsontext.Value `json:"callsign"`
|
||||
AuthKey jsontext.Value `json:"authKey"`
|
||||
TalkGroup jsontext.Value `json:"talkGroup"`
|
||||
CertificateDir jsontext.Value `json:"certificateDir"`
|
||||
EnableLogging jsontext.Value `json:"enableLogging"`
|
||||
LogFile jsontext.Value `json:"logFile"`
|
||||
}
|
||||
|
||||
type templateFile struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Callsign string `json:"callsign"`
|
||||
AuthKey string `json:"authKey"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
EnableLogging bool `json:"enableLogging"`
|
||||
LogFile string `json:"logFile"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Callsign string `json:"callsign"`
|
||||
AuthKey string `json:"authKey"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
CertificateDir string `json:"certificateDir"`
|
||||
EnableLogging bool `json:"enableLogging"`
|
||||
LogFile string `json:"logFile"`
|
||||
}
|
||||
|
||||
var callsignPattern = regexp.MustCompile(`^[A-Z0-9][A-Z]{0,2}\d[A-Z0-9]{0,3}[A-Z](?:-[A-Z0-9]{1,3})?$`)
|
||||
@@ -66,7 +70,7 @@ func writeTemplate(path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := jsonv2.Marshal(templateFile{LogFile: logFile})
|
||||
body, err := jsonv2.Marshal(templateFile{CertificateDir: "./funknetz", LogFile: logFile})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,15 +95,19 @@ func parseConfig(path string, body []byte) (loaded, error) {
|
||||
if err := jsonv2.Unmarshal(body, &shape); err != nil {
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
}
|
||||
if block, notice := blockedLog(shape); block {
|
||||
certBad := present(shape.CertificateDir) && shape.CertificateDir.Kind() != jsontext.KindString
|
||||
if block, notice := blockedLog(shape); block || certBad {
|
||||
cfg, err := parseOperational(shape)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
cfg.logNotice = notice
|
||||
return cfg, nil
|
||||
if !block {
|
||||
cfg = applyLog(cfg, shape)
|
||||
}
|
||||
return applyCertificate(cfg, shape, path), nil
|
||||
}
|
||||
if missingLog(shape) {
|
||||
if missingCertificate(shape) || missingLog(shape) {
|
||||
if !operationalKindsOK(shape) {
|
||||
return parseOperational(shape)
|
||||
}
|
||||
@@ -116,7 +124,7 @@ func parseConfig(path string, body []byte) (loaded, error) {
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
return applyLog(cfg, shape), nil
|
||||
return applyLog(applyCertificate(cfg, shape, path), shape), nil
|
||||
}
|
||||
|
||||
func parseOperational(shape fileShape) (loaded, error) {
|
||||
@@ -217,6 +225,10 @@ func missingLog(shape fileShape) bool {
|
||||
return !present(shape.EnableLogging) || !present(shape.LogFile)
|
||||
}
|
||||
|
||||
func missingCertificate(shape fileShape) bool {
|
||||
return !present(shape.CertificateDir)
|
||||
}
|
||||
|
||||
func operationalKindsOK(shape fileShape) bool {
|
||||
if present(shape.Host) && shape.Host.Kind() != jsontext.KindString {
|
||||
return false
|
||||
@@ -259,6 +271,7 @@ func withLogDefaults(shape fileShape) (fileShape, error) {
|
||||
shape.Callsign = rawOr(shape.Callsign, `""`)
|
||||
shape.AuthKey = rawOr(shape.AuthKey, `""`)
|
||||
shape.TalkGroup = rawOr(shape.TalkGroup, "0")
|
||||
shape.CertificateDir = rawOr(shape.CertificateDir, `"./funknetz"`)
|
||||
shape.EnableLogging = rawOr(shape.EnableLogging, "false")
|
||||
if !present(shape.LogFile) {
|
||||
logFile, err := defaultLogPath()
|
||||
@@ -316,6 +329,32 @@ func rewriteConfig(path string, shape fileShape) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyCertificate(cfg loaded, shape fileShape, configPath string) loaded {
|
||||
if !present(shape.CertificateDir) {
|
||||
cfg.certificateDir = resolveCertDir(configPath, "./funknetz")
|
||||
return cfg
|
||||
}
|
||||
dir, err := stringField("certificateDir", shape.CertificateDir)
|
||||
if err != nil {
|
||||
cfg.certFatal = err.Error()
|
||||
return cfg
|
||||
}
|
||||
if strings.TrimSpace(dir) == "" {
|
||||
cfg.certFatal = "certificateDir is empty"
|
||||
return cfg
|
||||
}
|
||||
cfg.certificateDir = resolveCertDir(configPath, dir)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func resolveCertDir(configPath, dir string) string {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if filepath.IsAbs(dir) {
|
||||
return filepath.Clean(dir)
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(configPath), dir))
|
||||
}
|
||||
|
||||
func applyLog(cfg loaded, shape fileShape) loaded {
|
||||
if shape.EnableLogging.Kind() != jsontext.KindTrue {
|
||||
return cfg
|
||||
|
||||
+113
-4
@@ -1,10 +1,14 @@
|
||||
package station_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jsonv2 "encoding/json/v2"
|
||||
|
||||
@@ -54,11 +58,15 @@ func TestMissingConfigIsCreatedAndRejected(t *testing.T) {
|
||||
if got["logFile"] != filepath.Join(home, "funknetz.log") {
|
||||
t.Fatalf("logFile = %#v, want funknetz.log in the home directory", got["logFile"])
|
||||
}
|
||||
if got["certificateDir"] != "./funknetz" {
|
||||
t.Fatalf("certificateDir = %#v, want ./funknetz", got["certificateDir"])
|
||||
}
|
||||
assertKeyOrder(t, body)
|
||||
}
|
||||
|
||||
func TestExistingConfigIsNotRewritten(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
||||
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"certificateDir\":\"/var/funknetz\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
||||
if err := os.WriteFile(path, original, 0o640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -240,6 +248,10 @@ func TestMissingLogKeysAreRewritten(t *testing.T) {
|
||||
if got["logFile"] != filepath.Join(home, "funknetz.log") {
|
||||
t.Fatalf("logFile = %#v", got["logFile"])
|
||||
}
|
||||
if got["certificateDir"] != "./funknetz" {
|
||||
t.Fatalf("certificateDir = %#v, want ./funknetz", got["certificateDir"])
|
||||
}
|
||||
assertKeyOrder(t, body)
|
||||
if st.State().Detail != "Idle" {
|
||||
t.Fatalf("Detail = %q, want Idle", st.State().Detail)
|
||||
}
|
||||
@@ -314,9 +326,9 @@ func TestLogSettingsDoNotStopTheStation(t *testing.T) {
|
||||
detail string
|
||||
same bool
|
||||
}{
|
||||
{name: "relative", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true,"logFile":"funknetz.log"}`, detail: "Cannot write funknetz.log", same: true},
|
||||
{name: "empty", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true,"logFile":" "}`, detail: "Cannot write logFile", same: true},
|
||||
{name: "tilde", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true,"logFile":"~/funknetz.log"}`, detail: "Cannot write ~/funknetz.log", same: true},
|
||||
{name: "relative", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":"./funknetz","enableLogging":true,"logFile":"funknetz.log"}`, detail: "Cannot write funknetz.log", same: true},
|
||||
{name: "empty", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":"./funknetz","enableLogging":true,"logFile":" "}`, detail: "Cannot write logFile", same: true},
|
||||
{name: "tilde", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":"./funknetz","enableLogging":true,"logFile":"~/funknetz.log"}`, detail: "Cannot write ~/funknetz.log", same: true},
|
||||
{name: "enable type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes","logFile":"/tmp/funknetz.log"}`, detail: "enableLogging has the wrong type", same: true},
|
||||
{name: "file type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true,"logFile":1}`, detail: "logFile has the wrong type", same: true},
|
||||
{name: "both types", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes","logFile":1}`, detail: "enableLogging has the wrong type", same: true},
|
||||
@@ -368,6 +380,103 @@ func TestLogSettingsDoNotStopTheStation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func assertKeyOrder(t *testing.T, body []byte) {
|
||||
t.Helper()
|
||||
keys := []string{`"host"`, `"port"`, `"callsign"`, `"authKey"`, `"talkGroup"`, `"certificateDir"`, `"enableLogging"`, `"logFile"`}
|
||||
last := -1
|
||||
for _, key := range keys {
|
||||
i := strings.Index(string(body), key)
|
||||
if i < 0 || i < last {
|
||||
t.Fatalf("key %s out of order in %s", key, body)
|
||||
}
|
||||
last = i
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCertificateDirIsWrittenAfterTalkGroup(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
||||
if err := os.WriteFile(path, original, 0o640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := station.New(station.Options{ConfigPath: path}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := info.Mode().Perm(), os.FileMode(0o640); got != want {
|
||||
t.Fatalf("mode = %o, want %o", got, want)
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := jsonv2.Unmarshal(body, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got["authKey"] != "a b" || got["callsign"] != "DL1ABC" || got["certificateDir"] != "./funknetz" {
|
||||
t.Fatalf("fields = %#v", got)
|
||||
}
|
||||
if strings.Contains(string(body), t.TempDir()) {
|
||||
t.Fatalf("file = %s, want the stored directory to stay relative", body)
|
||||
}
|
||||
assertKeyOrder(t, body)
|
||||
}
|
||||
|
||||
func TestCertificateDirStopsBeforeASocket(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
want string
|
||||
}{
|
||||
{name: "wrong type", dir: "1", want: "certificateDir has the wrong type"},
|
||||
{name: "empty", dir: `""`, want: "certificateDir is empty"},
|
||||
{name: "whitespace", dir: `" "`, want: "certificateDir is empty"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
body := fmt.Sprintf(`{"host":"127.0.0.1","port":%d,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":%s,"enableLogging":false,"logFile":"/tmp/funknetz.log"}`, port, tt.dir)
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := station.New(station.Options{ConfigPath: path})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(st.Close)
|
||||
got := st.State()
|
||||
if got.Detail != tt.want || got.Connection != "Disconnected" || got.Callsign != "DL1ABC" {
|
||||
t.Fatalf("State = %+v, want detail %q", got, tt.want)
|
||||
}
|
||||
after, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(after) != body {
|
||||
t.Fatalf("file = %s, want it left untouched", after)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
ln.(*net.TCPListener).SetDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
if conn, err := ln.Accept(); err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("opened a socket")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func configJSON(t testing.TB, host string, port int, callsign, authKey string, talkGroup int) string {
|
||||
t.Helper()
|
||||
body, err := jsonv2.Marshal(map[string]any{
|
||||
|
||||
@@ -2,9 +2,11 @@ package station_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -30,6 +32,9 @@ func TestConnectsToLabReflector(t *testing.T) {
|
||||
t.Skipf("lab reflector not listening on %s: %v", addr, err)
|
||||
}
|
||||
conn.Close()
|
||||
if !labUDPReachable() {
|
||||
t.Skipf("lab UDP port %s is not reachable", addr)
|
||||
}
|
||||
|
||||
path := writeConfigFile(t, configJSON(t, labHost, labPort, labCallsign, labAuthKey, labTalkGroup))
|
||||
st, err := station.New(station.Options{ConfigPath: path})
|
||||
@@ -54,3 +59,20 @@ func TestConnectsToLabReflector(t *testing.T) {
|
||||
t.Fatalf("talkGroup = %d, want %d", state.TalkGroup, labTalkGroup)
|
||||
}
|
||||
}
|
||||
|
||||
func labUDPReachable() bool {
|
||||
peer := &net.UDPAddr{IP: net.ParseIP(labHost), Port: labPort}
|
||||
conn, err := net.DialUDP("udp", nil, peer)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
_, _ = conn.Write([]byte{0})
|
||||
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
_, err = conn.Read(make([]byte, 1))
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
var op *net.OpError
|
||||
return !errors.As(err, &op) || !errors.Is(op.Err, syscall.ECONNREFUSED)
|
||||
}
|
||||
|
||||
+72
-5
@@ -4,26 +4,47 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
tcpHeartbeat = 1
|
||||
tcpProtoVer = 5
|
||||
tcpProtoDown = 6
|
||||
tcpAuthChallenge = 10
|
||||
tcpAuthResponse = 11
|
||||
tcpAuthOK = 12
|
||||
tcpError = 13
|
||||
tcpStartEncReq = 14
|
||||
tcpStartEnc = 15
|
||||
tcpCsrReq = 16
|
||||
tcpCsr = 17
|
||||
tcpClientCert = 18
|
||||
tcpCAInfo = 19
|
||||
tcpCABundleReq = 20
|
||||
tcpCABundle = 21
|
||||
tcpServerInfo = 100
|
||||
tcpNodeList = 101
|
||||
tcpNodeJoined = 102
|
||||
tcpNodeLeft = 103
|
||||
tcpTalkerStart = 104
|
||||
tcpTalkerStop = 105
|
||||
tcpSelectTG = 106
|
||||
tcpRequestQSY = 109
|
||||
tcpStateEvent = 110
|
||||
tcpNodeInfo = 111
|
||||
tcpSignal = 112
|
||||
tcpTxStatus = 113
|
||||
tcpStartUDPEnc = 114
|
||||
|
||||
udpHeartbeat = 1
|
||||
udpAudio = 101
|
||||
udpFlush = 102
|
||||
udpHeartbeat = 1
|
||||
udpAudio = 101
|
||||
udpFlush = 102
|
||||
udpAllFlushed = 103
|
||||
|
||||
maxFrame = 32768
|
||||
maxFrame = 32768
|
||||
maxPreauth20 = 64
|
||||
)
|
||||
|
||||
type frame struct {
|
||||
@@ -45,12 +66,16 @@ func writeFrame(w io.Writer, typ uint16, body []byte) error {
|
||||
}
|
||||
|
||||
func readFrame(r io.Reader) (frame, error) {
|
||||
return readFrameMax(r, maxFrame)
|
||||
}
|
||||
|
||||
func readFrameMax(r io.Reader, max uint32) (frame, error) {
|
||||
var hdr [4]byte
|
||||
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
||||
return frame{}, err
|
||||
}
|
||||
n := binary.BigEndian.Uint32(hdr[:])
|
||||
if n < 2 || n > maxFrame {
|
||||
if n < 2 || n > max {
|
||||
return frame{}, fmt.Errorf("frame length %d", n)
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
@@ -149,6 +174,48 @@ func parseServerInfo(b []byte) (uint16, []string, error) {
|
||||
return id, codecs, nil
|
||||
}
|
||||
|
||||
func versionWire(major, minor uint16) []byte {
|
||||
return appendU16(appendU16(nil, major), minor)
|
||||
}
|
||||
|
||||
func parseVersion(b []byte) (uint16, uint16, error) {
|
||||
major, rest, err := parseU16(b)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
minor, rest, err := parseU16(rest)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
return 0, 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
return major, minor, nil
|
||||
}
|
||||
|
||||
var atMostVersion = regexp.MustCompile(`^Unsupported protocol version \d+\.\d+\. May be at most (\d+)\.(\d+)\.$`)
|
||||
|
||||
// fallback20 reports that the reflector's maximum is at least 2.0 and below 3.0.
|
||||
func fallback20(msg string) bool {
|
||||
m := atMostVersion.FindStringSubmatch(msg)
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
major, err1 := strconv.Atoi(m[1])
|
||||
minor, err2 := strconv.Atoi(m[2])
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
return !versionBelow(major, minor, 2, 0) && versionBelow(major, minor, 3, 0)
|
||||
}
|
||||
|
||||
func versionBelow(major, minor, maj, min int) bool {
|
||||
if major != maj {
|
||||
return major < maj
|
||||
}
|
||||
return minor < min
|
||||
}
|
||||
|
||||
func hasOPUS(codecs []string) bool {
|
||||
for _, c := range codecs {
|
||||
if c == "OPUS" {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
package station_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.neitzel.de/konrad/funknetz/internal/opus"
|
||||
"gitea.neitzel.de/konrad/funknetz/station"
|
||||
)
|
||||
|
||||
func TestProtocol2DropsASequenceTooFarBehind(t *testing.T) {
|
||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||
enc, err := opus.NewEncoder()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer enc.Close()
|
||||
pkt, err := enc.Encode(make([]int16, opus.FrameSamples))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in.refl.writeUDP(t, 101, 7, 32768, appendBytes(nil, pkt))
|
||||
in.refl.writeUDP(t, 101, 7, 4, appendBytes(nil, pkt))
|
||||
if _, err := in.st.WaitPlayback(t.Context(), opus.FrameSamples); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if extra := in.st.TakePlayback(); len(extra) != 0 {
|
||||
t.Fatalf("played %d extra samples from the old sequence", len(extra))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocol2FlushCompletesOnce(t *testing.T) {
|
||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||
in.st.HoldButton()
|
||||
in.st.ReleaseButton()
|
||||
typ, id, _, body := in.refl.readUDP(t)
|
||||
if typ != 102 || id != 7 || len(body) != 0 {
|
||||
t.Fatalf("flush type %d id %d len %d", typ, id, len(body))
|
||||
}
|
||||
in.refl.writeUDP(t, 103, 7, 0, nil)
|
||||
in.clock.BlockUntilWaiting(t)
|
||||
in.clock.Advance(3 * time.Second)
|
||||
if pkt, _, ok := tryUDP(in.refl); ok {
|
||||
t.Fatalf("sent %d bytes after flush-complete", len(pkt))
|
||||
}
|
||||
if got := in.st.State(); got.Connection != "Connected" || got.Protocol != "2.0" {
|
||||
t.Fatalf("State = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocol2FlushTimesOutWithoutASecondFlush(t *testing.T) {
|
||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||
in.st.HoldButton()
|
||||
in.st.ReleaseButton()
|
||||
typ, _, _, _ := in.refl.readUDP(t)
|
||||
if typ != 102 {
|
||||
t.Fatalf("type %d, want flush", typ)
|
||||
}
|
||||
in.clock.BlockUntilWaiting(t)
|
||||
in.clock.Advance(3 * time.Second)
|
||||
if pkt, _, ok := tryUDP(in.refl); ok {
|
||||
t.Fatalf("sent %d bytes when flush-complete did not arrive", len(pkt))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilenceFreesPushToTalkAndKeepsTheTalker(t *testing.T) {
|
||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||
enc, err := opus.NewEncoder()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer enc.Close()
|
||||
pkt, err := enc.Encode(make([]int16, opus.FrameSamples))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in.refl.writeUDP(t, 101, 7, 0, appendBytes(nil, pkt))
|
||||
if _, err := in.st.WaitPlayback(t.Context(), opus.FrameSamples); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in.refl.writeFrame(t, 104, appendStr(appendU32(nil, 20), "DB0XYZ"))
|
||||
if _, err := in.st.Wait(t.Context(), func(s station.State) bool { return s.Talker == "DB0XYZ" }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in.clock.BlockUntilWaiting(t)
|
||||
in.clock.Advance(3 * time.Second)
|
||||
in.st.HoldButton()
|
||||
in.st.WriteMicrophone(make([]int16, opus.FrameSamples))
|
||||
if _, _, ok := tryUDP(in.refl); ok {
|
||||
t.Fatal("push-to-talk sent audio at exactly 3 seconds")
|
||||
}
|
||||
in.st.ReleaseButton()
|
||||
in.clock.BlockUntilWaiting(t)
|
||||
gen := in.clock.Generation()
|
||||
in.clock.Advance(time.Second)
|
||||
waitClock(t, in.clock, gen)
|
||||
if got := in.st.State(); got.Talker != "DB0XYZ" || got.Transmitting {
|
||||
t.Fatalf("State = %+v, want the talker kept and the button free", got)
|
||||
}
|
||||
in.st.HoldButton()
|
||||
if !in.st.State().Transmitting {
|
||||
t.Fatal("push-to-talk stayed blocked after the gap")
|
||||
}
|
||||
in.st.WriteMicrophone(make([]int16, opus.FrameSamples))
|
||||
utyp, id, _, _ := in.refl.readUDP(t)
|
||||
if utyp != 101 || id != 7 {
|
||||
t.Fatalf("audio type %d id %d", utyp, id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPLimitRunsBeforeAnyDatagram(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
for step := 0; step < 6; step++ {
|
||||
clock.BlockUntilWaiting(t)
|
||||
clock.Advance(10 * time.Second)
|
||||
if st.State().Connection == "Disconnected" && step < 5 {
|
||||
t.Fatalf("dropped on step %d", step)
|
||||
}
|
||||
replyTCP(t, refl)
|
||||
}
|
||||
if _, err := st.Wait(t.Context(), func(s station.State) bool { return s.Connection == "Disconnected" }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := st.State().Detail; got != "udp receive timeout" {
|
||||
t.Fatalf("Detail = %q, want udp receive timeout", got)
|
||||
}
|
||||
clock.BlockUntilWaiting(t)
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 3.0", typ, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOversizedFrameBeforeAuthRetries(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
payload := make([]byte, 65)
|
||||
var hdr [4]byte
|
||||
binary.BigEndian.PutUint32(hdr[:], 65)
|
||||
if _, err := refl.tcp.Write(append(hdr[:], payload...)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Disconnected" }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clock.BlockUntilWaiting(t)
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 3.0", typ, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationMessageBeforeLoginRetries(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
refl.writeFrame(t, 100, serverInfo(1, nil, []string{"OPUS"}))
|
||||
got, err := st.Wait(t.Context(), func(s station.State) bool { return s.Detail == "Protocol error" })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Connection != "Disconnected" || got.Protocol != "" {
|
||||
t.Fatalf("State = %+v", got)
|
||||
}
|
||||
clock.BlockUntilWaiting(t)
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry type %d body %v", typ, body)
|
||||
}
|
||||
if st.State().Detail != "Protocol error" {
|
||||
t.Fatalf("Detail = %q, want the reason kept", st.State().Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoredNoticesDoNotChangeTheStation(t *testing.T) {
|
||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||
in.refl.writeFrame(t, 101, appendStr(appendU16(nil, 1), "DB0XYZ"))
|
||||
in.refl.writeFrame(t, 102, appendStr(nil, "DB0XYZ"))
|
||||
in.refl.writeFrame(t, 103, appendStr(nil, "DB0XYZ"))
|
||||
in.refl.writeFrame(t, 109, appendU32(nil, 99))
|
||||
body := appendStr(nil, "src")
|
||||
body = appendStr(body, "name")
|
||||
body = appendStr(body, "msg")
|
||||
in.refl.writeFrame(t, 110, body)
|
||||
in.refl.writeFrame(t, 112, append(appendU16(nil, 1), []byte{1, 40, 1}...))
|
||||
in.refl.writeFrame(t, 113, append(appendU16(nil, 1), []byte{1, 1}...))
|
||||
in.clock.BlockUntilWaiting(t)
|
||||
in.clock.Advance(10 * time.Second)
|
||||
typ, _ := in.refl.readFrame(t)
|
||||
if typ != 1 {
|
||||
t.Fatalf("TCP type %d, want a heartbeat", typ)
|
||||
}
|
||||
got := in.st.State()
|
||||
if got.Connection != "Connected" || got.Protocol != "2.0" || got.TalkGroup != 20 || got.Talker != "" {
|
||||
t.Fatalf("State = %+v", got)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
package station_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.neitzel.de/konrad/funknetz/station"
|
||||
)
|
||||
|
||||
func TestVersionErrorFallsBackWhenTheMaximumIs2(t *testing.T) {
|
||||
for _, msg := range []string{
|
||||
"Unsupported protocol version 3.0. May be at most 2.0.",
|
||||
"Unsupported protocol version 3.0. May be at most 2.5.",
|
||||
} {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("type %d body %v", typ, body)
|
||||
}
|
||||
refl.writeFrame(t, 13, appendStr(nil, msg))
|
||||
refl.accept(t)
|
||||
typ, body = refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("fallback type %d body %v", typ, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionErrorBelow2Stops(t *testing.T) {
|
||||
for _, msg := range []string{
|
||||
"Unsupported protocol version 3.0. May be at most 1.9.",
|
||||
"Unsupported protocol version 3.0. May be at most 0.6.",
|
||||
} {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
refl.readFrame(t)
|
||||
refl.writeFrame(t, 13, appendStr(nil, msg))
|
||||
got, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == msg })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Connection != "Disconnected" || got.Protocol != "" {
|
||||
t.Fatalf("State = %+v", got)
|
||||
}
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
|
||||
if conn, err := refl.ln.Accept(); err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("retried")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDowngradeOtherThan2Stops(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
major, minor uint16
|
||||
want string
|
||||
}{
|
||||
{1, 0, "Unsupported protocol version 1.0"},
|
||||
{4, 2, "Unsupported protocol version 4.2"},
|
||||
} {
|
||||
t.Run(tc.want, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
refl.readFrame(t)
|
||||
refl.writeFrame(t, 6, versionWire(tc.major, tc.minor))
|
||||
got, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == tc.want })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Protocol != "" || got.Connection != "Disconnected" {
|
||||
t.Fatalf("State = %+v", got)
|
||||
}
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
|
||||
if conn, err := refl.ln.Accept(); err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("retried")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDowngradeDoesNotCreateTheCertificateDirectory(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
dir := t.TempDir()
|
||||
path := dir + "/funknetz.json"
|
||||
body := configWithDir(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "./funknetz", "")
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() { _ = st.Run(ctx) }()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
if _, err := os.Stat(dir + "/funknetz"); err == nil {
|
||||
t.Fatal("created the certificate directory on a 2.0 fallback")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package station_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"io"
|
||||
@@ -133,10 +134,18 @@ type reflector struct {
|
||||
ln net.Listener
|
||||
udp *net.UDPConn
|
||||
tcp net.Conn
|
||||
tls *tls.Conn
|
||||
peer *net.UDPAddr
|
||||
port int
|
||||
}
|
||||
|
||||
func (r *reflector) conn() net.Conn {
|
||||
if r.tls != nil {
|
||||
return r.tls
|
||||
}
|
||||
return r.tcp
|
||||
}
|
||||
|
||||
func newReflector(t *testing.T) *reflector {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
@@ -166,16 +175,18 @@ func (r *reflector) accept(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.tcp = conn
|
||||
r.tls = nil
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
}
|
||||
|
||||
func (r *reflector) readFrame(t *testing.T) (uint16, []byte) {
|
||||
t.Helper()
|
||||
if err := r.tcp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
conn := r.conn()
|
||||
if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var hdr [4]byte
|
||||
if _, err := io.ReadFull(r.tcp, hdr[:]); err != nil {
|
||||
if _, err := io.ReadFull(conn, hdr[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := binary.BigEndian.Uint32(hdr[:])
|
||||
@@ -183,7 +194,7 @@ func (r *reflector) readFrame(t *testing.T) (uint16, []byte) {
|
||||
t.Fatalf("frame length %d", n)
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
if _, err := io.ReadFull(r.tcp, buf); err != nil {
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(buf) < 2 {
|
||||
@@ -199,14 +210,29 @@ func (r *reflector) writeFrame(t *testing.T, typ uint16, body []byte) {
|
||||
copy(payload[2:], body)
|
||||
var hdr [4]byte
|
||||
binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
|
||||
if err := r.tcp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
conn := r.conn()
|
||||
if err := conn.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.tcp.Write(append(hdr[:], payload...)); err != nil {
|
||||
if _, err := conn.Write(append(hdr[:], payload...)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reflector) handshake(t *testing.T, cfg *tls.Config) error {
|
||||
t.Helper()
|
||||
r.tls = tls.Server(r.tcp, cfg)
|
||||
if err := r.tls.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
return err
|
||||
}
|
||||
err := r.tls.Handshake()
|
||||
_ = r.tls.SetDeadline(time.Time{})
|
||||
if err != nil {
|
||||
r.tls = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *reflector) readUDP(t *testing.T) (typ, id, seq uint16, body []byte) {
|
||||
t.Helper()
|
||||
if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
@@ -228,6 +254,33 @@ func (r *reflector) readUDP(t *testing.T) (typ, id, seq uint16, body []byte) {
|
||||
return typ, id, seq, body
|
||||
}
|
||||
|
||||
func (r *reflector) readRawUDP(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := make([]byte, 2048)
|
||||
n, addr, err := r.udp.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.peer = addr
|
||||
return append([]byte(nil), buf[:n]...)
|
||||
}
|
||||
|
||||
func (r *reflector) writeRawUDP(t *testing.T, pkt []byte) {
|
||||
t.Helper()
|
||||
if r.peer == nil {
|
||||
t.Fatal("no udp peer")
|
||||
}
|
||||
if err := r.udp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.udp.WriteToUDP(pkt, r.peer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reflector) writeUDP(t *testing.T, typ, id, seq uint16, body []byte) {
|
||||
t.Helper()
|
||||
if r.peer == nil {
|
||||
@@ -294,8 +347,11 @@ func requireSoftwareIdentity(t *testing.T, body []byte) {
|
||||
if err := json.Unmarshal([]byte(raw), &got); err != nil {
|
||||
t.Fatalf("software identity %q: %v", raw, err)
|
||||
}
|
||||
if len(got) != 2 || got["sw"] != station.SoftwareName || got["swVer"] != station.SoftwareVersion {
|
||||
t.Fatalf("software identity = %#v, want sw %q and swVer %q only", got, station.SoftwareName, station.SoftwareVersion)
|
||||
if len(got) != 3 || got["sw"] != station.SoftwareName || got["swVer"] != station.SoftwareVersion || got["projVer"] != station.SoftwareVersion {
|
||||
t.Fatalf("software identity = %#v, want sw, swVer, and projVer %q", got, station.SoftwareName)
|
||||
}
|
||||
if _, ok := got["machineArch"]; ok {
|
||||
t.Fatal("protocol 2.0 node information includes machineArch")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+874
-170
File diff suppressed because it is too large
Load Diff
+12
-6
@@ -27,14 +27,20 @@ func TestLoginAnnouncesProtocolAndJoinsTheTalkGroup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Callsign != "DL1ABC" || got.TalkGroup != 20 || got.Detail != "Idle" {
|
||||
t.Fatalf("State = %+v, want DL1ABC talk group 20 Idle", got)
|
||||
if got.Callsign != "DL1ABC" || got.TalkGroup != 20 || got.Detail != "Idle" || got.Protocol != "" {
|
||||
t.Fatalf("State = %+v, want DL1ABC talk group 20 Idle and a blank protocol", got)
|
||||
}
|
||||
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || len(body) != 4 || body[0] != 0 || body[1] != 2 || body[2] != 0 || body[3] != 0 {
|
||||
t.Fatalf("first frame type %d body %v, want protocol 2.0", typ, body)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("first frame type %d body %v, want protocol 3.0", typ, body)
|
||||
}
|
||||
refl.writeFrame(t, 6, versionWire(2, 0))
|
||||
refl.accept(t)
|
||||
typ, body = refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("fallback frame type %d body %v, want protocol 2.0", typ, body)
|
||||
}
|
||||
|
||||
challenge := make([]byte, 20)
|
||||
@@ -91,8 +97,8 @@ func TestLoginAnnouncesProtocolAndJoinsTheTalkGroup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Detail != "Idle" || got.Talker != "" {
|
||||
t.Fatalf("State = %+v, want Connected Idle", got)
|
||||
if got.Detail != "Idle" || got.Talker != "" || got.Protocol != "2.0" {
|
||||
t.Fatalf("State = %+v, want Connected Idle protocol 2.0", got)
|
||||
}
|
||||
|
||||
// No other talk group: the next TCP message is a heartbeat, not a monitor set.
|
||||
|
||||
+53
-8
@@ -1,6 +1,11 @@
|
||||
package station
|
||||
|
||||
import jsonv2 "encoding/json/v2"
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
jsonv2 "encoding/json/v2"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// SoftwareName and SoftwareVersion are the identity sent to the reflector
|
||||
// after login. Edit them here.
|
||||
@@ -9,16 +14,56 @@ const (
|
||||
SoftwareVersion = "1.0.0"
|
||||
)
|
||||
|
||||
type nodeSoftware struct {
|
||||
SW string `json:"sw"`
|
||||
SWVer string `json:"swVer"`
|
||||
ProjVer string `json:"projVer"`
|
||||
MachineArch string `json:"machineArch,omitempty"`
|
||||
}
|
||||
|
||||
func softwareIdentityBody() ([]byte, error) {
|
||||
raw, err := jsonv2.Marshal(struct {
|
||||
SW string `json:"sw"`
|
||||
SWVer string `json:"swVer"`
|
||||
}{
|
||||
SW: SoftwareName,
|
||||
SWVer: SoftwareVersion,
|
||||
})
|
||||
raw, err := marshalNode(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appendString(nil, string(raw)), nil
|
||||
}
|
||||
|
||||
func nodeInfoV3(iv, key []byte) ([]byte, error) {
|
||||
raw, err := marshalNode(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := appendBlob(nil, iv)
|
||||
body = appendBlob(body, key)
|
||||
return appendString(body, string(raw)), nil
|
||||
}
|
||||
|
||||
func marshalNode(arch bool) ([]byte, error) {
|
||||
info := nodeSoftware{
|
||||
SW: SoftwareName,
|
||||
SWVer: SoftwareVersion,
|
||||
ProjVer: SoftwareVersion,
|
||||
}
|
||||
if arch {
|
||||
if name, ok := machineArch(); ok {
|
||||
info.MachineArch = name
|
||||
}
|
||||
}
|
||||
return jsonv2.Marshal(info)
|
||||
}
|
||||
|
||||
func machineArch() (string, bool) {
|
||||
var u unix.Utsname
|
||||
if err := unix.Uname(&u); err != nil {
|
||||
return "", false
|
||||
}
|
||||
b := u.Machine[:]
|
||||
if i := bytes.IndexByte(b, 0); i >= 0 {
|
||||
b = b[:i]
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return string(b), true
|
||||
}
|
||||
|
||||
+19
-1
@@ -24,6 +24,7 @@ type State struct {
|
||||
Detail string `json:"detail"`
|
||||
Callsign string `json:"callsign"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
Protocol string `json:"protocol"`
|
||||
Talker string `json:"talker"`
|
||||
Transmitting bool `json:"transmitting"`
|
||||
}
|
||||
@@ -39,13 +40,27 @@ type Station struct {
|
||||
buttonHeld bool
|
||||
spaceHeld bool
|
||||
fatal string
|
||||
retryReason string
|
||||
protocol string
|
||||
remoteTalker string
|
||||
roomBusy bool
|
||||
audioHeard bool
|
||||
lastAudio time.Time
|
||||
flushWaiting bool
|
||||
flushAt time.Time
|
||||
rxSeq uint16
|
||||
secure bool
|
||||
udpWait bool
|
||||
ivPrefix []byte
|
||||
udpKey []byte
|
||||
rxCounter uint32
|
||||
txCounter uint32
|
||||
mic []int16
|
||||
play []int16
|
||||
clientID uint16
|
||||
seq uint16
|
||||
udp *net.UDPConn
|
||||
udpPeer *net.UDPAddr
|
||||
lastUDPSend time.Time
|
||||
enc *opus.Encoder
|
||||
dec *opus.Decoder
|
||||
@@ -73,7 +88,9 @@ func New(opt Options) (*Station, error) {
|
||||
clock = wallClock{}
|
||||
}
|
||||
detail := "Idle"
|
||||
if cfg.logNotice != "" {
|
||||
if cfg.certFatal != "" {
|
||||
detail = cfg.certFatal
|
||||
} else if cfg.logNotice != "" {
|
||||
detail = cfg.logNotice
|
||||
}
|
||||
s := &Station{
|
||||
@@ -82,6 +99,7 @@ func New(opt Options) (*Station, error) {
|
||||
wake: make(chan struct{}, 1),
|
||||
logPath: cfg.logPath,
|
||||
logNotice: cfg.logNotice,
|
||||
fatal: cfg.certFatal,
|
||||
state: State{
|
||||
Connection: "Disconnected",
|
||||
Detail: detail,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package station
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errHostMismatch = errors.New("host mismatch")
|
||||
|
||||
func tlsConfig(host string, caPEM []byte, client tls.Certificate, present bool) (*tls.Config, error) {
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caPEM) {
|
||||
return nil, errors.New("ca bundle")
|
||||
}
|
||||
cfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
RootCAs: pool,
|
||||
InsecureSkipVerify: true,
|
||||
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||
return verifyPeer(host, pool, cs)
|
||||
},
|
||||
}
|
||||
if present {
|
||||
cfg.Certificates = []tls.Certificate{client}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func verifyPeer(host string, pool *x509.CertPool, cs tls.ConnectionState) error {
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
return errors.New("no peer certificate")
|
||||
}
|
||||
leaf := cs.PeerCertificates[0]
|
||||
opts := x509.VerifyOptions{Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}
|
||||
if len(cs.PeerCertificates) > 1 {
|
||||
opts.Intermediates = x509.NewCertPool()
|
||||
for _, cert := range cs.PeerCertificates[1:] {
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
}
|
||||
if _, err := leaf.Verify(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
for _, cand := range leaf.IPAddresses {
|
||||
if cand.Equal(ip) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errHostMismatch
|
||||
}
|
||||
if err := leaf.VerifyHostname(host); err != nil {
|
||||
return errHostMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user