-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9f8d34f
Showing
11 changed files
with
511 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Oops, something went wrong.