feat(udp): show the packet behind a parse error, and stop repeating it
"WSJT parse error: bad magic 0x3132372e" named neither the sender nor the payload, so there was nothing to act on — even though those four bytes are ASCII "127.", i.e. some program broadcasting an address on a port expecting WSJT-X binary. The line now carries the remote address, the size, a printable preview and a hex dump of the first 96 bytes. Text senders are readable at a glance; a genuinely binary payload still shows its bytes. And it stops after five. The reported case wrote that line about 150 times a second: a permanently misconfigured port would fill the 10 MB rotating log with one repeated sentence and bury every other piece of evidence — the log's whole purpose. The fifth line names the two things worth checking, the sender and the service type. N1MM's parse error goes through the same path; it had no packet detail either.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"Worked before: an operator's portable callsigns now count as the same station. Typing RK3DWA finds the RK3DWA/3, /P and /MM contacts too, and the other way round — before, the history only appeared if you typed the exact form. Can be turned off in Settings → General; contest dupe checking is unaffected and stays exact.",
|
||||
"QSO editor: the OpsLog card now appears with the other confirmations. It has its own Sent / Received row in the QSL Info table, next to QSL, LoTW, eQSL and the rest, and the \"QSL received\" tick moved there from Contact's details — with its PSE QSL / TNX indicator, which is unchanged. Sent stays read-only: OpsLog stamps it when the card actually goes out.",
|
||||
"QSL card: the {qso.pse_tnx} stamp now prints \"TNX QSL\" instead of just \"TNX\", to read as the counterpart of \"PSE QSL\".",
|
||||
"UDP: an unreadable packet now says who sent it and what it contained — the sender's address, the size, a text preview and a hex dump — instead of just a magic number. And it stops after five: a port receiving the wrong traffic was writing that line a hundred times a second, filling the log and burying everything else. A closing line points at the two things to check, the sender and the service type.",
|
||||
"Awards: the QRZ.com and Custom confirmation sources actually work now. Both were offered in the award editor but neither was implemented, so ticking one marked no QSO as confirmed — silently. QRZ.com reads the download status (them confirming back, not our upload). Custom reads any QSO field or ADIF tag you name, with an optional list of values that count: point it at APP_OPSLOG_QSL_RCVD for a card received through OpsLog, or at a tag stamped by a club list you imported. Leave the value empty and any non-empty content confirms. A Custom source naming no field confirms nothing.",
|
||||
"Window: closing OpsLog while it is minimised no longer loses its size and position. Windows parks a minimised window at -32000,-32000 with a stub size, and that was what got saved — so the next launch found geometry no screen could hold, discarded it, and reopened at the default place. The stored geometry is now kept instead; it repairs itself the first time you close a window that is on screen."
|
||||
],
|
||||
@@ -13,6 +14,7 @@
|
||||
"Déjà contacté : les indicatifs portables d'un opérateur comptent désormais comme la même station. Taper RK3DWA retrouve aussi les QSO en RK3DWA/3, /P et /MM, et inversement — avant, l'historique n'apparaissait que si tu tapais la forme exacte. Désactivable dans Réglages → Général ; le contrôle de doublon en concours n'est pas touché et reste strict.",
|
||||
"Éditeur de QSO : la carte OpsLog rejoint les autres confirmations. Elle a sa propre ligne Envoyée / Reçue dans le tableau de l'onglet QSL Info, à côté de QSL, LoTW, eQSL et les autres, et la case « QSL reçue » y a été déplacée depuis Détails du contact — avec son indicateur PSE QSL / TNX, inchangé. « Envoyée » reste en lecture seule : OpsLog l'inscrit quand la carte part réellement.",
|
||||
"Carte QSL : le tampon {qso.pse_tnx} imprime désormais « TNX QSL » et non plus « TNX » seul, pour répondre à « PSE QSL ».",
|
||||
"UDP : un paquet illisible indique désormais qui l'a envoyé et ce qu'il contenait — adresse de l'émetteur, taille, aperçu en texte et vidage hexadécimal — au lieu d'un simple nombre magique. Et il s'arrête après cinq : un port recevant le mauvais trafic écrivait cette ligne cent fois par seconde, saturant le journal et enterrant tout le reste. Une ligne finale rappelle les deux choses à vérifier, l'émetteur et le type de service.",
|
||||
"Diplômes : les sources de confirmation QRZ.com et Custom fonctionnent enfin. Les deux étaient proposées dans l'éditeur de diplômes sans être implémentées : cocher l'une ou l'autre ne confirmait aucun QSO, en silence. QRZ.com lit le statut de téléchargement (leur confirmation en retour, pas notre envoi). Custom lit n'importe quel champ de QSO ou balise ADIF que tu désignes, avec une liste facultative de valeurs qui comptent : pointe-la sur APP_OPSLOG_QSL_RCVD pour une carte reçue via OpsLog, ou sur une balise inscrite à l'import d'une liste de club. Laisse la valeur vide et tout contenu non vide confirme. Une source Custom sans champ ne confirme rien.",
|
||||
"Fenêtre : fermer OpsLog alors qu'il est réduit ne fait plus perdre sa taille et sa position. Windows range une fenêtre réduite en -32000,-32000 avec une taille factice, et c'est cela qui était enregistré — au lancement suivant, OpsLog trouvait une géométrie qu'aucun écran ne peut contenir, la rejetait et rouvrait à l'emplacement par défaut. La géométrie déjà enregistrée est désormais conservée ; cela se répare tout seul à la première fermeture d'une fenêtre visible à l'écran."
|
||||
]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The diagnostic that matters: a text payload arriving on a WSJT port must be
|
||||
// READABLE in the log. "bad magic 0x3132372e" alone told the operator nothing —
|
||||
// those four bytes are ASCII "127.", i.e. some program broadcasting an address
|
||||
// where WSJT-X binary was expected.
|
||||
func TestDescribePacketShowsTextAndHex(t *testing.T) {
|
||||
got := describePacket([]byte("127.0.0.1:4532"))
|
||||
for _, want := range []string{`14 bytes`, `"127.0.0.1:4532"`, `31 32 37 2e`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("describePacket missing %q\ngot: %s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Binary stays inspectable: unprintable bytes become dots in the preview and
|
||||
// the hex carries the real values.
|
||||
func TestDescribePacketHandlesBinary(t *testing.T) {
|
||||
got := describePacket([]byte{0xad, 0xbc, 0xcb, 0xda, 0x00})
|
||||
if !strings.Contains(got, `"....."`) || !strings.Contains(got, "ad bc cb da 00") {
|
||||
t.Errorf("binary preview wrong: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A long datagram is truncated, and says so, rather than dumping a whole packet
|
||||
// into the log on every line.
|
||||
func TestDescribePacketTruncates(t *testing.T) {
|
||||
got := describePacket([]byte(strings.Repeat("A", 300)))
|
||||
if !strings.Contains(got, "300 bytes") {
|
||||
t.Errorf("lost the real length: %s", got)
|
||||
}
|
||||
if !strings.Contains(got, "…") {
|
||||
t.Errorf("truncation not marked: %s", got)
|
||||
}
|
||||
if strings.Count(got, "41 ") > 96 {
|
||||
t.Errorf("hex not capped: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,47 @@ type Server struct {
|
||||
// id is distinct whenever there is more than one.
|
||||
dialHz map[string]int64
|
||||
lastDX string // WSJT: last non-empty DX Call seen, to detect a clear
|
||||
|
||||
// badPkts counts datagrams this listener could not parse, so the diagnostic
|
||||
// dump below stays bounded. A misconfigured port is not a one-off: the
|
||||
// sender that produced "bad magic 0x3132372e" put out ~150 packets a second,
|
||||
// which fills the whole rotating log with the same line and buries the
|
||||
// evidence of anything else.
|
||||
badPkts int
|
||||
}
|
||||
|
||||
// maxBadPktDumps is how many unparseable datagrams a listener describes in full
|
||||
// before going quiet. Enough to identify the sender and the payload; few enough
|
||||
// that a permanently misconfigured port costs a handful of lines, not a log.
|
||||
const maxBadPktDumps = 5
|
||||
|
||||
// describePacket renders a datagram for the log: its size, a printable preview
|
||||
// and the first bytes in hex.
|
||||
//
|
||||
// Both forms, deliberately. "bad magic 0x3132372e" is already readable as ASCII
|
||||
// "127." to someone who thinks to decode it — and that one fact (the sender is
|
||||
// emitting text, not WSJT-X binary) is the whole diagnosis. The hex stays for
|
||||
// the case where the payload really is binary and the preview shows nothing.
|
||||
func describePacket(pkt []byte) string {
|
||||
const maxShown = 96
|
||||
head := pkt
|
||||
if len(head) > maxShown {
|
||||
head = head[:maxShown]
|
||||
}
|
||||
var text, hex strings.Builder
|
||||
for _, b := range head {
|
||||
if b >= 0x20 && b < 0x7f {
|
||||
text.WriteByte(b)
|
||||
} else {
|
||||
text.WriteByte('.')
|
||||
}
|
||||
fmt.Fprintf(&hex, "%02x ", b)
|
||||
}
|
||||
more := ""
|
||||
if len(pkt) > maxShown {
|
||||
more = "…"
|
||||
}
|
||||
return fmt.Sprintf("%d bytes | text %q%s | hex %s%s", len(pkt), text.String(), more, strings.TrimSpace(hex.String()), more)
|
||||
}
|
||||
|
||||
func newServer(cfg Config, out chan<- Event) *Server {
|
||||
@@ -201,13 +242,38 @@ func (s *Server) run() {
|
||||
}
|
||||
}
|
||||
|
||||
// logBadPacket reports a datagram this listener could not parse, with enough of
|
||||
// it to identify the sender — then falls silent.
|
||||
//
|
||||
// The point is the SENDER: an unparseable packet on a WSJT port almost always
|
||||
// means another program is broadcasting on it, or the service type is wrong for
|
||||
// what is actually arriving. The remote address names the culprit, and the
|
||||
// payload preview says what it really is. Neither was logged before, so the
|
||||
// operator saw only a magic number repeated a few hundred times a second.
|
||||
func (s *Server) logBadPacket(kind string, remote *net.UDPAddr, pkt []byte, err error) {
|
||||
s.mu.Lock()
|
||||
s.badPkts++
|
||||
n := s.badPkts
|
||||
s.mu.Unlock()
|
||||
if n > maxBadPktDumps {
|
||||
return
|
||||
}
|
||||
applog.Printf("udp: [%s] %s parse error from %s: %v — %s\n",
|
||||
s.cfg.Name, kind, remote, err, describePacket(pkt))
|
||||
if n == maxBadPktDumps {
|
||||
applog.Printf("udp: [%s] further unparseable packets on port %d will not be logged — "+
|
||||
"check that the sender belongs on this port and that the service type matches\n",
|
||||
s.cfg.Name, s.cfg.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
ev := Event{ConfigID: s.cfg.ID, Service: s.cfg.ServiceType, Source: remote.String()}
|
||||
switch s.cfg.ServiceType {
|
||||
case ServiceWSJT:
|
||||
w, ok, err := ParseWSJT(pkt)
|
||||
if err != nil {
|
||||
applog.Printf("udp: [%s] WSJT parse error: %v\n", s.cfg.Name, err)
|
||||
s.logBadPacket("WSJT", remote, pkt, err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
@@ -321,7 +387,7 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
case ServiceN1MM:
|
||||
adifText, ok, err := ParseN1MM(pkt)
|
||||
if err != nil {
|
||||
applog.Printf("udp: [%s] N1MM parse error: %v\n", s.cfg.Name, err)
|
||||
s.logBadPacket("N1MM", remote, pkt, err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user