diff --git a/examples/bridge/main.go b/examples/bridge/main.go index f246937..43c7037 100644 --- a/examples/bridge/main.go +++ b/examples/bridge/main.go @@ -11,7 +11,6 @@ import ( "net/netip" "os" "runtime" - "strconv" "strings" "time" @@ -24,6 +23,7 @@ import ( "github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/internet" "github.com/soypat/lneto/internet/pcap" + "github.com/soypat/lneto/ntp" ) var softRand = time.Now().Unix() @@ -43,11 +43,13 @@ func run() (err error) { 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) @@ -80,34 +82,41 @@ func run() (err error) { return err } brHW := nicHW - brHW[5] += byte(softRand)%128 + 1 // We'll be using a similar HW address but with NIC specific identifier modified. + brHW[4]++ 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 Stack + err = stack.Reset(brHW, netip.AddrFrom4([4]byte{}), uint16(mtu)) if err != nil { return err } - err = stack.BeginDHCPRequest([4]byte{192, 168, 1, 199}) - if err != nil { - return err - } + buf := make([]byte, mtu) lastAction := time.Now() const ( stateDHCP = iota stateInitARP + stateDNSNTP + stateNTP stateDNS stateDone ) + + err = stack.BeginDHCPRequest([4]byte{192, 168, 1, 96}) + if err != nil { + return err + } state := stateDHCP + prevState := state for { switch state { case stateDHCP: @@ -128,14 +137,46 @@ func run() (err error) { router := stack.dhcp.RouterAddr() hw, err := stack.ResultResolveHardwareAddress6(netip.AddrFrom4(router)) if err == nil { - state = stateDNS stack.link.SetGateway6(hw) + stack.link.SetHardwareAddr6([6]byte{0xd8, 0x5e, 0xd3, 0x43, 0x03, 0xeb}) + stack.ip.SetAddr(netip.AddrFrom4([4]byte{192, 168, 1, 53})) + if flagDoNTP { + state = stateDNSNTP + err = stack.StartLookupIP("pool.ntp.org") + } else { + state = stateDNS + err = stack.StartLookupIP(flagHostToResolve) + } + if err != nil { + return err + } + } + case stateDNSNTP: + addrs, done, err := stack.ResultLookupIP() + if err == nil { + state = stateNTP + fmt.Println("START NTP") + err = stack.StartNTP(addrs[0]) + } else if !done { + err = nil + } + if err != nil { + return err + } + case stateNTP: + offset, done := stack.ResultNTP() + if done { + relative := "behind" + if offset < 0 { + relative = "ahead" + } + fmt.Println("NTP completed. You are", offset.Abs(), relative, "of the NTP server") + state = stateDNS err = stack.StartLookupIP(flagHostToResolve) if err != nil { return err } } - case stateDNS: addrs, done, err := stack.ResultLookupIP() if err == nil { @@ -145,6 +186,10 @@ func run() (err error) { return err } } + if prevState != state { + fmt.Println("STATE CHANGE", prevState, state) + } + prevState = state clear(buf) nwrite, err := stack.Encapsulate(buf[:], 0) @@ -189,6 +234,8 @@ type Stack struct { dns dns.Client ednsopt dns.Resource lookup dns.Message + ntp ntp.Client + sysprec int8 // NTP system precision. // Packet capture and top level filtering. shark pcap.PacketBreakdown @@ -198,8 +245,9 @@ type Stack struct { func (s *Stack) Demux(b []byte, _ int) (err error) { s.aux, err = s.shark.CaptureEthernet(s.aux[:0], b, 0) topFrame := s.aux[len(s.aux)-1] - isOK := topFrame.Protocol == "DHCPv4" || // Allow DHCP responses. + isOK := topFrame.Protocol == "DHCPv4" || // Allow DHCP, DNS and NTP responses. topFrame.Protocol == "DNS" || + topFrame.Protocol == "NTP" || topFrame.Protocol == ethernet.TypeARP // Allow ARP responses. if !isOK { return nil @@ -269,6 +317,8 @@ func (s *Stack) Reset(mac [6]byte, addr netip.Addr, mtu uint16) error { return err } s.ip.SetLogger(slog.Default()) + var timebuf [32]time.Time + s.sysprec = ntp.CalculateSystemPrecision(time.Now, timebuf[:]) return nil } @@ -281,8 +331,6 @@ func (s *Stack) StartLookupIP(host string) error { if err != nil { return err } - s.link.SetHardwareAddr6([6]byte{0xd8, 0x5e, 0xd3, 0x43, 0x03, 0xeb}) - s.ip.SetAddr(netip.AddrFrom4([4]byte{192, 168, 1, 53})) s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil) err = s.dns.StartResolve(uint16(softRand>>1)+1024, uint16(softRand), dns.ResolveConfig{ Questions: []dns.Question{ @@ -330,6 +378,10 @@ func (s *Stack) ResultLookupIP() ([]netip.Addr, bool, error) { return addrs, done, nil } +func (s *Stack) ResultNTP() (time.Duration, bool) { + return s.ntp.Offset(), s.ntp.IsDone() +} + func (s *Stack) BeginDHCPRequest(request [4]byte) error { var buf [4]byte rand.Read(buf[:]) @@ -337,7 +389,7 @@ func (s *Stack) BeginDHCPRequest(request [4]byte) error { err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{ RequestedAddr: request, ClientHardwareAddr: s.link.HardwareAddr6(), - Hostname: "lneto" + strconv.FormatInt(softRand%100, 16), + Hostname: "lneto", }) if err != nil { return err @@ -351,6 +403,15 @@ func (s *Stack) BeginDHCPRequest(request [4]byte) error { return err } +func (s *Stack) StartNTP(addr netip.Addr) error { + s.ntp.Reset(time.Now, s.sysprec) + var u internet.StackUDPPort + addr4 := addr.As4() + u.SetStackNode(&s.ntp, addr4[:], ntp.ServerPort) + err := s.udps.Register(&u) + return err +} + func (s *Stack) StartResolveHardwareAddress6(ip netip.Addr) error { if !ip.Is4() { return errors.New("unsupported or invalid IP address") diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 937e61d..07f63af 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -15,6 +15,7 @@ import ( "github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/ipv6" + "github.com/soypat/lneto/ntp" "github.com/soypat/lneto/tcp" "github.com/soypat/lneto/udp" ) @@ -324,10 +325,14 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([ dst = append(dst, finfo) end := bitOffset + 8*octet payload := ufrm.Payload() + dstport := ufrm.DestinationPort() + srcport := ufrm.SourcePort() if dhcpv4.PayloadIsDHCPv4(payload) { dst, err = pc.CaptureDHCPv4(dst, pkt, end) - } else if ufrm.DestinationPort() == 53 || ufrm.SourcePort() == 53 { + } else if dstport == dns.ServerPort || srcport == dns.ServerPort { dst, err = pc.CaptureDNS(dst, pkt, end) + } else if dstport == ntp.ServerPort || srcport == ntp.ServerPort { + dst, err = pc.CaptureNTP(dst, pkt, end) } if err != nil { dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) @@ -361,6 +366,24 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([ return dst, nil } +func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { + if bitOffset%8 != 0 { + return nil, errors.New("NTP must be parsed at byte boundary") + } + ntpData := pkt[bitOffset/8:] + _, err := ntp.NewFrame(ntpData) + if err != nil { + return dst, err + } + finfo := Frame{ + Protocol: "NTP", + PacketBitOffset: bitOffset, + } + finfo.Fields = append(finfo.Fields, baseNTPFields[:]...) + dst = append(dst, finfo) + return dst, nil +} + func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { if bitOffset%8 != 0 { return nil, errors.New("DHCP must be parsed at byte boundary") @@ -954,6 +977,86 @@ var baseDHCPv4Fields = [...]FrameField{ }, } +var baseNTPFields = [...]FrameField{ + { + Name: "Mode", + Class: FieldClassType, + FrameBitOffset: 0, + BitLength: 3, + }, + { + Class: FieldClassVersion, + FrameBitOffset: 3, + BitLength: 2, + }, + { + Name: "Leap Indicator", + Class: fieldClassUndefined, + FrameBitOffset: 5, + BitLength: 3, + }, + { + Name: "Stratum", + Class: fieldClassUndefined, + FrameBitOffset: 1 * octet, + BitLength: 1 * octet, + }, + { + Name: "Poll", + Class: fieldClassUndefined, + FrameBitOffset: 2 * octet, + BitLength: 1 * octet, + }, + { + Name: "System Precision", + Class: fieldClassUndefined, + FrameBitOffset: 3 * octet, + BitLength: 1 * octet, + }, + { + Name: "Root Delay", + Class: fieldClassUndefined, + FrameBitOffset: 4 * octet, + BitLength: 4 * octet, + }, + { + Name: "Root Dispersion", + Class: fieldClassUndefined, + FrameBitOffset: 8 * octet, + BitLength: 4 * octet, + }, + { + Name: "Reference ID", + Class: FieldClassText, + FrameBitOffset: 12 * octet, + BitLength: 4 * octet, + }, + { + Name: "Reference Time", + Class: FieldClassText, + FrameBitOffset: 16 * octet, + BitLength: 8 * octet, + }, + { + Name: "Origin Time", + Class: FieldClassText, + FrameBitOffset: 24 * octet, + BitLength: 8 * octet, + }, + { + Name: "Receive Time", + Class: FieldClassText, + FrameBitOffset: 32 * octet, + BitLength: 8 * octet, + }, + { + Name: "Transit Time", + Class: FieldClassText, + FrameBitOffset: 40 * octet, + BitLength: 8 * octet, + }, +} + func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame { return Frame{ Protocol: proto, diff --git a/ntp/client.go b/ntp/client.go index ba49eeb..3d3c2da 100644 --- a/ntp/client.go +++ b/ntp/client.go @@ -2,7 +2,6 @@ package ntp import ( "errors" - "io" "time" ) @@ -23,33 +22,37 @@ type Client struct { connID uint64 start time.Time _now func() time.Time - t [4]Timestamp + // t stores the time offsets needed to compute the time at client + // taking into consideration the round-trip delay. + // - t[0] (orig): Client timestamp of request packet transmission. + // - t[1] (rec): Server timestamp of request packet reception. + // - t[2] (xmt): Server timestamp of response packet transmission. + // - t[3]: Client timestamp of response packet reception. + t [4]Timestamp // org Timestamp - // rec Timestamp - xmt Timestamp state state serverStratum Stratum - _sysprec int8 + sysprec int8 } -func (c *Client) Reset(now func() time.Time) { - if c._sysprec == 0 { - c._sysprec = sysprecRecalcNeeded - } +func (c *Client) Reset(now func() time.Time, sysprec int8) { *c = Client{ - connID: c.connID + 1, - _now: now, - _sysprec: c._sysprec, + connID: c.connID + 1, + _now: now, + sysprec: sysprec, + state: stateSend1, } } +func (c *Client) Protocol() uint64 { return 0 } +func (c *Client) LocalPort() uint16 { return ClientPort } func (c *Client) ConnectionID() *uint64 { return &c.connID } func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) { - if c.isDone() { - return 0, io.EOF + if c.IsDone() { + return 0, nil } payload := carrierData[frameOffset:] frm, err := NewFrame(payload) @@ -60,10 +63,10 @@ func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) { switch c.state { case stateSend1: c.start = c.now() - c.xmt = TimestampFromUint64(0) + c.t[0] = TimestampFromUint64(0) c.state = stateAwait1 case stateSend2: - c.xmt = c.unsyncTimestamp(c.now()) + // c.xmt = c.unsyncTimestamp(c.now()) c.state = stateDone default: return 0, nil // Nothing to handle. @@ -72,46 +75,47 @@ func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) { for i := range payload[:SizeHeader] { payload[i] = 0 } - sysprec := c.sysprec() + frm.ClearHeader() frm.SetStratum(StratumUnsync) frm.SetPoll(6) - frm.SetPrecision(sysprec) - frm.SetOriginTime(c.xmt) + frm.SetPrecision(c.sysprec) + frm.SetOriginTime(c.t[0]) frm.SetFlags(ModeClient, Version4, LeapNoWarning) return SizeHeader, nil } func (c *Client) Demux(carrierData []byte, frameOffset int) error { - if c.isDone() { - return io.EOF + if c.IsDone() { + return nil } payload := carrierData[frameOffset:] frm, err := NewFrame(payload) if err != nil { return err } - t := &c.t + switch c.state { case stateAwait1: - tstx := frm.TransmitTime() - tsorig := frm.OriginTime() - if tstx == tsorig || tsorig == c.xmt { + xmt := frm.TransmitTime() + orig := frm.OriginTime() + if xmt == orig || orig != c.t[0] { return errors.New("bogus NTP packet") } - t[0] = tsorig - t[1] = frm.ReceiveTime() - t[2] = tstx - t[3] = c.unsyncTimestamp(c.now()) + + txelapsed := c.now().Sub(c.start) + c.t[1] = frm.ReceiveTime() + c.t[2] = xmt + c.t[3] = c.t[0].Add(txelapsed) c.serverStratum = frm.Stratum() - c.state = stateDone + c.state = stateDone // TODO: add second exchange part. case stateAwait2: - c.state = stateAwait2 + c.state = stateDone } return nil } -func (c *Client) isDone() bool { +func (c *Client) IsDone() bool { return c.state == stateDone } @@ -122,30 +126,48 @@ func (c *Client) now() time.Time { return c._now() } -func (c *Client) unsyncTimestamp(now time.Time) Timestamp { - return TimestampFromUint64(0).Add(now.Sub(c.start)) -} - -func (c *Client) sysprec() int8 { - if c._sysprec == sysprecRecalcNeeded { - c._sysprec = CalculateSystemPrecision(c._now) - } - return c._sysprec -} - // Now returns the current time as corrected by NTP protocol. func (c *Client) Now() time.Time { - return c.now().Add(c.Offset()) + now, off := c.offsetAndNow() + return now.Add(off) } // ServerStratum returns the stratum of the server client synchronized with. func (c *Client) ServerStratum() Stratum { return c.serverStratum } -// Offset returns the +// Offset is a helper method to determine the difference between the Client's clock and the server's clock. +// Use [Client.Now] to calculate the server's time. func (c *Client) Offset() time.Duration { - if c.isDone() { - t := &c.t - return t[1].Sub(t[0])/2 + t[2].Sub(t[3])/2 + if c.IsDone() { + _, off := c.offsetAndNow() + return off } return 0 } + +func (c *Client) offsetAndNow() (clientNow time.Time, offset time.Duration) { + now := c.now() + serverToBase := c.OffsetUnsynced() + clientToBase := now.Sub(BaseTime()) + serverToClient := serverToBase - clientToBase + return now, serverToClient +} + +// OffsetUnsynced returns the absolute time offset difference between client and server clock +// as calculated by the clock synchonization algorithm. It is unsynchonized- the result of OffsetUnsynced will not change with time. +func (c *Client) OffsetUnsynced() time.Duration { + if c.IsDone() { + t := &c.t + return (t[1].Sub(t[0]) + t[2].Sub(t[3])) / 2 + } + return 0 +} + +func (c *Client) RoundTripDelay() time.Duration { + if c.IsDone() { + d0 := c.t[3].Sub(c.t[0]) + d1 := c.t[2].Sub(c.t[1]) + return d0 - d1 + } + return -1 +} diff --git a/ntp/ntp.go b/ntp/ntp.go index 232af14..92c7e86 100644 --- a/ntp/ntp.go +++ b/ntp/ntp.go @@ -43,13 +43,13 @@ type Frame struct { func (frm Frame) Flags() (mode Mode, version uint8, lp LeapIndicator) { b := frm.buf[0] mode = Mode(b & 0b111) - version = (b << 3) & 0b11 - lp = LeapIndicator(b >> 5) + version = (b >> 3) & 0b111 + lp = LeapIndicator(b >> 6) return mode, version, lp } func (frm Frame) SetFlags(mode Mode, version uint8, lp LeapIndicator) { - b := uint8(mode)&0b111 | (Version4&0b11)<<3 | uint8(lp&0b111)<<5 + b := uint8(mode)&0b111 | (version&0b111)<<3 | uint8(lp&0b11)<<6 frm.buf[0] = b } @@ -269,31 +269,19 @@ var ( sysPrec int8 ) -// SystemPrecision calculates the Precision field value for the NTP header once -// and reuses it for all future calls. -func SystemPrecision() int8 { - ntpOnceSystemClock.Do(recalculateSystemPrecision) - return sysPrec -} - -func recalculateSystemPrecision() { - sysPrec = CalculateSystemPrecision(nil) -} - // CalculateSystemPrecision calculates the NTP system precision for a time source. // If the time source is nil the default static call to [time.Now] is used. -func CalculateSystemPrecision(now func() time.Time) int8 { - const maxIter = 16 - var times [maxIter]time.Time +func CalculateSystemPrecision(now func() time.Time, iters []time.Time) int8 { + maxIter := len(iters) if now == nil { for i := 0; i < maxIter; i++ { - times[i] = time.Now() + iters[i] = time.Now() } } else { for i := 0; i < maxIter; i++ { - times[i] = now() + iters[i] = now() } } - avg := times[maxIter-1].Sub(times[0]) / maxIter + avg := iters[maxIter-1].Sub(iters[0]) / time.Duration(maxIter) return int8(math.Log2(avg.Seconds())) }