diff --git a/.gitignore b/.gitignore index 48e7fa6..3d00455 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ vendor/ **__debug_bin* # `__debug_bin` Debug binary generated in VSCode when using the built-in debugger. *bin +/xnet /bridge # IDE .vscode/ diff --git a/README.md b/README.md index 0fdc655..68e38fb 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ If you can use the `net` package, use it. Need something faster and less-heap al - [`lneto/dns`](./dns): DNS protocol implementation and low level logic. - [`lneto/ntp`](./ntp): NTP implementation and low level logic. Includes NTP time primitives manipulation and conversion to Go native types. - [`lneto/internal`](./internal): Lightweight and flexible ring buffer implementation and debugging primitives. +- [`lneto/x`](./x): Experimental packages. + - [`lneto/x/xnet`](./x/xnet/): `net` package like abstractions of stack implementations for ease of reuse. Still in testing phase and likely subject to breaking API change. ### Abstractions The following interface is implemented by networking stack nodes and the stack themselves. diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..2da5133 --- /dev/null +++ b/errors.go @@ -0,0 +1,16 @@ +package lneto + +// type ErrorPacketDrop struct { +// Message string +// } + +// var genericErrPacketDrop = &ErrorPacketDrop{Message: "lneto: packet dropped"} + +// // ErrGenericPacketDrop returns the generic packet drop error. It performs no allocations. +// func ErrGenericPacketDrop() error { +// return genericErrPacketDrop +// } + +// func (err *ErrorPacketDrop) Error() string { +// return err.Message +// } diff --git a/examples/xnet/main.go b/examples/xnet/main.go new file mode 100644 index 0000000..859b704 --- /dev/null +++ b/examples/xnet/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net" + "net/netip" + "os" + "runtime" + "strings" + "time" + + "github.com/soypat/lneto/dns" + "github.com/soypat/lneto/internal" + "github.com/soypat/lneto/internal/ltesto" + "github.com/soypat/lneto/x/xnet" +) + +var softRand = time.Now().Unix() + +func main() { + err := run() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("success") +} + +func run() (err error) { + var ( + flagInterface = "tap0" + flagUseHTTP = false + flagHostToResolve = "" + flagRequestedIP = "" + flagDoNTP = false + ) + flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.") + flag.BoolVar(&flagUseHTTP, "http", flagUseHTTP, "Use HTTP tap interface.") + flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.") + flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.") + flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time") + flag.Parse() + fmt.Println("softrand", softRand) + _, err = dns.NewName(flagHostToResolve) + if err != nil { + flag.Usage() + return err + } + var iface ltesto.Interface + if flagUseHTTP { + iface = ltesto.NewHTTPTapClient("http://127.0.0.1:7070") + } else { + if strings.HasPrefix(flagInterface, "tap") { + tap, err := internal.NewTap(flagInterface, netip.MustParsePrefix("192.168.1.1/24")) + if err != nil { + return err + } + iface = tap + } else { + bridge, err := internal.NewBridge(flagInterface) + if err != nil { + return err + } + iface = bridge + } + } + defer iface.Close() + + nicHW, err := iface.HardwareAddress6() + if err != nil { + return err + } + brHW := nicHW + mtu, err := iface.MTU() + if err != nil { + return err + } + + nicAddr, err := iface.IPMask() + if err != nil { + return err + } + fmt.Println("NIC hardware address:", net.HardwareAddr(nicHW[:]).String(), "bridgeHW:", net.HardwareAddr(brHW[:]).String(), "mtu:", mtu, "addr:", nicAddr.String()) + var stack xnet.StackAsync + err = stack.Reset(xnet.StackConfig{ + Hostname: "xnet-test", + RandSeed: softRand, + HardwareAddress: brHW, + MTU: uint16(mtu), + }) + if err != nil { + return err + } + // Loop goroutine. + go func() { + lastAction := time.Now() + buf := make([]byte, mtu) + for { + clear(buf) + nwrite, err := stack.Encapsulate(buf[:], 0) + if err != nil { + fmt.Println("ERR:ENCAPSULATE", err) + } else if nwrite > 0 { + n, err := iface.Write(buf[:nwrite]) + if err != nil { + log.Fatal("groutine encapsulate:", err) + } else if n != nwrite { + log.Fatalf("mismatch written bytes %d!=%d", nwrite, n) + } + } + + clear(buf) + nread, err := iface.Read(buf) + if err != nil { + log.Fatal("groutine read:", err) + } else if nread > 0 { + err = stack.Demux(buf[:nread], 0) + if err != nil { + log.Println("groutine demux:", err) + } + } + + if nread == 0 && nwrite == 0 && time.Since(lastAction) > 4*time.Second { + time.Sleep(5 * time.Millisecond) + } else { + lastAction = time.Now() + runtime.Gosched() + } + } + }() + + rstack := stack.StackRetrying() + + const ( + dhcpTimeout = 6 * time.Second + dhcpRetries = 2 + ) + results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries) + if err != nil { + return fmt.Errorf("DHCP failed: %w", err) + } + err = stack.AssimilateDHCPResults(results) + if err != nil { + return fmt.Errorf("assimilating DHCP results: %w", err) + } + const ( + arpTimeout = 2 * time.Second + arpRetries = 2 + ) + const ( + internetTimeout = 3 * time.Second + internetRetries = 2 + ) + routerHw, err := rstack.DoResolveHardwareAddress6(results.Router, arpTimeout, arpRetries) + if err != nil { + return fmt.Errorf("ARP resolution of router failed: %w", err) + } + stack.SetGateway6(routerHw) + if flagDoNTP { + const ntpHost = "pool.ntp.org" + addrs, err := rstack.DoLookupIP(ntpHost, internetTimeout, internetRetries) + if err != nil { + return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err) + } + offset, err := rstack.DoNTP(addrs[0], internetTimeout, internetRetries) + if err != nil { + return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err) + } + relative := "behind" + if offset < 0 { + relative = "ahead" + } + fmt.Println("NTP completed. You are", offset.Abs().String(), relative, "of the NTP server") + } + addrs, err := rstack.DoLookupIP(flagHostToResolve, internetTimeout, internetRetries) + if err != nil { + return fmt.Errorf("DNS of host %q failed: %w", flagHostToResolve, err) + } + fmt.Printf("DNS resolution of %q complete and resolved to %v\n", flagHostToResolve, addrs) + return nil +} + +func clear(buf []byte) { + for i := range buf { + buf[i] = 0 + } +} diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go new file mode 100644 index 0000000..5aed0a7 --- /dev/null +++ b/x/xnet/stack-async.go @@ -0,0 +1,456 @@ +package xnet + +import ( + "errors" + "net/netip" + "sync" + "time" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/arp" + "github.com/soypat/lneto/dhcpv4" + "github.com/soypat/lneto/dns" + "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/internet" + "github.com/soypat/lneto/ntp" + "github.com/soypat/lneto/tcp" +) + +type StackAsync struct { + mu sync.Mutex + hostname string + clientID string + link internet.StackEthernet + ip internet.StackIP + arp arp.Handler + udps internet.StackPorts + tcps internet.StackPorts + tcpconns []tcp.Conn + + dhcpUDP internet.StackUDPPort + dhcp dhcpv4.Client + dhcpResults DHCPResults + + dnsUDP internet.StackUDPPort + dns dns.Client + ednsopt dns.Resource + lookup dns.Message + dnssv netip.Addr + + ntpUDP internet.StackUDPPort + ntp ntp.Client + + sysprec int8 // NTP system precision. + + prng uint32 + lastrecv uint16 +} + +type StackConfig struct { + StaticAddress netip.Addr + DNSServer netip.Addr + NTPServer netip.Addr + Hostname string + MaxTCPConns int + RandSeed int64 + HardwareAddress [6]byte + MTU uint16 +} + +func (s *StackAsync) Hostname() string { + return s.hostname +} + +func (s *StackAsync) Demux(carrierData []byte, etherOff int) error { + s.mu.Lock() + defer s.mu.Unlock() + s.lastrecv = uint16(len(carrierData)) + return s.link.Demux(carrierData, etherOff) +} + +func (s *StackAsync) Encapsulate(carrierData []byte, etherOff int) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.link.Encapsulate(carrierData, etherOff) +} + +func (s *StackAsync) Reset(cfg StackConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + mac := cfg.HardwareAddress + mtu := cfg.MTU + addr := cfg.StaticAddress + s.prng = uint32(cfg.RandSeed) + if s.prng == 0 { + return errors.New("zero random seed") + } + s.hostname = cfg.Hostname + if !addr.IsValid() { + addr = netip.AddrFrom4([4]byte{}) // If static not set DHCP will be performed and address will be zero. + } else if addr.Is6() { + return errors.New("IPv6 unsupported as of yet") + } + const linkNodes = 2 // ARP and IP nodes + err := s.link.Reset6(mac, ethernet.BroadcastAddr(), int(mtu), linkNodes) + if err != nil { + return err + } + const ipNodes = 2 // UDP, TCP ports. + err = s.ip.Reset(addr, ipNodes) + if err != nil { + return err + } + err = s.resetARP() + if err != nil { + return err + } + const udpMaintenanceConns = 3 // DHCP, DNS, NTP. + err = s.udps.ResetUDP(udpMaintenanceConns) + if err != nil { + return err + } + + // Enable TCP if connections present. + if cfg.MaxTCPConns > 0 { + if cap(s.tcpconns) < cfg.MaxTCPConns { + s.tcpconns = make([]tcp.Conn, cfg.MaxTCPConns) + } + err = s.tcps.ResetTCP(cfg.MaxTCPConns) + if err != nil { + return err + } + err = s.ip.Register(&s.tcps) + if err != nil { + return err + } + } + + // Now setup stacks. + err = s.link.Register(&s.arp) // ARP. + if err != nil { + return err + } + err = s.link.Register(&s.ip) // IPv4 | IPv6 + if err != nil { + return err + } + err = s.ip.Register(&s.udps) + if err != nil { + return err + } + var timebuf [32]time.Time + s.sysprec = ntp.CalculateSystemPrecision(time.Now, timebuf[:]) + if s.clientID == "" { + s.clientID = "lneto-" + s.hostname + } + return nil +} + +var errInvalidIPAddr = errors.New("invaldi IP address") + +func (s *StackAsync) resetARP() error { + mac := s.link.HardwareAddr6() + addr := s.ip.Addr() + if !addr.IsValid() { + return errInvalidIPAddr + } + proto := ethernet.TypeIPv4 + if addr.Is6() { + proto = ethernet.TypeIPv6 + } + return s.arp.Reset(arp.HandlerConfig{ + HardwareAddr: mac[:], + ProtocolAddr: addr.AsSlice(), + MaxQueries: 3, + MaxPending: 3, + HardwareType: 1, + ProtocolType: proto, + }) +} + +// Prand32 generates a pseudo random 32-bit unsigned integer from the internal state and advances the seed. +func (s *StackAsync) Prand32() uint32 { + /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */ + seed := s.prng + seed ^= seed << 13 + seed ^= seed >> 17 + seed ^= seed << 5 + s.prng = seed + return seed +} + +func (s *StackAsync) SetIPAddr(addr netip.Addr) error { + s.mu.Lock() + defer s.mu.Unlock() + err := s.ip.SetAddr(addr) + if err != nil { + return err + } + return s.resetARP() +} + +func (s *StackAsync) SetHardwareAddress(hw [6]byte) error { + s.mu.Lock() + defer s.mu.Unlock() + s.link.SetHardwareAddr6(hw) + return s.resetARP() +} + +func (s *StackAsync) SetGateway6(gwhw [6]byte) { + s.mu.Lock() + defer s.mu.Unlock() + s.link.SetGateway6(gwhw) +} + +var ( + errNoTCP = errors.New("no TCP initialized") + errNoTCPConnsAvail = errors.New("all allocated TCP connections busy") +) + +func (s *StackAsync) DialTCP(localPort uint16, addrp netip.AddrPort) (conn *tcp.Conn, err error) { + if len(s.tcpconns) == 0 { + return nil, errNoTCP + } + for i := range s.tcpconns { + maybeFreeConn := &s.tcpconns[i] + state := conn.State() + if state.IsClosed() { + conn = maybeFreeConn + break // Can be used! + } + } + if conn == nil { + return nil, errNoTCPConnsAvail + } + conn.Abort() // Conn is closed, safe to abort. + err = conn.OpenActive(localPort, addrp, tcp.Value(s.Prand32())) + if err != nil { + conn.Abort() + return nil, err + } + err = s.tcps.Register(conn) + if err != nil { + conn.Abort() + return nil, err + } + return conn, nil +} + +var errNoDNSServer = errors.New("no DNS server- did DHCP complete? You can set a predetermined DNS server in Stack configuration") + +func (s *StackAsync) StartLookupIP(host string) error { + s.mu.Lock() + defer s.mu.Unlock() + if !s.dnssv.IsValid() { + return errNoDNSServer + } + name, err := dns.NewName(host) + if err != nil { + return err + } + + s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil) + rand := s.Prand32() + err = s.dns.StartResolve(uint16(rand>>1)+1024, uint16(rand), dns.ResolveConfig{ + Questions: []dns.Question{ + { + Name: name, + Type: dns.TypeA, + Class: dns.ClassINET, + }, + }, + Additional: []dns.Resource{ + s.ednsopt, + }, + EnableRecursion: true, + }) + if err != nil { + return err + } + dns4 := s.dnssv.As4() + s.dnsUDP.SetStackNode(&s.dns, dns4[:], dns.ServerPort) + err = s.udps.Register(&s.dnsUDP) + return err +} + +var errDNSNotDone = errors.New("DNS not done") + +func (s *StackAsync) ResultLookupIP(host string) ([]netip.Addr, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + done, err := s.dns.MessageCopyTo(&s.lookup) + if err != nil { + return nil, done, err + } else if !done { + return nil, done, errDNSNotDone + } + + var addrs []netip.Addr + ans := s.lookup.Answers + for i := range ans { + data := ans[i].RawData() + if len(data) == 4 { + addrs = append(addrs, netip.AddrFrom4([4]byte(data))) + } else if len(data) == 16 { + addrs = append(addrs, netip.AddrFrom16([16]byte(data))) + } else { + return addrs, done, errors.New("bogus IP") + } + } + return addrs, done, nil +} + +func (s *StackAsync) StartDHCPv4Request(request [4]byte) error { + s.mu.Lock() + defer s.mu.Unlock() + s.dhcp.Reset() + xid := s.Prand32() + err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{ + RequestedAddr: request, + ClientHardwareAddr: s.link.HardwareAddr6(), + Hostname: s.hostname, + ClientID: s.clientID, + }) + if err != nil { + return err + } + + s.dhcpUDP.SetStackNode(&s.dhcp, nil, dhcpv4.DefaultServerPort) + err = s.udps.Register(&s.dhcpUDP) + if err != nil { + return err + } + return err +} + +func (s *StackAsync) StartNTP(addr netip.Addr) error { + s.mu.Lock() + defer s.mu.Unlock() + s.ntp.Reset(s.sysprec, time.Now) + + addr4 := addr.As4() + s.ntpUDP.SetStackNode(&s.ntp, addr4[:], ntp.ServerPort) + err := s.udps.Register(&s.ntpUDP) + return err +} + +// ResultNTPOffset returns the result of the NTP protocol such that the following code returns the corrected time. +// If the bool is false then the NTP has not yet completed. +// +// nowCorrected := time.Now().Add(resultNTP) +func (s *StackAsync) ResultNTPOffset() (time.Duration, bool) { + s.mu.Lock() + defer s.mu.Unlock() + return s.ntp.Offset(), s.ntp.IsDone() +} + +func (s *StackAsync) StartResolveHardwareAddress6(ip netip.Addr) error { + s.mu.Lock() + defer s.mu.Unlock() + if !ip.Is4() { + return errors.New("unsupported or invalid IP address") + } + addr := ip.As4() + return s.arp.StartQuery(addr[:]) +} + +// ResultResolveHardwareAddress6 +func (s *StackAsync) ResultResolveHardwareAddress6(ip netip.Addr) (hw [6]byte, err error) { + s.mu.Lock() + defer s.mu.Unlock() + if !ip.Is4() { + return hw, errors.New("unsupported or invalid IP address") + } + addr := ip.As4() + hwslice, err := s.arp.QueryResult(addr[:]) + if err != nil { + return hw, err + } else if len(hwslice) != 6 { + panic("unreachable slice hw length") + } + return [6]byte(hwslice), nil +} + +type DHCPResults struct { + DNSServers []netip.Addr + Router netip.Addr + AssignedAddr netip.Addr + ServerAddr netip.Addr + BroadcastAddr netip.Addr + Gateway netip.Addr + Subnet netip.Prefix + TRebind uint32 // [seconds] + TRenewal uint32 + TLease uint32 // IP lease time [seconds]. +} + +func (s *StackAsync) ResultDHCP() (*DHCPResults, error) { + err := s.populateDHCPResults() + if err != nil { + return nil, err + } + return &s.dhcpResults, nil +} + +// AssimilateDHCPResults sets the stack's following parameters: +// - IPv4 address. +// - DNS server. +func (stack *StackAsync) AssimilateDHCPResults(results *DHCPResults) error { + stack.mu.Lock() + defer stack.mu.Unlock() + if results.AssignedAddr.IsValid() { + err := stack.ip.SetAddr(results.AssignedAddr) + if err != nil { + return err + } + } + if len(results.DNSServers) > 0 { + if !results.DNSServers[0].IsValid() || !results.DNSServers[0].Is4() { + return errors.New("bad DNS server address, IPv6 or invalid") + } + stack.dnssv = results.DNSServers[0] + } + return nil +} + +func (s *StackAsync) populateDHCPResults() error { + if !s.dhcp.State().HasIP() { + return errors.New("DHCP not completed") + } + router4, ok := s.dhcp.RouterAddr() + if !ok { + return errors.New("no DHCP router address") + } + assigned4, ok := s.dhcp.AssignedAddr() + if !ok { + return errors.New("no DHCP assigned address") + } + router := netip.AddrFrom4(router4) + s.dhcpResults = DHCPResults{ + Router: router, + Subnet: netip.PrefixFrom(router, int(s.dhcp.SubnetCIDRBits())), + AssignedAddr: netip.AddrFrom4(assigned4), + ServerAddr: addr4(s.dhcp.ServerAddr()), + BroadcastAddr: addr4(s.dhcp.BroadcastAddr()), + Gateway: addr4(s.dhcp.GatewayAddr()), + TRebind: s.dhcp.RebindingSeconds(), + TRenewal: s.dhcp.RenewalSeconds(), + TLease: s.dhcp.IPLeaseSeconds(), + DNSServers: s.dhcpResults.DNSServers[:0], // reuse field capacity. + } + s.dhcpResults.DNSServers = s.dhcp.AppendDNSServers(s.dhcpResults.DNSServers) + return nil +} + +func addr4(addr [4]byte, ok bool) netip.Addr { + if !ok { + return netip.Addr{} + } + return netip.AddrFrom4(addr) +} + +func hash(b []byte) uint16 { + var csum lneto.CRC791 + csum.Write(b) + return csum.Sum16() +} diff --git a/x/xnet/stack-blocking.go b/x/xnet/stack-blocking.go new file mode 100644 index 0000000..a52a5a7 --- /dev/null +++ b/x/xnet/stack-blocking.go @@ -0,0 +1,147 @@ +package xnet + +import ( + "errors" + "net/netip" + "time" + + "github.com/soypat/lneto/dhcpv4" + "github.com/soypat/lneto/tcp" +) + +const ( + maxIter = 1000 + maxTimeout = time.Minute + maxSleep = maxTimeout / maxIter +) + +var ( + errDeadlineExceed = errors.New("cywnet: deadline exceeded") +) + +func (s *StackAsync) StackBlocking() StackBlocking { + return StackBlocking{ + async: s, + } +} + +type StackBlocking struct { + async *StackAsync +} + +func (s StackBlocking) DoDHCPv4(reqAddr [4]byte, timeout time.Duration) (*DHCPResults, error) { + err := s.async.StartDHCPv4Request(reqAddr) + if err != nil { + return nil, err + } + sleep := timeout/maxIter + 1 + deadline := time.Now().Add(timeout) + requested := false + for i := 0; i < maxIter; i++ { + state := s.async.dhcp.State() + requested = requested || state > dhcpv4.StateInit + if requested && state == dhcpv4.StateInit { + return nil, errors.New("DHCP NACK") + } else if state == dhcpv4.StateBound { + break // DHCP done succesfully. + } else if err = s.checkDeadline(deadline); err != nil { + return nil, err + } + time.Sleep(sleep) + } + return s.async.ResultDHCP() +} + +func (s StackBlocking) DoNTP(hostAddr netip.Addr, timeout time.Duration) (offset time.Duration, err error) { + err = s.async.StartNTP(hostAddr) + if err != nil { + return -1, err + } + sleep := timeout/maxIter + 1 + deadline := time.Now().Add(timeout) + var done bool + for i := 0; i < maxIter; i++ { + offset, done = s.async.ResultNTPOffset() + if done { + return offset, nil + } else if err = s.checkDeadline(deadline); err != nil { + return -1, err + } + time.Sleep(sleep) + } + return -1, errDeadlineExceed +} + +func (s StackBlocking) DoResolveHardwareAddress6(addr netip.Addr, timeout time.Duration) (hw [6]byte, err error) { + err = s.async.StartResolveHardwareAddress6(addr) + if err != nil { + return hw, err + } + sleep := timeout/maxIter + 1 + deadline := time.Now().Add(timeout) + for i := 0; i < maxIter; i++ { + hw, err = s.async.ResultResolveHardwareAddress6(addr) + if err == nil { + break + } else if err = s.checkDeadline(deadline); err != nil { + return hw, err + } + time.Sleep(sleep) + err = errDeadlineExceed // Ensure that if iterations done error is returned. + } + return hw, err +} + +func (s StackBlocking) DoLookupIP(host string, timeout time.Duration) (addrs []netip.Addr, err error) { + err = s.async.StartLookupIP(host) + if err != nil { + return nil, err + } + sleep := timeout/maxIter + 1 + deadline := time.Now().Add(timeout) + for i := 0; i < maxIter; i++ { + addrs, completed, err := s.async.ResultLookupIP(host) + if completed { + return addrs, err + } else if err = s.checkDeadline(deadline); err != nil { + return nil, err + } + time.Sleep(sleep) + } + return nil, errDeadlineExceed +} + +var errTCPFailedToConnect = errors.New("tcp failed to connect") + +func (s StackBlocking) DoDialTCP(localPort uint16, addrp netip.AddrPort, timeout time.Duration) (conn *tcp.Conn, err error) { + conn, err = s.async.DialTCP(localPort, addrp) + if err != nil { + return nil, err + } + sleep := timeout/maxIter + 1 + deadline := time.Now().Add(timeout) + for i := 0; i < maxIter; i++ { + state := conn.State() + switch state { + case tcp.StateEstablished: + break + case tcp.StateSynSent, tcp.StateSynRcvd: + if err = s.checkDeadline(deadline); err != nil { + return nil, err + } + time.Sleep(sleep) + default: + // Unexpected state, abort and terminate connection. + conn.Abort() + return nil, errTCPFailedToConnect + } + } + return conn, nil +} + +func (s StackBlocking) checkDeadline(deadline time.Time) error { + if time.Since(deadline) > 0 { + return errDeadlineExceed + } + return nil +} diff --git a/x/xnet/stack-retrying.go b/x/xnet/stack-retrying.go new file mode 100644 index 0000000..4866a61 --- /dev/null +++ b/x/xnet/stack-retrying.go @@ -0,0 +1,98 @@ +package xnet + +import ( + "errors" + "net/netip" + "time" + + "github.com/soypat/lneto/tcp" +) + +func (s *StackAsync) StackRetrying() StackRetrying { + return StackRetrying{ + block: s.StackBlocking(), + } +} + +var ( + errRetriesExceeded = errors.New("cywnet: retries exceeded") +) + +type StackRetrying struct { + block StackBlocking +} + +func (s StackRetrying) DoDHCPv4(reqAddr [4]byte, timeout time.Duration, retries int) (results *DHCPResults, err error) { + expectEnd := time.Now().Add(timeout * time.Duration(retries)) + for i := 0; i < retries; i++ { + if i > 0 { + println("Retrying DHCP") + } + results, err = s.block.DoDHCPv4(reqAddr, timeout) + if err == nil { + return results, nil + } + } + if time.Now().Before(expectEnd) { + return nil, err + } + return nil, errRetriesExceeded +} + +func (s StackRetrying) DoNTP(ntpHost netip.Addr, timeout time.Duration, retries int) (offset time.Duration, err error) { + expectEnd := time.Now().Add(timeout * time.Duration(retries)) + for i := 0; i < retries; i++ { + if i > 0 { + println("Retrying DHCP") + } + offset, err = s.block.DoNTP(ntpHost, timeout) + if err == nil { + return offset, nil + } + } + if time.Now().Before(expectEnd) { + return -1, err + } + return -1, errRetriesExceeded +} +func (s StackRetrying) DoLookupIP(host string, timeout time.Duration, retries int) (addrs []netip.Addr, err error) { + expectEnd := time.Now().Add(timeout * time.Duration(retries)) + for i := 0; i < retries; i++ { + addrs, err = s.block.DoLookupIP(host, timeout) + if err == nil { + return addrs, nil + } + } + if time.Now().Before(expectEnd) { + return addrs, err + } + return nil, errRetriesExceeded +} + +func (s StackRetrying) DoResolveHardwareAddress6(addr netip.Addr, timeout time.Duration, retries int) (hw [6]byte, err error) { + expectEnd := time.Now().Add(timeout * time.Duration(retries)) + for i := 0; i < retries; i++ { + hw, err = s.block.DoResolveHardwareAddress6(addr, timeout) + if err == nil { + return hw, nil + } + } + if time.Now().Before(expectEnd) { + return hw, err + } + return hw, errRetriesExceeded +} + +func (s StackRetrying) DoDialTCP(localPort uint16, addrp netip.AddrPort, timeout time.Duration, retries int) (conn *tcp.Conn, err error) { + expectEnd := time.Now().Add(timeout * time.Duration(retries)) + for i := 0; i < retries; i++ { + conn, err = s.block.DoDialTCP(localPort, addrp, timeout) + if err == nil { + return conn, nil + } + } + if time.Now().Before(expectEnd) { + return conn, err + } + return nil, errRetriesExceeded +}