Skip to content

Commit

Permalink
remodel ARP client implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
soypat committed Dec 12, 2023
1 parent 9fd0611 commit d39f48a
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 65 deletions.
121 changes: 71 additions & 50 deletions stacks/arp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,15 @@ package stacks
import (
"errors"
"log/slog"
"net/netip"

"github.com/soypat/seqs/eth"
)

func (ps *PortStack) BeginResolveARPv4(target [4]byte) {
ps.arpResult = eth.ARPv4Header{
Operation: 1, // Request.
HardwareType: 1, // Ethernet.
ProtoType: uint16(eth.EtherTypeIPv4),
HardwareLength: 6,
ProtoLength: 4,
HardwareSender: ps.MACAs6(),
ProtoSender: ps.ip,
HardwareTarget: [6]byte{}, // Zeroes, is filled by target.
ProtoTarget: target,
}
}

// ARPv4Result returns the result of the last ARPv4 request.
func (ps *PortStack) ARPv4Result() (eth.ARPv4Header, bool) {
return ps.arpResult, ps.arpResult.Operation == 2
}

func (ps *PortStack) pendingReplyToARP() bool {
return ps.pendingARPresponse.Operation == 2 // 2 means reply.
}

func (ps *PortStack) pendingResolveARPv4() bool {
return ps.arpResult.Operation == 1 // User asked for a ARP request.
// ARP returns the ARP client for this stack. The type is not exported since
// it's implementation is experimental.
func (ps *PortStack) ARP() *arpClient {
return &ps.arpClient
}

/*
Expand All @@ -52,75 +32,116 @@ PortStack.pendingARPresponse contains state:
1. Upon ARP request in `recvARP`, case 1: Store outgoing ARP response in PortStack.pendingARPresponse. PortStack.pendingARPresponse.Operation = 2 (reply)
2. Upon `handleARP`: Packet sent out. PortStack.pendingARPresponse.Operation = 0 (no pending response) Ready to receive.
*/
type arpClient struct {
stack *PortStack
result eth.ARPv4Header
pendingResponse eth.ARPv4Header
}

func (ps *PortStack) handleARP(dst []byte) (n int) {
pendingResolve := ps.pendingResolveARPv4()
func (c *arpClient) ResultAs6() (netip.Addr, [6]byte, error) {
switch c.result.Operation {
case 0:
return netip.Addr{}, [6]byte{}, errors.New("no ARP in progress")
case 1:
return netip.Addr{}, [6]byte{}, errors.New("ARP response pending")
}
return netip.AddrFrom4(c.result.ProtoSender), c.result.HardwareSender, nil
}

func (c *arpClient) BeginResolve(addr netip.Addr) error {
if !addr.Is4() {
return errors.New("ARP only supports IPv4")
}
c.result = eth.ARPv4Header{
Operation: 1, // Request.
HardwareType: 1, // Ethernet.
ProtoType: uint16(eth.EtherTypeIPv4),
HardwareLength: 6,
ProtoLength: 4,
HardwareSender: c.stack.MACAs6(),
ProtoSender: c.stack.ip,
HardwareTarget: [6]byte{}, // Zeroes, is filled by target.
ProtoTarget: addr.As4(),
}
return nil
}

func (c *arpClient) isPending() bool {
return c.pendingReplyToARP() || c.pendingResolveARPv4()
}

func (c *arpClient) pendingReplyToARP() bool {
return c.pendingResponse.Operation == 2 // 2 means reply.
}

func (c *arpClient) pendingResolveARPv4() bool {
return c.result.Operation == 1 // User asked for a ARP request.
}

func (c *arpClient) handle(dst []byte) (n int) {
pendingResolve := c.pendingResolveARPv4()
switch {
case pendingResolve:
// We have a pending request from user to perform ARP.
ehdr := eth.EthernetHeader{
Destination: eth.BroadcastHW6(),
Source: ps.MACAs6(),
Source: c.stack.MACAs6(),
SizeOrEtherType: uint16(eth.EtherTypeARP),
}
ehdr.Put(dst)
ps.arpResult.Put(dst[eth.SizeEthernetHeader:])
ps.arpResult.Operation = arpOpWait // Clear pending ARP to not loop.
c.result.Put(dst[eth.SizeEthernetHeader:])
c.result.Operation = arpOpWait // Clear pending ARP to not loop.
n = eth.SizeEthernetHeader + eth.SizeARPv4Header

case ps.pendingReplyToARP():
case c.pendingReplyToARP():
// We need to respond to an ARP request that queries our address.
ehdr := eth.EthernetHeader{
Destination: ps.pendingARPresponse.HardwareTarget,
Source: ps.MACAs6(),
Destination: c.pendingResponse.HardwareTarget,
Source: c.stack.MACAs6(),
SizeOrEtherType: uint16(eth.EtherTypeARP),
}
ehdr.Put(dst)
ps.pendingARPresponse.Put(dst[eth.SizeEthernetHeader:])
ps.pendingARPresponse.Operation = 0 // Clear pending ARP.
c.pendingResponse.Put(dst[eth.SizeEthernetHeader:])
c.pendingResponse.Operation = 0 // Clear pending ARP.
n = eth.SizeEthernetHeader + eth.SizeARPv4Header

default:
// return 0 // Nothing to do, n=0.
}
if n > 0 {
ps.debug("ARP:send", slog.Bool("isReply", !pendingResolve))
c.stack.debug("ARP:send", slog.Bool("isReply", !pendingResolve))
}
return n
}

func (ps *PortStack) recvARP(ethPayload []byte) error {
if len(ethPayload) < eth.SizeARPv4Header {
return errors.New("short ARP payload")
}
ahdr := eth.DecodeARPv4Header(ethPayload)
func (c *arpClient) recv(ahdr *eth.ARPv4Header) error {
if ahdr.HardwareLength != 6 || ahdr.ProtoLength != 4 || ahdr.HardwareType != 1 || ahdr.AssertEtherType() != eth.EtherTypeIPv4 {
return errors.New("unsupported ARP") // Ignore ARP unsupported requests.
}
switch ahdr.Operation {
case 1: // We received ARP request.
if ps.pendingReplyToARP() || ahdr.ProtoTarget != ps.ip {
if c.pendingReplyToARP() || ahdr.ProtoTarget != c.stack.ip {
return nil // ARP reply pending or not for us.
}
// We need to respond to this ARP request by inverting Sender/Target fields.
ahdr.HardwareTarget = ahdr.HardwareSender
ahdr.ProtoTarget = ahdr.ProtoSender

ahdr.HardwareSender = ps.MACAs6()
ahdr.ProtoSender = ps.ip
ahdr.HardwareSender = c.stack.MACAs6()
ahdr.ProtoSender = c.stack.ip
ahdr.Operation = 2 // Set as reply. This also flags the packet as pending.
ps.pendingARPresponse = ahdr
c.pendingResponse = *ahdr

case 2: // We received ARP reply.
if ps.arpResult.Operation != arpOpWait || // Result already received
ahdr.ProtoTarget != ps.ip || // Not meant for us.
ahdr.ProtoSender != ps.arpResult.ProtoTarget { // does not correspond to last request.
if c.result.Operation != arpOpWait || // Result already received
ahdr.ProtoTarget != c.stack.ip || // Not meant for us.
ahdr.ProtoSender != c.result.ProtoTarget { // does not correspond to last request.
return nil
}
ps.arpResult = ahdr
c.result = *ahdr
default:
return errors.New("unsupported ARP operation")
}
ps.debug("ARP:recv", slog.Int("op", int(ahdr.Operation)))
c.stack.debug("ARP:recv", slog.Int("op", int(ahdr.Operation)))
return nil
}
18 changes: 11 additions & 7 deletions stacks/portstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ type PortStackConfig struct {

// NewPortStack creates a ready to use TCP/UDP Stack instance.
func NewPortStack(cfg PortStackConfig) *PortStack {
var s PortStack
s := &PortStack{}
s.arpClient.stack = s
s.mac = cfg.MAC
// s.ip = cfg.IP.As4()
s.portsUDP = make([]udpPort, cfg.MaxOpenPortsUDP)
Expand All @@ -47,7 +48,7 @@ func NewPortStack(cfg PortStackConfig) *PortStack {
panic("please use a smaller MTU. max=" + strconv.Itoa(defaultMTU))
}
s.mtu = cfg.MTU
return &s
return s
}

var ErrFlagPending = io.ErrNoProgress
Expand Down Expand Up @@ -100,7 +101,7 @@ type PortStack struct {
// that have been dropped due to the port requiring handling before admitting more packets.
droppedPackets uint32
// ARP state. See arp.go for detailed information on the ARP state machine.
pendingARPresponse, arpResult eth.ARPv4Header
arpClient arpClient
// Auxiliary struct to avoid allocations passed to global handler.
auxEth eth.EthernetHeader
mac [6]byte
Expand Down Expand Up @@ -175,7 +176,11 @@ func (ps *PortStack) RecvEth(ethernetFrame []byte) (err error) {
}

if etype == eth.EtherTypeARP {
return ps.recvARP(payload[eth.SizeEthernetHeader:])
if len(payload) < eth.SizeEthernetHeader+eth.SizeARPv4Header {
return errPacketSmol
}
ahdr := eth.DecodeARPv4Header(payload[eth.SizeEthernetHeader:])
return ps.arpClient.recv(&ahdr)
}

// IP parsing block.
Expand Down Expand Up @@ -352,8 +357,7 @@ func (ps *PortStack) handleEth(dst []byte) (n int, err error) {
case !ps.IsPendingHandling():
return 0, nil // No remaining packets to handle.
}

n = ps.handleARP(dst)
n = ps.arpClient.handle(dst)
if n != 0 {
return n, nil
}
Expand Down Expand Up @@ -429,7 +433,7 @@ func (ps *PortStack) handleEth(dst []byte) (n int, err error) {

// IsPendingHandling checks if a call to HandleEth could possibly result in a packet being generated by the PortStack.
func (ps *PortStack) IsPendingHandling() bool {
return ps.pendingUDPv4 > 0 || ps.pendingTCPv4 > 0 || ps.pendingResolveARPv4() || ps.pendingReplyToARP()
return ps.pendingUDPv4 > 0 || ps.pendingTCPv4 > 0 || ps.arpClient.isPending()
}

// OpenUDP opens a UDP port and sets the handler.
Expand Down
19 changes: 11 additions & 8 deletions stacks/stacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func TestARP(t *testing.T) {
target := stacks[1]
const expectedARP = eth.SizeEthernetHeader + eth.SizeARPv4Header
// Send ARP request from sender to target.
sender.BeginResolveARPv4(target.Addr().As4())
sender.ARP().BeginResolve(target.Addr())
egr := NewExchanger(stacks...)
ex, n := egr.DoExchanges(t, 1)
if n != expectedARP {
Expand All @@ -108,15 +108,18 @@ func TestARP(t *testing.T) {
t.Errorf("ex[%d] sent=%d want=%d", ex, n, expectedARP)
}

result, ok := sender.ARPv4Result()
if !ok {
t.Fatal("no ARP result", result.HardwareSender)
ip, mac, err := sender.ARP().ResultAs6()
if err != nil {
t.Fatal(err)
}
if !ip.IsValid() {
t.Fatal("invalid IP")
}
if result.HardwareSender != target.MACAs6() {
t.Errorf("result.HardwareSender=%s want=%s", result.HardwareSender, target.MACAs6())
if mac != target.MACAs6() {
t.Errorf("result.HardwareSender=%s want=%s", mac, target.MACAs6())
}
if result.ProtoSender != target.Addr().As4() {
t.Errorf("result.ProtoSender=%s want=%s", result.ProtoSender, target.Addr().As4())
if ip.As4() != target.Addr().As4() {
t.Errorf("result.ProtoSender=%s want=%s", ip, target.Addr().As4())
}

// No more data to exchange.
Expand Down

0 comments on commit d39f48a

Please sign in to comment.