From dcc7a95e62b6f2f7c3a7fad39122cd3c463c9b03 Mon Sep 17 00:00:00 2001 From: soypat Date: Sun, 15 Jun 2025 18:01:04 -0300 Subject: [PATCH] huge tap/bridge overhaul; udp node; dhcp node; DHCP example --- .gitignore | 2 +- README.md | 20 ++++ dhcpv4/client.go | 24 ++++- ethernet/definitions.go | 4 + examples/bridge/main.go | 189 +++++++++++++++++++++++++++++++++++++ examples/tap/main.go | 53 ++++++++--- internal/ltesto/httptap.go | 32 +++++-- internal/tap.go | 187 ++++++++++++++++++++++++++++++++---- internet/definitions.go | 56 ++++++++--- internet/node-ntp.go | 31 ------ internet/pcap/capture.go | 15 ++- internet/stack-ethernet.go | 12 +++ internet/stack-ip.go | 129 +++++++++++++++---------- internet/stack-ports.go | 18 ++-- internet/stack-udpport.go | 78 +++++++++++++++ ntp/client.go | 13 +-- 16 files changed, 706 insertions(+), 157 deletions(-) create mode 100644 examples/bridge/main.go delete mode 100644 internet/node-ntp.go create mode 100644 internet/stack-udpport.go diff --git a/.gitignore b/.gitignore index 59fdd94..48e7fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ vendor/ **__debug_bin* # `__debug_bin` Debug binary generated in VSCode when using the built-in debugger. *bin - +/bridge # IDE .vscode/ diff --git a/README.md b/README.md index 8bd971f..7aee726 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,26 @@ Userspace networking primitives. - [`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. +### Abstractions +The following interface is implemented by networking stack nodes and the stack themselves. + +```go +type StackNode interface { + // Encapsulate receives a buffer the receiver must fill with data. + // The receiver's start byte is at carrierData[frameOffset]. + Encapsulate(carrierData []byte, frameOffset int) (int, error) + // Demux receives a buffer the receiver must decode and pass on to corresponding child StackNode(s). + // The receiver's start byte is at carrierData[frameOffset]. + Demux(carrierData []byte, frameOffset int) error + // LocalPort returns the port of the node if applicable or zero. Used for UDP/TCP nodes. + LocalPort() uint16 + // Protocol returns the protocol of this node if applicable or zero. Usually either a ethernet.Type (EtherType) or lneto.IPProto (IP Protocol number). + Protocol() uint64 + // ConnectionID returns a pointer to the connection ID of the StackNode. + // A change in the ID means the node is no longer valid and should be discarded. + // A change in the ID could mean the connection was closed by the user or that the node will not send nor receive any more data over said connection ID. + ConnectionID() *uint64 +``` ## Install How to install package with newer versions of Go (+1.16): diff --git a/dhcpv4/client.go b/dhcpv4/client.go index 01cc811..be520d1 100644 --- a/dhcpv4/client.go +++ b/dhcpv4/client.go @@ -7,9 +7,13 @@ import ( "io" "math" "math/bits" + "net" + + "github.com/soypat/lneto" ) type Client struct { + connID uint64 reqHostname string hostname []byte dns [][4]byte @@ -41,8 +45,13 @@ type RequestConfig struct { func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error { if len(cfg.Hostname) > 36 { return errors.New("requested hostname too long") + } else if c.state != StateInit && c.state != 0 { + return errors.New("dhcp client must be closed/done before new request") + } else if xid == 0 { + return errors.New("zero xid") } c.reset(xid) + c.state = StateInit c.currentXID = xid c.reqHostname = cfg.Hostname c.reqIP = cfg.RequestedAddr @@ -50,14 +59,19 @@ func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error { return nil } -func (c *Client) Send(dst []byte) (int, error) { +func (c *Client) Protocol() uint64 { return uint64(lneto.IPProtoUDP) } +func (c *Client) LocalPort() uint16 { return DefaultClientPort } +func (c *Client) ConnectionID() *uint64 { return &c.connID } + +func (c *Client) Encapsulate(carrierFrame []byte, frameOffset int) (int, error) { if c.isClosed() { - return 0, io.EOF + return 0, net.ErrClosed } else if c.state == StateSelecting && c.offer == [4]byte{} { return 0, nil // No offer received yet. } else if c.state == StateBound { return 0, nil // Done! } + dst := carrierFrame[frameOffset:] frm, err := NewFrame(dst) if err != nil { return 0, err @@ -108,10 +122,11 @@ func (c *Client) Send(dst []byte) (int, error) { return optionsOffset + n, nil } -func (c *Client) Recv(pkt []byte) error { +func (c *Client) Demux(carrierData []byte, frameOffset int) error { if c.isClosed() { - return io.EOF + return net.ErrClosed } + pkt := carrierData[frameOffset:] frm, err := NewFrame(pkt) if err != nil { return err @@ -217,6 +232,7 @@ func (c *Client) setHeader(frm Frame) { func (c *Client) reset(xid uint32) { *c = Client{ + connID: c.connID + 1, reqHostname: c.reqHostname, currentXID: xid, reqIP: c.reqIP, diff --git a/ethernet/definitions.go b/ethernet/definitions.go index 89cc6fb..80c50cd 100644 --- a/ethernet/definitions.go +++ b/ethernet/definitions.go @@ -4,6 +4,10 @@ const ( sizeHeaderNoVLAN = 14 ) +func BroadcastAddr() [6]byte { + return [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} +} + //go:generate stringer -type=Type -linecomment -output stringers.go . type Type uint16 diff --git a/examples/bridge/main.go b/examples/bridge/main.go new file mode 100644 index 0000000..d2867e3 --- /dev/null +++ b/examples/bridge/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "net" + "net/netip" + "os" + "time" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/arp" + "github.com/soypat/lneto/dhcpv4" + "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/internal/ltesto" + "github.com/soypat/lneto/internet" + "github.com/soypat/lneto/internet/pcap" +) + +func main() { + err := run() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("success") +} + +func run() (err error) { + br := ltesto.NewHTTPTapClient("http://127.0.0.1:7070") + defer br.Close() + + nicHW := br.HardwareAddr6() + + brHW := nicHW + brHW[5]++ // We'll be using a similar HW address but with NIC specific identifier modified. + mtu := br.MTU() + nicAddr := br.IPPrefix() + + 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, nicAddr.Addr().Next(), uint16(mtu)) + if err != nil { + return err + } + err = stack.BeginDHCPRequest() + if err != nil { + return err + } + var shark pcap.PacketBreakdown + buf := make([]byte, mtu) + var iframes []pcap.Frame + lastAction := time.Now() + for { + clear(buf) + nwrite, err := stack.Encapsulate(buf[:], 0) + if err != nil { + fmt.Println("ERR:ENCAPSULATE", err) + } else if nwrite > 0 { + iframes, err = shark.CaptureEthernet(iframes[:0], buf[:nwrite], 0) + if err != nil { + fmt.Println("OU", iframes, err.Error()) + } else { + fmt.Println("OU", iframes) + } + n, err := br.Write(buf[:nwrite]) + if err != nil { + return err + } else if n != nwrite { + return fmt.Errorf("mismatch written bytes %d!=%d", nwrite, n) + } + } + + clear(buf) + nread, err := br.Read(buf) + if err != nil { + return err + } else if nread > 0 { + iframes, err = shark.CaptureEthernet(iframes[:0], buf[:nread], 0) + if err != nil { + fmt.Println("IN", iframes, err.Error()) + } else { + fmt.Println("IN", iframes) + } + err = stack.Demux(buf[:nread], 0) + if err != nil { + fmt.Println("ERR:DEMUX", err) + } + } + + if nread == 0 && nwrite == 0 && time.Since(lastAction) > 4*time.Second { + time.Sleep(5 * time.Millisecond) + } else { + lastAction = time.Now() + } + } + return nil +} + +type Stack struct { + link internet.StackEthernet + ip internet.StackIP + arp internet.NodeARP + udps internet.StackPorts + dhcp dhcpv4.Client +} + +func (s *Stack) Demux(b []byte, _ int) error { + return s.link.Demux(b, 0) +} + +func (s *Stack) Encapsulate(b []byte, _ int) (int, error) { + return s.link.Encapsulate(b, 0) +} + +func (s *Stack) Reset(mac [6]byte, addr netip.Addr, mtu uint16) error { + err := s.link.Reset6(mac, ethernet.BroadcastAddr(), int(mtu)) + if err != nil { + return err + } + err = s.ip.Reset(addr) + if err != nil { + return err + } + ipaddr := addr.AsSlice() + proto := ethernet.TypeIPv4 + if addr.Is6() { + proto = ethernet.TypeIPv6 + } + err = s.arp.Reset(arp.HandlerConfig{ + HardwareAddr: mac[:], + ProtocolAddr: ipaddr, + MaxQueries: 3, + MaxPending: 3, + HardwareType: 1, + ProtocolType: proto, + }) + if err != nil { + return err + } + err = s.udps.Reset(uint64(lneto.IPProtoUDP), 2) + 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 + } + return nil +} + +func (s *Stack) BeginDHCPRequest() error { + addr4 := s.ip.Addr().As4() + var buf [4]byte + rand.Read(buf[:]) + xid := binary.LittleEndian.Uint32(buf[:]) + err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{ + RequestedAddr: addr4, + ClientHardwareAddr: s.link.HardwareAddr6(), + Hostname: "lneto", + }) + if err != nil { + return err + } + var u internet.StackUDPPort + u.SetStackNode(&s.dhcp, dhcpv4.DefaultServerPort) + err = s.udps.Register(&u) + if err != nil { + return err + } + return err +} + +func clear(buf []byte) { + for i := range buf { + buf[i] = 0 + } +} diff --git a/examples/tap/main.go b/examples/tap/main.go index a1e68f4..b5e1442 100644 --- a/examples/tap/main.go +++ b/examples/tap/main.go @@ -2,6 +2,7 @@ package main import ( "errors" + "flag" "fmt" "log" "log/slog" @@ -9,9 +10,11 @@ import ( "net/http" "net/netip" "runtime" + "strings" "time" "github.com/soypat/lneto" + "github.com/soypat/lneto/internal" "github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/internet/pcap" "github.com/soypat/lneto/tcp" @@ -27,17 +30,35 @@ func main() { } func run() error { + var ( + flagInterface = "tap0" + ) + flag.StringVar(&flagInterface, "i", flagInterface, "Interface to select. tap* creates a tap interface. Any other name will create a bridge to the name of the interface i.e: 'enp7s0', 'wlp8s0', 'lo'") var ( flagNet = "192.168.10.1/24" flagiface = "tap0" - flagMTU = 1500 flagPacketQueueSize = 2048 ) - ip, err := netip.ParsePrefix(flagNet) - if err != nil { - return err + var iface ltesto.Interface + if strings.HasPrefix(flagInterface, "tap") { + pfx, err := netip.ParsePrefix(flagNet) + if err != nil { + return err + } + tap, err := internal.NewTap(flagiface, pfx) + if err != nil { + return err + } + iface = tap + } else { + br, err := internal.NewBridge(flagInterface) + if err != nil { + return err + } + iface = br } - sv, err := ltesto.NewHTTPTapServer(flagiface, ip, flagMTU, flagPacketQueueSize, flagPacketQueueSize) + + sv, err := ltesto.NewHTTPTapServer(iface, flagPacketQueueSize, flagPacketQueueSize) if err != nil { return err } @@ -48,8 +69,12 @@ func run() error { frames, err := cap.CaptureEthernet(nil, pkt, 0) if err == nil { flags, src, dst := getTCPData(frames, pkt) - if flags != 0 { - fmt.Println(channel, captime.Format("15:04:05.000"), frames, flags.String(), src, "->", dst) + if src != 0 { + if flags != 0 { + fmt.Println(channel, captime.Format("15:04:05.000"), frames, flags.String(), src, "->", dst) + } else { + fmt.Println(channel, captime.Format("15:04:05.000"), frames, src, "->", dst) + } } else { fmt.Println(channel, captime.Format("15:04:05.000"), frames) } @@ -86,12 +111,16 @@ func run() error { func getTCPData(frames []pcap.Frame, pkt []byte) (flags tcp.Flags, src, dst uint16) { for i := range frames { - if frames[i].Protocol != lneto.IPProtoTCP { - continue + proto := frames[i].Protocol + if proto == lneto.IPProtoTCP { + return tcp.Flags(getFrameClassUint(frames[i], pkt, pcap.FieldClassFlags)), + uint16(getFrameClassUint(frames[i], pkt, pcap.FieldClassSrc)), + uint16(getFrameClassUint(frames[i], pkt, pcap.FieldClassDst)) + } else if proto == lneto.IPProtoUDP { + return 0, + uint16(getFrameClassUint(frames[i], pkt, pcap.FieldClassSrc)), + uint16(getFrameClassUint(frames[i], pkt, pcap.FieldClassDst)) } - return tcp.Flags(getFrameClassUint(frames[i], pkt, pcap.FieldClassFlags)), - uint16(getFrameClassUint(frames[i], pkt, pcap.FieldClassSrc)), - uint16(getFrameClassUint(frames[i], pkt, pcap.FieldClassDst)) } return 0, 0, 0 } diff --git a/internal/ltesto/httptap.go b/internal/ltesto/httptap.go index 897fa69..e73eea0 100644 --- a/internal/ltesto/httptap.go +++ b/internal/ltesto/httptap.go @@ -9,12 +9,19 @@ import ( "net/http" "net/netip" "net/url" - - "github.com/soypat/lneto/internal" ) const minMTU = 256 +type Interface interface { + Read(b []byte) (int, error) + Write(b []byte) (int, error) + Close() error + HardwareAddress6() ([6]byte, error) + MTU() (int, error) + IPMask() (netip.Prefix, error) +} + // NewHTTPTapClient returns a HTTPTapClient ready for use. func NewHTTPTapClient(baseURL string) *HTTPTapClient { var h HTTPTapClient @@ -150,7 +157,7 @@ func (h *HTTPTapClient) Close() error { return nil } type HTTPTapServer struct { router *http.ServeMux stack stack - tap *internal.Tap + tap Interface buf []byte onTx func(channel int, pkt []byte) tapfailed bool @@ -166,11 +173,17 @@ func (sv *HTTPTapServer) OnTransfer(cb func(channel int, pkt []byte)) { sv.onTx = cb } -func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) (*HTTPTapServer, error) { - if mtu < minMTU { +func NewHTTPTapServer(iface Interface, queueOut, queueIn int) (*HTTPTapServer, error) { + if iface == nil { + return nil, errors.New("nil interface argument to HTTP interface server") + } + mtu, err := iface.MTU() + if err != nil { + return nil, err + } else if mtu < minMTU { return nil, errors.New("too small MTU") } - tap, err := internal.NewTap(iface, ip) + netmask, err := iface.IPMask() if err != nil { return nil, err } @@ -183,7 +196,7 @@ func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) taps := &HTTPTapServer{ router: sv, stack: s, - tap: tap, + tap: iface, buf: make([]byte, mtu), } sv.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) { @@ -210,13 +223,14 @@ func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) json.NewEncoder(w).Encode("") // send empty string. } }) - ipstr := ip.String() + + ipstr := netmask.String() sv.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) { info := tapInfo{ MTU: mtu, IPPrefix: ipstr, } - hw, err := tap.HardwareAddress6() + hw, err := iface.HardwareAddress6() if err == nil { info.HardwareAddr = net.HardwareAddr(hw[:]).String() } diff --git a/internal/tap.go b/internal/tap.go index d82ddae..ca92c47 100644 --- a/internal/tap.go +++ b/internal/tap.go @@ -3,8 +3,11 @@ package internal import ( + "encoding/binary" "errors" "fmt" + "math/bits" + "net" "net/netip" "os" "os/exec" @@ -12,6 +15,8 @@ import ( "unsafe" ) +const safamily_hw6 = 1 + type Tap struct { fd int // points to /dev/net/tun device. name string @@ -25,12 +30,7 @@ func NewTap(name string, ip netip.Prefix) (*Tap, error) { if err != nil { return nil, fmt.Errorf("failed to open tun device: %w", err) } - tap := Tap{ - name: name, - fd: fd, - } - ifr := tap.ifreq() - + ifr := makeifreq(name) // Set the flags (starting at offset IFNAMSIZ). flags := uint16(syscall.IFF_TAP | syscall.IFF_NO_PI) ifr.setflags(flags) @@ -55,6 +55,14 @@ func NewTap(name string, ip netip.Prefix) (*Tap, error) { return &Tap{fd: fd, name: name}, nil } +func (tap *Tap) IPMask() (netip.Prefix, error) { + sockfd, err := tap.getSock() + if err != nil { + return netip.Prefix{}, err + } + return getSocketMask(sockfd, tap.name) +} + func (tap *Tap) Read(b []byte) (int, error) { return syscall.Read(tap.fd, b) } @@ -75,27 +83,108 @@ func ioctl(fd int, request uintptr, argp unsafe.Pointer) error { return nil } -func (tap *Tap) HardwareAddress6() (hw [6]byte, err error) { - // We cannot use tap.sock to query the hardware address, this is something known by the network stack, so get a sock to network stack. - sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP) +func (tap *Tap) MTU() (int, error) { + sock, err := tap.getSock() if err != nil { - return hw, fmt.Errorf("socket open: %w", err) + return 0, err } defer syscall.Close(sock) - ifr := tap.ifreq() + return getSocketMTU(sock, tap.name) +} - err = ioctl(sock, syscall.SIOCGIFHWADDR, ifr.ptr()) +func (tap *Tap) HardwareAddress6() (hw [6]byte, err error) { + // We cannot use tap.sock to query the hardware address, this is something known by the network stack, so get a sock to network stack. + sock, err := tap.getSock() if err != nil { return hw, err } - sa_family := *(*uint16)(unsafe.Pointer(&ifr.Data[0])) - if sa_family != 1 { + defer syscall.Close(sock) + return getSocketHW(sock, tap.name) +} + +func (tap *Tap) getSock() (int, error) { + sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP) + if err != nil { + return 0, fmt.Errorf("tap socket open: %w", err) + } + return sock, err +} + +func getSocketMTU(sockfd int, ifaceName string) (int, error) { + ifr := makeifreq(ifaceName) + err := ioctl(sockfd, syscall.SIOCGIFMTU, ifr.ptr()) + if err != nil { + return 0, err + } + mtu := *(*int32)(unsafe.Pointer(&ifr.Data[0])) + return int(mtu), nil +} + +func getSocketHW(sockfd int, ifaceName string) (hw [6]byte, err error) { + ifr := makeifreq(ifaceName) + err = ioctl(sockfd, syscall.SIOCGIFHWADDR, ifr.ptr()) + if err != nil { + return hw, err + } + sa_family := *(*uint16)(unsafe.Pointer(&ifr.Data[0])) // Host order. + if sa_family != safamily_hw6 { return hw, fmt.Errorf("expecting sa_family=1 got %d", sa_family) } copy(hw[:], ifr.Data[2:]) // first two bytes are sa_family return hw, nil } +func getSocketMask(sockfd int, ifaceName string) (netip.Prefix, error) { + addrp, err := getSocketIP(sockfd, ifaceName) + if err != nil { + return netip.Prefix{}, err + } + ifr := makeifreq(ifaceName) + err = ioctl(sockfd, syscall.SIOCGIFNETMASK, ifr.ptr()) + if err != nil { + return netip.Prefix{}, err + } + addr32 := binary.BigEndian.Uint32(ifr.Data[4:8]) + cidr := bits.OnesCount32(addr32) + return netip.PrefixFrom(addrp.Addr(), cidr), nil +} + +func setSocketHW(sockfd int, ifaceName string, hw [6]byte) error { + ifr := makeifreq(ifaceName) + *(*uint16)(unsafe.Pointer(&ifr.Data[0])) = safamily_hw6 + copy(ifr.Data[2:], hw[:]) + err := ioctl(sockfd, syscall.SIOCSIFHWADDR, ifr.ptr()) + if err != nil { + return fmt.Errorf("setting hw addr: %w", err) + } + return nil +} + +func getSocketIP(sockfd int, ifaceName string) (addrp netip.AddrPort, err error) { + ifr := makeifreq(ifaceName) + err = ioctl(sockfd, syscall.SIOCGIFADDR, ifr.ptr()) + if err != nil { + return netip.AddrPort{}, err + } + safamily := *(*uint16)(unsafe.Pointer(&ifr.Data[0])) + port := *(*uint16)(unsafe.Pointer(&ifr.Data[2])) + switch safamily { + case 2: + addr, _ := netip.AddrFromSlice(ifr.Data[4:8]) + addrp = netip.AddrPortFrom(addr, port) + default: + return addrp, fmt.Errorf("unsupported IP addr sa_family=%d", safamily) + } + return addrp, nil +} + +func makeifreq(name string) ifreq { + // Set the name; it will be zero-padded automatically. + var ifr ifreq + copy(ifr.Name[:], name) + return ifr +} + type ifreq struct { Name [syscall.IFNAMSIZ]byte Data [64]byte // union data (covers ifr_hwaddr, etc.) @@ -107,9 +196,69 @@ func (ifr *ifreq) setflags(flags uint16) { func (ifr *ifreq) ptr() unsafe.Pointer { return unsafe.Pointer(ifr) } -func (tap *Tap) ifreq() ifreq { - // Set the name; it will be zero-padded automatically. - var ifr ifreq - copy(ifr.Name[:], tap.name) - return ifr +// Bridge serves as a virtual socket that "bridges" to an existing interface (could be TAP or an actual NIC that connects to internet). +// Bridge is used in this project to +type Bridge struct { + fd int + name string + index int } + +func NewBridge(name string) (*Bridge, error) { + iface, err := net.InterfaceByName(name) + if err != nil { + return nil, err + } + proto := htons(syscall.ETH_P_IP) + fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(proto)) + if err != nil { + return nil, err + } + ll := syscall.SockaddrLinklayer{ + Protocol: proto, + Ifindex: iface.Index, + } + if err := syscall.Bind(fd, &ll); err != nil { + return nil, err + } + return &Bridge{fd: fd, name: iface.Name, index: iface.Index}, nil +} + +func (br *Bridge) Write(frame []byte) (int, error) { + return syscall.Write(br.fd, frame) +} + +func (br *Bridge) Read(frame []byte) (int, error) { + return syscall.Read(br.fd, frame) +} + +func (br *Bridge) Close() error { + return syscall.Close(br.fd) +} + +func (br *Bridge) HardwareAddress6() (hw [6]byte, err error) { + return getSocketHW(br.fd, br.name) +} + +func (br *Bridge) SetHardwareAddress6(hw [6]byte) error { + return setSocketHW(br.fd, br.name, hw) +} + +func (br *Bridge) IPMask() (netip.Prefix, error) { + return getSocketMask(br.fd, br.name) +} + +func (br *Bridge) Addr() (netip.Addr, error) { + addrp, err := getSocketIP(br.fd, br.name) + if err != nil { + return netip.Addr{}, err + } + return addrp.Addr(), nil +} + +func (br *Bridge) MTU() (int, error) { + return getSocketMTU(br.fd, br.name) +} + +// htons converts a uint16 from host to network byte order. +func htons(i uint16) uint16 { return (i<<8)&0xff00 | i>>8 } diff --git a/internet/definitions.go b/internet/definitions.go index b7f6b6c..2985ed5 100644 --- a/internet/definitions.go +++ b/internet/definitions.go @@ -24,7 +24,6 @@ type StackNode interface { Encapsulate(carrierData []byte, frameOffset int) (int, error) // Demux reads from the argument buffer where frameOffset is the offset of this StackNode's frame first byte. // The stack node then dispatches(demuxes) the encapsulated frames to its corresponding sub-stack-node(s). - // Demux(carrierData []byte, frameOffset int) error LocalPort() uint16 Protocol() uint64 @@ -39,7 +38,6 @@ type node struct { connID *uint64 demux func([]byte, int) error encapsulate func([]byte, int) (int, error) - lastErrs [2]error proto uint16 port uint16 } @@ -57,34 +55,66 @@ func handleNodeError(nodesPtr *[]node, nodeIdx int, err error) (discarded bool) panic("unreachable") } nodes := *nodesPtr - badConnID := nodes[nodeIdx].connID != nil && *nodes[nodeIdx].connID != nodes[nodeIdx].currConnID - if err == net.ErrClosed || badConnID { + if checkNodeErr(&nodes[nodeIdx], err) { *nodesPtr = slices.Delete(nodes, nodeIdx, nodeIdx+1) discarded = true - } else { - // Advance Queue of errors - nodes[nodeIdx].lastErrs[1] = nodes[nodeIdx].lastErrs[0] - nodes[nodeIdx].lastErrs[0] = err } } return discarded } +func checkNode(node *node) (discard bool) { + return node.demux == nil || node.connID != nil && node.currConnID != *node.connID +} + +func checkNodeErr(node *node, err error) (discard bool) { + return checkNode(node) || (err != nil && err == net.ErrClosed) +} + func addNode(nodes *[]node, h StackNode, port uint16, protocol uint64) { + *nodes = append(*nodes, nodeFromStackNode(h, port, protocol)) +} + +func nodeFromStackNode(s StackNode, port uint16, protocol uint64) node { if protocol > math.MaxUint16 { panic(">16bit protocol number unsupported") } var currConnID uint64 - connIDPtr := h.ConnectionID() + connIDPtr := s.ConnectionID() if connIDPtr != nil { currConnID = *connIDPtr } - *nodes = append(*nodes, node{ + return node{ currConnID: currConnID, connID: connIDPtr, - demux: h.Demux, - encapsulate: h.Encapsulate, + demux: s.Demux, + encapsulate: s.Encapsulate, proto: uint16(protocol), port: port, - }) + } +} + +func getNode(nodes []node, port uint16, protocol uint16) (node *node) { + for i := range nodes { + node := &nodes[i] + if node.port == port && node.proto == protocol { + return node + } + } + return nil +} + +// destroy removes all references to underlying StackNode. Allows garbage collection of node if possible. +func (n *node) destroy() { + *n = node{} +} + +func getNodeByProto(nodes []node, protocol uint16) int { + for i := range nodes { + node := &nodes[i] + if node.proto == protocol { + return i + } + } + return -1 } diff --git a/internet/node-ntp.go b/internet/node-ntp.go deleted file mode 100644 index 2b03565..0000000 --- a/internet/node-ntp.go +++ /dev/null @@ -1,31 +0,0 @@ -package internet - -import ( - "github.com/soypat/lneto/ntp" -) - -var _ StackNode = (*NodeNTPClient)(nil) - -type NodeNTPClient struct { - c ntp.Client -} - -func (n *NodeNTPClient) Protocol() uint64 { - return 0 -} - -func (n *NodeNTPClient) LocalPort() uint16 { - return ntp.ClientPort -} - -func (n *NodeNTPClient) ConnectionID() *uint64 { - return n.c.ConnectionID() -} - -func (n *NodeNTPClient) Demux(carrierData []byte, ntpOffset int) error { - return nil -} - -func (n *NodeNTPClient) Encapsulate(carrierData []byte, ntpOffset int) (int, error) { - return 0, nil -} diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 1fa19e5..6a20062 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -208,6 +208,11 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( ifrm4.CRCWriteTCPPseudo(&crc) tfrm, err := tcp.NewFrame(ifrm4.Payload()) if err == nil { + tfrm.ValidateSize(pc.validator()) + if pc.vld.HasError() { + println("BAD TCP") + return dst, pc.vld.ErrPop() + } tfrm.CRCWrite(&crc) wantSum := crc.Sum16() gotSum := tfrm.CRC() @@ -215,10 +220,15 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( protoErrs = append(protoErrs, &crcError16{protocol: "ipv4+tcp", want: wantSum, got: gotSum}) } } - } else if proto == lneto.IPProtoUDP || proto == lneto.IPProtoUDPLite { + } else if proto == lneto.IPProtoUDP { ifrm4.CRCWriteUDPPseudo(&crc) ufrm, err := udp.NewFrame(ifrm4.Payload()) if err == nil { + ufrm.ValidateSize(pc.validator()) + if pc.vld.HasError() { + println("BAD UDP") + return dst, pc.vld.ErrPop() + } ufrm.CRCWriteIPv4(&crc) wantSum := crc.Sum16() gotSum := ufrm.CRC() @@ -289,6 +299,9 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([ return dst, nil } +// func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { +// } + func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { if bitOffset%8 != 0 { return dst, errors.New("UDP must be parsed at byte boundary") diff --git a/internet/stack-ethernet.go b/internet/stack-ethernet.go index 51802e3..bbe4445 100644 --- a/internet/stack-ethernet.go +++ b/internet/stack-ethernet.go @@ -20,6 +20,18 @@ type StackEthernet struct { mtu uint16 } +func (ls *StackEthernet) SetGateway6(gw [6]byte) { + ls.gwmac = gw +} + +func (ls *StackEthernet) SetHardwareAddr6(mac [6]byte) { + ls.mac = mac +} + +func (ls *StackEthernet) HardwareAddr6() [6]byte { + return ls.mac +} + func (ls *StackEthernet) Reset6(mac, gateway [6]byte, mtu int) error { if mtu > math.MaxUint16 || mtu < 256 { return errors.New("invalid MTU") diff --git a/internet/stack-ip.go b/internet/stack-ip.go index 4d5d420..d15a3fc 100644 --- a/internet/stack-ip.go +++ b/internet/stack-ip.go @@ -4,15 +4,14 @@ import ( "errors" "io" "log/slog" - "net" "net/netip" - "slices" "github.com/soypat/lneto" "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/internal" "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/tcp" + "github.com/soypat/lneto/udp" ) var _ StackNode = (*StackIP)(nil) @@ -73,40 +72,60 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error { } dst := ifrm.DestinationAddr() if *dst != sb.ip { - goto DROP - } - { - sb.validator.ResetErr() - ifrm.ValidateExceptCRC(&sb.validator) - if err = sb.validator.ErrPop(); err != nil { - return err - } - gotCRC := ifrm.CRC() - wantCRC := ifrm.CalculateHeaderCRC() - if gotCRC != wantCRC { - sb.error("StackIP:Demux:crc-mismatch", slog.Uint64("want", uint64(wantCRC)), slog.Uint64("got", uint64(gotCRC))) - return errors.New("IPv4 CRC mismatch") - } - off := ifrm.HeaderLength() - totalLen := ifrm.TotalLength() - for i := range sb.handlers { - h := &sb.handlers[i] - proto := ifrm.Protocol() - if h.proto == uint16(proto) { - sb.info("ipDemux", slog.String("ipproto", proto.String()), slog.Int("plen", int(totalLen))) - err = h.demux(frame[:totalLen], off) - if err == net.ErrClosed { - sb.info("ipclose", slog.String("proto", proto.String())) - sb.handlers = slices.Delete(sb.handlers, i, i+1) - } - return err - } - } + return nil // Not meant for us. } -DROP: - sb.info("iprecv:drop", slog.String("dstaddr", netip.AddrFrom4(*ifrm.DestinationAddr()).String()), slog.String("proto", ifrm.Protocol().String())) - return nil + sb.validator.ResetErr() + ifrm.ValidateExceptCRC(&sb.validator) + if err = sb.validator.ErrPop(); err != nil { + return err + } + gotCRC := ifrm.CRC() + wantCRC := ifrm.CalculateHeaderCRC() + if gotCRC != wantCRC { + sb.error("StackIP:Demux:crc-mismatch", slog.Uint64("want", uint64(wantCRC)), slog.Uint64("got", uint64(gotCRC))) + return errors.New("IPv4 CRC mismatch") + } + off := ifrm.HeaderLength() + totalLen := ifrm.TotalLength() + proto := ifrm.Protocol() + nodeIdx := getNodeByProto(sb.handlers, uint16(proto)) + if nodeIdx < 0 { + // Drop packet. + sb.info("iprecv:drop", slog.String("dstaddr", netip.AddrFrom4(*ifrm.DestinationAddr()).String()), slog.String("proto", ifrm.Protocol().String())) + return nil + } + // Incoming CRC Validation of common IP Protocols. + var crc lneto.CRC791 + switch proto { + case lneto.IPProtoTCP: + ifrm.CRCWriteTCPPseudo(&crc) + tfrm, err := tcp.NewFrame(ifrm.Payload()) + if err != nil { + return err + } + tfrm.CRCWrite(&crc) + if crc.Sum16() != tfrm.CRC() { + return errors.New("TCP CRC mismatch") + } + case lneto.IPProtoUDP: + ifrm.CRCWriteUDPPseudo(&crc) + ufrm, err := udp.NewFrame(ifrm.Payload()) + if err != nil { + return err + } + ufrm.CRCWriteIPv4(&crc) + if crc.Sum16() != ufrm.CRC() { + return errors.New("UDP CRC mismatch") + } + } + sb.info("ipDemux", slog.String("ipproto", proto.String()), slog.Int("plen", int(totalLen))) + err = sb.handlers[nodeIdx].demux(frame[:totalLen], off) + if handleNodeError(&sb.handlers, nodeIdx, err) { + sb.info("ipclose", slog.String("proto", proto.String())) + err = nil + } + return err } func (sb *StackIP) Encapsulate(carrierData []byte, frameOffset int) (int, error) { @@ -117,7 +136,7 @@ func (sb *StackIP) Encapsulate(carrierData []byte, frameOffset int) (int, error) ifrm, _ := ipv4.NewFrame(frame) const ihl = 5 const headerlen = ihl * 4 - ifrm.SetVersionAndIHL(4, 5) + ifrm.SetVersionAndIHL(4, ihl) ifrm.SetToS(0) ifrm.SetID(0) *ifrm.SourceAddr() = sb.ip @@ -128,25 +147,31 @@ func (sb *StackIP) Encapsulate(carrierData []byte, frameOffset int) (int, error) if err != nil { sb.error("StackIP:handle", slog.String("proto", proto.String()), slog.String("err", err.Error())) continue + } else if n == 0 { + continue } - if n > 0 { - const dontFrag = 0x4000 - totalLen := n + headerlen - ifrm.SetTotalLength(uint16(totalLen)) - ifrm.SetFlags(dontFrag) - ifrm.SetTTL(64) - ifrm.SetProtocol(proto) - ifrm.SetCRC(ifrm.CalculateHeaderCRC()) - if ifrm.Protocol() == lneto.IPProtoTCP { - var crc lneto.CRC791 - ifrm.CRCWriteTCPPseudo(&crc) - tfrm, _ := tcp.NewFrame(ifrm.Payload()) - tfrm.CRCWrite(&crc) - tfrm.SetCRC(crc.Sum16()) - sb.info("StackIP:send", slog.String("ip", ifrm.String()), slog.String("tcp", tfrm.String())) - } - return totalLen, nil + const dontFrag = 0x4000 + totalLen := n + headerlen + ifrm.SetTotalLength(uint16(totalLen)) + ifrm.SetFlags(dontFrag) + ifrm.SetTTL(64) + ifrm.SetProtocol(proto) + ifrm.SetCRC(ifrm.CalculateHeaderCRC()) + // Calculate CRC for our newly generated packet. + var crc lneto.CRC791 + switch proto { + case lneto.IPProtoTCP: + ifrm.CRCWriteTCPPseudo(&crc) + tfrm, _ := tcp.NewFrame(ifrm.Payload()) + tfrm.CRCWrite(&crc) + tfrm.SetCRC(crc.Sum16()) + case lneto.IPProtoUDP: + ifrm.CRCWriteUDPPseudo(&crc) + ufrm, _ := udp.NewFrame(ifrm.Payload()) + ufrm.CRCWriteIPv4(&crc) + ufrm.SetCRC(crc.Sum16()) } + return totalLen, nil } return 0, nil } diff --git a/internet/stack-ports.go b/internet/stack-ports.go index 65ea9dc..2cb5082 100644 --- a/internet/stack-ports.go +++ b/internet/stack-ports.go @@ -3,26 +3,32 @@ package internet import ( "encoding/binary" "io" + "math" ) type StackPorts struct { connID uint64 - protocol uint64 handlers []node dstPortOff int + protocol uint16 } -func (ps *StackPorts) Reset(protocol uint64, dstPortOffset int) { +func (ps *StackPorts) Reset(protocol uint64, dstPortOffset int) error { + if protocol > math.MaxUint16 { + return errInvalidProto + } *ps = StackPorts{ connID: ps.connID + 1, handlers: ps.handlers[:0], dstPortOff: dstPortOffset, - protocol: protocol, + protocol: uint16(protocol), } + return nil } + func (ps *StackPorts) LocalPort() uint16 { return 0 } -func (ps *StackPorts) Protocol() uint64 { return ps.protocol } +func (ps *StackPorts) Protocol() uint64 { return uint64(ps.protocol) } func (ps *StackPorts) ConnectionID() *uint64 { return &ps.connID } @@ -65,13 +71,13 @@ func (ps *StackPorts) Register(h StackNode) error { proto := h.Protocol() if port <= 0 { return errZeroPort - } else if proto != ps.protocol { + } else if proto != uint64(ps.protocol) { return errInvalidProto } ps.handlers = append(ps.handlers, node{ demux: h.Demux, encapsulate: h.Encapsulate, - port: uint16(port), + port: port, }) return nil } diff --git a/internet/stack-udpport.go b/internet/stack-udpport.go new file mode 100644 index 0000000..c71a9f8 --- /dev/null +++ b/internet/stack-udpport.go @@ -0,0 +1,78 @@ +package internet + +import ( + "log/slog" + "net" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/udp" +) + +type StackUDPPort struct { + h node + vld lneto.Validator + rmport uint16 +} + +func (sudp *StackUDPPort) SetStackNode(node StackNode, rmport uint16) { + sudp.h = nodeFromStackNode(node, node.LocalPort(), node.Protocol()) + sudp.rmport = rmport +} + +func (sudp *StackUDPPort) Protocol() uint64 { return uint64(lneto.IPProtoUDP) } + +func (sudp *StackUDPPort) LocalPort() uint16 { return sudp.h.port } + +func (sudp *StackUDPPort) ConnectionID() *uint64 { return sudp.h.connID } + +func (sudp *StackUDPPort) Demux(carrierData []byte, frameOffset int) error { + if checkNode(&sudp.h) { + sudp.h.destroy() + return net.ErrClosed + } + ufrm, err := udp.NewFrame(carrierData[frameOffset:]) + if err != nil { + return err + } + ufrm.ValidateSize(&sudp.vld) + if sudp.vld.HasError() { + return sudp.vld.ErrPop() + } + dst := ufrm.DestinationPort() + if dst != sudp.h.port { + return nil // Not meant for us. + } + + src := ufrm.SourcePort() + if sudp.rmport != 0 && src != sudp.rmport { + return nil // Not from our target remote port. + } + err = sudp.h.demux(ufrm.Payload(), 8) + if err != nil { + if checkNodeErr(&sudp.h, err) { + sudp.h.destroy() + } + slog.Error("stackudp:demux", slog.String("err", err.Error())) + } + return err +} + +func (sudp *StackUDPPort) Encapsulate(carrierData []byte, frameOffset int) (int, error) { + if checkNode(&sudp.h) { + sudp.h.destroy() + return 0, net.ErrClosed + } + ufrm, err := udp.NewFrame(carrierData[frameOffset:]) + if err != nil { + return 0, err + } + ufrm.SetSourcePort(sudp.h.port) + ufrm.SetDestinationPort(sudp.rmport) + n, err := sudp.h.encapsulate(carrierData[frameOffset:], 8) + if err != nil { + slog.Error("stackudp:demux", slog.String("err", err.Error())) + } + ufrm.SetLength(8 + uint16(n)) + // UDP CRC left to IP layer. + return n, err +} diff --git a/ntp/client.go b/ntp/client.go index d63f953..ba49eeb 100644 --- a/ntp/client.go +++ b/ntp/client.go @@ -19,13 +19,6 @@ const ( const sysprecRecalcNeeded int8 = 127 -func NewClient(now func() time.Time) *Client { - return &Client{ - _now: now, - _sysprec: sysprecRecalcNeeded, - } -} - type Client struct { connID uint64 start time.Time @@ -54,10 +47,11 @@ func (c *Client) ConnectionID() *uint64 { return &c.connID } -func (c *Client) Send(payload []byte) (int, error) { +func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) { if c.isDone() { return 0, io.EOF } + payload := carrierData[frameOffset:] frm, err := NewFrame(payload) if err != nil { return 0, err @@ -88,10 +82,11 @@ func (c *Client) Send(payload []byte) (int, error) { return SizeHeader, nil } -func (c *Client) Read(payload []byte) error { +func (c *Client) Demux(carrierData []byte, frameOffset int) error { if c.isDone() { return io.EOF } + payload := carrierData[frameOffset:] frm, err := NewFrame(payload) if err != nil { return err