Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jan4843 committed May 14, 2023
0 parents commit 9f8d34f
Show file tree
Hide file tree
Showing 11 changed files with 511 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/release
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
release: \
linux/amd64 \
linux/arm \
linux/arm64 \

clean:
$(RM) -r release

NAME := $(shell go list)
VERSION := $(shell git name-rev --tags --name-only HEAD)
DISTS := $(shell go tool dist list)
$(DISTS): GOOS = $(firstword $(subst /, ,$@))
$(DISTS): GOARCH = $(lastword $(subst /, ,$@))
$(DISTS):
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -ldflags="-buildid= -s -w" -trimpath -o release/$(NAME)-$(VERSION)-$(GOOS)-$(GOARCH)
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# xarpd

xarpd bridges ARP requests between separate networks.

xarpd listens for incoming ARP requests, and when a request matches a configured subnetwork, it asks other peers running xarpd to resolve the request. When a peer can resolve the ARP request, xarpd responds to the initial ARP request with the hardware address of the host it is running on, so that traffic can be forwarded through it.

## Motivation

### Scenario

- `network1` is on `10.0.0.0/24` and has the hosts:
- `server1` with IP address `10.0.0.1`
- `device1` with IP address `10.0.0.42`
- `network2` is on `10.0.0.0/24` and has the hosts:
- `server2` with IP address `10.0.0.129`
- `device2` with IP address `10.0.0.170`

`network1` and `network2` have the same address but are different networks.

`server1` has a route to the subnetwork `10.0.0.128/25` of `network2` via a VPN and can therefore reach `server2` and `device2`.

It is assumed that `server1` and `server2` have IP forwarding enabled.

### Goal

`device1` needs to reach `device2` without the ability to join VPNs or configure custom routes. Therefore, the traffic would need to flow:
`device1` → `server1` → `server2` → `device2`.

### Solution

xarpd running on `server1` intercepts ARP requests from `device1` for `10.0.0.129` and replies with the hardware address of `server1`. Before replying, it asks `server2` to check whether `10.0.0.129` exists on `network2`.

## Usage

xarpd must be running on at least two networks to be useful.

On `server1` with IP address `10.0.0.1/24` in `network1`:

```console
$ xarpd 10.0.0.129/25
Forwarding ARP requests for 10.0.0.128/25 to 10.0.0.129
Listening for ARP on eth0
Listening for HTTP on 10.0.0.1:2707
```

On `server2` with IP address `10.0.0.129/24` in `network2`:

```console
$ xarpd 10.0.0.1/25
Forwarding ARP requests for 10.0.0.0/25 to 10.0.0.1
Listening for ARP on eth0
Listening for HTTP on 10.0.0.129:2707
```
114 changes: 114 additions & 0 deletions arp/arp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package arp

import (
"net"
"net/netip"
"sync"

"github.com/mdlayher/arp"
)

type Client struct {
client *arp.Client
mutex sync.Mutex
subs map[netip.Addr][]chan net.HardwareAddr
handleRequest RequestHandler
}

type RequestHandler func(ip netip.Addr) (shouldReply bool)

func NewClient(iface *net.Interface, handleRequest RequestHandler) (*Client, error) {
client, err := arp.Dial(iface)
if err != nil {
return nil, err
}
return &Client{
client: client,
subs: make(map[netip.Addr][]chan net.HardwareAddr),
handleRequest: handleRequest,
}, nil
}

func (c *Client) HardwareAddr() net.HardwareAddr {
return c.client.HardwareAddr()
}

func (c *Client) Subscribe(ip netip.Addr, ch chan net.HardwareAddr) {
c.mutex.Lock()
defer c.mutex.Unlock()

for _, existingCh := range c.subs[ip] {
if ch == existingCh {
return
}
}
c.subs[ip] = append(c.subs[ip], ch)
}

func (c *Client) Unsubscribe(ip netip.Addr, ch chan net.HardwareAddr) {
c.mutex.Lock()
defer c.mutex.Unlock()

for i, replyCh := range c.subs[ip] {
if replyCh == ch {
c.subs[ip][i] = c.subs[ip][len(c.subs[ip])-1]
c.subs[ip][len(c.subs[ip])-1] = nil
c.subs[ip] = c.subs[ip][:len(c.subs[ip])-1]
break
}
}
if len(c.subs[ip]) == 0 {
delete(c.subs, ip)
}
}

func (c *Client) Request(ip netip.Addr) {
c.client.Request(ip)
}

func (c *Client) Run() {
go func() {
for {
pkt, _, err := c.client.Read()
if err != nil {
continue
}

switch pkt.Operation {
case arp.OperationReply:
go c.handleReply(pkt)
case arp.OperationRequest:
go func() {
shouldReply := c.handleRequest(pkt.TargetIP)
if shouldReply {
c.reply(pkt)
}
}()
}
}
}()
}

func (c *Client) handleReply(pkt *arp.Packet) {
c.mutex.Lock()
defer c.mutex.Unlock()

for _, ch := range c.subs[pkt.SenderIP] {
select {
case ch <- pkt.SenderHardwareAddr:
default:
}
}
}

func (c *Client) reply(requestPkt *arp.Packet) error {
reply, err := arp.NewPacket(arp.OperationReply,
c.HardwareAddr(), requestPkt.TargetIP,
requestPkt.SenderHardwareAddr, requestPkt.SenderIP,
)
if err != nil {
return err
}

return c.client.WriteTo(reply, requestPkt.SenderHardwareAddr)
}
56 changes: 56 additions & 0 deletions arphttp/arphttp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package arphttp

import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"time"
)

const (
port = 2707
resolvePath = "/resolve"
selfPath = "/self"

resolveTimeout = 3 * time.Second
)

func Resolve(ip netip.Addr, clients ...*Client) (net.HardwareAddr, error) {
var result net.HardwareAddr
resolved := false

var wg sync.WaitGroup
var once sync.Once
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

for _, client := range clients {
client := client
if !client.ServerPrefix().Contains(ip) {
continue
}

wg.Add(1)
go func() {
defer wg.Done()

mac, err := client.ResolveWithContext(ctx, ip)
if err != nil {
return
}
once.Do(func() {
result = mac
resolved = true
cancel()
})
}()
}
wg.Wait()

if resolved {
return result, nil
}
return nil, fmt.Errorf("%s: cannot resolve", ip)
}
51 changes: 51 additions & 0 deletions arphttp/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package arphttp

import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"strings"
)

type Client struct {
serverPrefix netip.Prefix
httpClient *http.Client
}

func NewClient(serverPrefix netip.Prefix) *Client {
return &Client{
serverPrefix: serverPrefix,
httpClient: &http.Client{Timeout: resolveTimeout},
}
}

func (c *Client) ServerPrefix() netip.Prefix {
return c.serverPrefix
}

func (c *Client) ResolveWithContext(ctx context.Context, ip netip.Addr) (net.HardwareAddr, error) {
url := fmt.Sprintf("http://%s:%d%s?ip=%s", c.serverPrefix.Addr(), port, resolvePath, ip)
if c.serverPrefix.Addr() == ip {
url = fmt.Sprintf("http://%s:%d%s", c.serverPrefix.Addr(), port, selfPath)
}

request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
response, err := c.httpClient.Do(request)
if err != nil {
return nil, err
}

defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}

return net.ParseMAC(strings.TrimSpace(string(body)))
}
79 changes: 79 additions & 0 deletions arphttp/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package arphttp

import (
"fmt"
"net"
"net/http"
"net/netip"
"xarpd/arp"
)

type Server struct {
arpClient *arp.Client
addr string
}

func NewServer(arpClient *arp.Client, iface *net.Interface) (*Server, error) {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
if len(addrs) < 1 {
return nil, fmt.Errorf("no address available for interface %s", iface.Name)
}
prefix := netip.MustParsePrefix(addrs[0].String())
return &Server{
arpClient: arpClient,
addr: fmt.Sprintf("%s:%d", prefix.Addr(), port),
}, nil
}

func (s *Server) Addr() string {
return s.addr
}

func (s *Server) Start() error {
return http.ListenAndServe(s.addr, s)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
switch r.URL.Path {
case resolvePath:
s.resolve(w, r)
return
case selfPath:
s.self(w, r)
return
}
}

w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, "bad request")
}

func (s *Server) resolve(w http.ResponseWriter, r *http.Request) {
param := r.URL.Query().Get("ip")
ip, err := netip.ParseAddr(param)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, "invalid 'ip' query param")
return
}

arpReplies := make(chan net.HardwareAddr)
s.arpClient.Subscribe(ip, arpReplies)
defer s.arpClient.Unsubscribe(ip, arpReplies)

s.arpClient.Request(ip)

select {
case mac := <-arpReplies:
fmt.Fprintln(w, mac)
case <-r.Context().Done():
}
}

func (s *Server) self(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, s.arpClient.HardwareAddr())
}
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module xarpd

go 1.19

require github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875

require (
github.com/josharian/native v1.0.0 // indirect
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
github.com/mdlayher/packet v1.0.0 // indirect
github.com/mdlayher/socket v0.2.1 // indirect
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
)
Loading

0 comments on commit 9f8d34f

Please sign in to comment.