/* Copyright 2024 Josh Deprez Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "cmp" "context" "errors" "flag" "fmt" "log" "math/rand/v2" "net" "net/http" "os" "os/signal" "regexp" "runtime/debug" "slices" "strings" "sync" "time" "gitea.drjosh.dev/josh/jrouter/aurp" "gitea.drjosh.dev/josh/jrouter/router" "gitea.drjosh.dev/josh/jrouter/status" "github.com/google/gopacket/pcap" "github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ethernet" ) const routingTableTemplate = ` {{range $route := . }} {{end}}
Network range Extended? Zone names Distance Last seen Port
{{$route.NetStart}}{{if not (eq $route.NetStart $route.NetEnd)}} - {{$route.NetEnd}}{{end}} {{if $route.Extended}}✅{{else}}-{{end}} {{range $route.ZoneNames.ToSlice}}{{.}}
{{end}}
{{$route.Distance}} {{$route.LastSeenAgo}} {{- with $route.AURPPeer -}} [redacted] {{- end -}} {{- with $route.EtherTalkPeer -}} {{.Port.Device}} {{.PeerAddr.Network}}.{{.PeerAddr.Node}} {{- end -}} {{- with $route.EtherTalkDirect -}} {{.Device}} {{- end -}}
` const peerTableTemplate = ` {{range $peer := . }} {{end}}
Configured addr Remote addr Receiver state Sender state Last heard from Last reconnect Last update Last send Send retries
[redacted] [redacted] {{$peer.ReceiverState}} {{$peer.SenderState}} {{$peer.LastHeardFromAgo}} {{$peer.LastReconnectAgo}} {{$peer.LastUpdateAgo}} {{$peer.LastSendAgo}} {{$peer.SendRetries}}
` var hasPortRE = regexp.MustCompile(`:\d+$`) var configFilePath = flag.String("config", "jrouter.yaml", "Path to configuration file to use") func main() { // For some reason it occasionally panics and the panics have no traceback? debug.SetTraceback("all") flag.Parse() log.Println("jrouter") cfg, err := router.LoadConfig(*configFilePath) if err != nil { log.Fatalf("Couldn't load configuration file: %v", err) } localIP := net.ParseIP(cfg.LocalIP).To4() if localIP == nil { iaddrs, err := net.InterfaceAddrs() if err != nil { log.Fatalf("Couldn't read network interface addresses: %v", err) } for _, iaddr := range iaddrs { inet, ok := iaddr.(*net.IPNet) if !ok { continue } if !inet.IP.IsGlobalUnicast() { continue } localIP = inet.IP.To4() if localIP != nil { break } } if localIP == nil { log.Fatalf("No global unicast IPv4 addresses on any network interfaces, and no valid local_ip address in configuration") } } localDI := aurp.IPDomainIdentifier(localIP) // log.Printf("Using %v as local domain identifier", localIP) // log.Printf("EtherTalk configuration: %+v", cfg.EtherTalk) ln, err := net.ListenUDP("udp4", &net.UDPAddr{Port: int(cfg.ListenPort)}) if err != nil { log.Fatalf("AURP: Couldn't listen on udp4:387: %v", err) } defer ln.Close() log.Printf("AURP: Listening on %v", ln.LocalAddr()) log.Println("Press ^C or send SIGINT to stop the router gracefully") cctx, cancel := context.WithCancel(context.Background()) defer cancel() ctx, _ := signal.NotifyContext(cctx, os.Interrupt) // --------------------------------- HTTP --------------------------------- http.HandleFunc("/status", status.Handle) go func() { log.Print(http.ListenAndServe(":9459", nil)) }() // --------------------------------- Pcap --------------------------------- // First check the interface iface, err := net.InterfaceByName(cfg.EtherTalk.Device) if err != nil { log.Fatalf("Couldn't find interface named %q: %v", cfg.EtherTalk.Device, err) } myHWAddr := ethernet.Addr(iface.HardwareAddr) if cfg.EtherTalk.EthAddr != "" { // Override myHWAddr with the configured address netHWAddr, err := net.ParseMAC(cfg.EtherTalk.EthAddr) if err != nil { log.Fatalf("Couldn't parse ethertalk.ethernet_addr value %q: %v", cfg.EtherTalk.EthAddr, err) } myHWAddr = ethernet.Addr(netHWAddr) } pcapHandle, err := pcap.OpenLive(cfg.EtherTalk.Device, 4096, true, 100*time.Millisecond) if err != nil { log.Fatalf("Couldn't open %q for packet capture: %v", cfg.EtherTalk.Device, err) } bpfFilter := fmt.Sprintf("(atalk or aarp) and (ether multicast or ether dst %s)", myHWAddr) if err := pcapHandle.SetBPFFilter(bpfFilter); err != nil { pcapHandle.Close() log.Fatalf("Couldn't set BPF filter on packet capture: %v", err) } defer pcapHandle.Close() // -------------------------------- Tables -------------------------------- routes := router.NewRouteTable() status.AddItem(ctx, "Routing table", routingTableTemplate, func(context.Context) (any, error) { rs := routes.Dump() slices.SortFunc(rs, func(ra, rb router.Route) int { return cmp.Compare(ra.NetStart, rb.NetStart) }) return rs, nil }) // -------------------------------- Peers --------------------------------- var peersMu sync.Mutex peers := make(map[udpAddr]*router.AURPPeer) status.AddItem(ctx, "AURP Peers", peerTableTemplate, func(context.Context) (any, error) { var peerInfo []*router.AURPPeer func() { peersMu.Lock() defer peersMu.Unlock() peerInfo = make([]*router.AURPPeer, 0, len(peers)) for _, p := range peers { peerInfo = append(peerInfo, p) } }() slices.SortFunc(peerInfo, func(pa, pb *router.AURPPeer) int { return cmp.Or( -cmp.Compare( bool2Int(pa.ReceiverState() == router.ReceiverConnected), bool2Int(pb.ReceiverState() == router.ReceiverConnected), ), -cmp.Compare( bool2Int(pa.SenderState() == router.SenderConnected), bool2Int(pb.SenderState() == router.SenderConnected), ), cmp.Compare(pa.ConfiguredAddr, pb.ConfiguredAddr), ) }) return peerInfo, nil }) var nextConnID uint16 for nextConnID == 0 { nextConnID = uint16(rand.IntN(0x10000)) } var wg sync.WaitGroup goPeerHandler := func(p *router.AURPPeer) { wg.Add(1) go func() { defer wg.Done() p.Handle(ctx) }() } // ------------------------- Configured peer setup ------------------------ if cfg.PeerListURL != "" { log.Print("Fetching peer list...") existing := len(cfg.Peers) func() { resp, err := http.Get(cfg.PeerListURL) if err != nil { log.Fatalf("Couldn't fetch peer list!") } defer resp.Body.Close() sc := bufio.NewScanner(resp.Body) for sc.Scan() { p := strings.TrimSpace(sc.Text()) if p == "" { continue } cfg.Peers = append(cfg.Peers, p) } if err := sc.Err(); err != nil { log.Fatalf("Couldn't scan peer list response: %v", err) } }() log.Printf("Fetched list containing %d peers", len(cfg.Peers)-existing) } for _, peerStr := range cfg.Peers { if !hasPortRE.MatchString(peerStr) { peerStr += ":387" } raddr, err := net.ResolveUDPAddr("udp4", peerStr) if err != nil { log.Print("couldn't resolve UDP address, skipping peer") continue } //log.Printf("resolved %q to %v", peerStr, raddr) if raddr.IP.Equal(localIP) { //log.Print("peer == me, skipping") continue } peer := &router.AURPPeer{ Transport: &aurp.Transport{ LocalDI: localDI, RemoteDI: aurp.IPDomainIdentifier(raddr.IP), LocalConnID: nextConnID, }, UDPConn: ln, ConfiguredAddr: peerStr, RemoteAddr: raddr, ReceiveCh: make(chan aurp.Packet, 1024), RouteTable: routes, } aurp.Inc(&nextConnID) peersMu.Lock() peers[udpAddrFromNet(raddr)] = peer peersMu.Unlock() goPeerHandler(peer) } // --------------------------------- AARP --------------------------------- aarpMachine := router.NewAARPMachine(cfg, pcapHandle, myHWAddr) go aarpMachine.Run(ctx) // -------------------------------- Router -------------------------------- rooter := &router.Router{ Config: cfg, RouteTable: routes, // ZoneTable: zones, } etherTalkPort := &router.EtherTalkPort{ Device: cfg.EtherTalk.Device, EthernetAddr: myHWAddr, NetStart: cfg.EtherTalk.NetStart, NetEnd: cfg.EtherTalk.NetEnd, DefaultZoneName: cfg.EtherTalk.ZoneName, AvailableZones: router.SetFromSlice([]string{cfg.EtherTalk.ZoneName}), PcapHandle: pcapHandle, AARPMachine: aarpMachine, Router: rooter, } rooter.Ports = append(rooter.Ports, etherTalkPort) routes.InsertEtherTalkDirect(etherTalkPort) // --------------------------------- RTMP --------------------------------- go etherTalkPort.RunRTMP(ctx) // ---------------------- Raw AppleTalk/AARP inbound ---------------------- wg.Add(1) go func() { defer wg.Done() ctx, setStatus, _ := status.AddSimpleItem(ctx, "EtherTalk inbound") defer setStatus("EtherTalk Serve goroutine exited!") setStatus(fmt.Sprintf("Listening on %s", cfg.EtherTalk.Device)) etherTalkPort.Serve(ctx) }() // ----------------------------- AURP inbound ----------------------------- wg.Add(1) go func() { defer wg.Done() ctx, setStatus, done := status.AddSimpleItem(ctx, "AURP inbound") defer done() setStatus(fmt.Sprintf("Listening on UDP port %d", cfg.ListenPort)) for { if ctx.Err() != nil { return } ln.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) pktbuf := make([]byte, 4096) pktlen, raddr, readErr := ln.ReadFromUDP(pktbuf) var operr *net.OpError if errors.As(readErr, &operr) && operr.Timeout() { continue } // log.Printf("AURP: Received packet of length %d from %v", pktlen, raddr) dh, pkt, parseErr := aurp.ParsePacket(pktbuf[:pktlen]) if parseErr != nil { log.Printf("AURP: Failed to parse packet: %v", parseErr) continue } if readErr != nil { log.Printf("AURP: Failed to read packet: %v", readErr) return } // log.Printf("AURP: Got %T from %v (%v)", pkt, raddr, dh.SourceDI) // Existing peer? ra := udpAddrFromNet(raddr) peersMu.Lock() pr := peers[ra] if pr == nil { if !cfg.OpenPeering { log.Print("AURP: Got packet but it's not in my config and open peering is disabled; dropping the packet") peersMu.Unlock() continue } // New peer! pr = &router.AURPPeer{ Transport: &aurp.Transport{ LocalDI: localDI, RemoteDI: dh.SourceDI, // platinum rule LocalConnID: nextConnID, }, UDPConn: ln, RemoteAddr: raddr, ReceiveCh: make(chan aurp.Packet, 1024), RouteTable: routes, } aurp.Inc(&nextConnID) peers[ra] = pr goPeerHandler(pr) } peersMu.Unlock() switch dh.PacketType { case aurp.PacketTypeRouting: // It's AURP routing data. // Pass the packet to the goroutine in charge of this peer. select { case pr.ReceiveCh <- pkt: // That's it for us. case <-ctx.Done(): return } continue case aurp.PacketTypeAppleTalk: apkt, ok := pkt.(*aurp.AppleTalkPacket) if !ok { log.Printf("AURP: Got %T but domain header packet type was %v ?", pkt, dh.PacketType) continue } // Route or otherwise handle the encapsulated AppleTalk traffic ddpkt := new(ddp.ExtPacket) if err := ddp.ExtUnmarshal(apkt.Data, ddpkt); err != nil { log.Printf("AURP: Couldn't unmarshal encapsulated DDP packet: %v", err) continue } // log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d", // ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket, // ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket, // ddpkt.Proto, len(ddpkt.Data)) // Is it addressed to me? var localPort *router.EtherTalkPort for _, port := range rooter.Ports { if ddpkt.DstNet >= port.NetStart && ddpkt.DstNet <= port.NetEnd { localPort = port break } } if ddpkt.DstNode == 0 && localPort != nil { // Node 0 = any router for the network = me // Is it NBP? FwdReq needs translating. if ddpkt.DstSocket != 2 { // Something else?? TODO log.Printf("DDP/AURP: I don't have anything 'listening' on socket %d", ddpkt.DstSocket) continue } // It's NBP, specifically it should be a FwdReq if err := rooter.HandleNBPFromAURP(ctx, ddpkt); err != nil { log.Printf("NBP/DDP/AURP: %v", err) } continue } // Route the packet! if err := rooter.Forward(ctx, ddpkt); err != nil { log.Printf("DDP/AURP: Couldn't route packet: %v", err) } default: log.Printf("AURP: Got unknown packet type %v", dh.PacketType) } } }() // -------------------------------- Close --------------------------------- wg.Wait() } // Hashable net.UDPAddr type udpAddr struct { ipv4 [4]byte port uint16 } func udpAddrFromNet(a *net.UDPAddr) udpAddr { return udpAddr{ ipv4: [4]byte(a.IP.To4()), port: uint16(a.Port), } } func (u udpAddr) toNet() *net.UDPAddr { return &net.UDPAddr{ IP: u.ipv4[:], Port: int(u.port), } } func bool2Int(b bool) int { if b { return 1 } return 0 }