Skip to content

Commit

Permalink
Merge pull request #22 from synfinatic/auto-learn
Browse files Browse the repository at this point in the history
learn client IPs on non-broadcast interfaces
  • Loading branch information
synfinatic authored Oct 3, 2020
2 parents 939ecfd + 8a785e2 commit 9c36680
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 96 deletions.
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ GOARCH ?= $(shell uname -m)
BUILDINFOSDET ?=
UDP_PROXY_2020_ARGS ?=

PROJECT_VERSION := 0.0.3
PROJECT_VERSION := 0.0.4
DOCKER_REPO := synfinatic
PROJECT_NAME := udp-proxy-2020
PROJECT_TAG := $(shell git describe --tags 2>/dev/null $(git rev-list --tags --max-count=1))
Expand Down Expand Up @@ -79,21 +79,22 @@ fmt: ## Format Go code
@go fmt cmd

.PHONY: test-fmt
test-fmt: fmt
test-fmt: fmt ## Test to make sure code if formatted correctly
@if test `git diff cmd | wc -l` -gt 0; then \
echo "Code changes detected when running 'go fmt':" ; \
git diff -Xfiles ; \
exit -1 ; \
fi

.PHONY: test-tidy
test-tidy:
test-tidy: ## Test to make sure go.mod is tidy
@go mod tidy
@if test `git diff go.mod | wc -l` -gt 0; then \
echo "Need to run 'go mod tidy' to clean up go.mod" ; \
exit -1 ; \
fi

precheck: test test-fmt test-tidy ## Run all tests that happen in a PR

######################################################################
# Docker targets for testing
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,10 @@ udp-proxy-2020 is still under heavy development. Run `udp-proxy-2020 --help`
for a current list of command line options. Also, please note on many operating
systems you will need to run it as the `root` user. Linux systems can
optionally grant the `CAP_NET_RAW` capability.

Currently there are only a few flags you probaly need to worry about:

* `--interface` -- specify two or more network interfaces to listen on
* `--port` -- specify one or more UDP ports to monitor

There are other flags of course, run `./udp-proxy-2020 --help` for a full list.
5 changes: 3 additions & 2 deletions cmd/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ func initializeInterface(l *Listen) {
}

// set our BPF filter
log.Debugf("%s: applying BPF Filter: %s", l.iname, l.filter)
err = l.handle.SetBPFFilter(l.filter)
bpf_filter := buildBPFFilter(l.ports)
log.Debugf("%s: applying BPF Filter: %s", l.iname, bpf_filter)
err = l.handle.SetBPFFilter(bpf_filter)
if err != nil {
log.Fatalf("%s: %s", l.iname, err)
}
Expand Down
202 changes: 130 additions & 72 deletions cmd/listen.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package main

import (
"encoding/binary"
"net"
"strings"
"sync"
"time"

Expand All @@ -16,15 +16,16 @@ const SendBufferSize = 100

// Struct containing everything for an interface
type Listen struct {
iname string // interface to use
iface *net.Interface // interface descriptor
filter string // bpf filter string to listen on
ports []int32 // port(s) we listen for packets
ipaddr string // dstip we send packets to
promisc bool // do we enable promisc on this interface?
handle *pcap.Handle
timeout time.Duration
sendpkt chan Send // channel used to recieve packets we need to send
iname string // interface to use
netif *net.Interface // interface descriptor
ports []int32 // port(s) we listen for packets
ipaddr string // dstip we send packets to
promisc bool // do we enable promisc on this interface?
handle *pcap.Handle // gopacket.pcap handle
timeout time.Duration // timeout for loop
clientTTL time.Duration // ttl for client cache
sendpkt chan Send // channel used to recieve packets we need to send
clients map[string]time.Time // keep track of clients for non-promisc interfaces
}

// List of LayerTypes we support in sendPacket()
Expand All @@ -34,61 +35,52 @@ var validLinkTypes = []layers.LinkType{
layers.LinkTypeNull,
}

// takes the list of listen or promisc and returns a list of Listen
// which then can be initialized
func processListener(interfaces *[]string, lp []string, bpf_filter string, ports []int32, to time.Duration) []Listen {
var ret = []Listen{}
for _, i := range lp {
s := strings.Split(i, "@")
if len(s) != 2 {
log.Fatalf("%s is invalid. Expected: <interface>@<ipaddr>", i)
}
iname := s[0]
ipaddr := s[1]

iname_prefix := iname + "@"
if stringPrefixInSlice(iname_prefix, *interfaces) {
log.Fatalf("Can't specify the same interface (%s) multiple times", iname)
}
*interfaces = append(*interfaces, iname)

netif, err := net.InterfaceByName(iname)
// Creates a Listen struct for the given interface, promisc mode, udp sniff ports and timeout
func newListener(netif *net.Interface, promisc bool, ports []int32, to time.Duration) Listen {
log.Debugf("%s: ifIndex: %d", netif.Name, netif.Index)
addrs, err := netif.Addrs()
if err != nil {
log.Fatalf("Unable to obtain addresses for %s", netif.Name)
}
var bcastaddr string = ""
// only calc the broadcast address on promiscuous interfaces
// for non-promisc, we use our clients
if !promisc {
for _, addr := range addrs {
log.Debugf("%s network: %s\t\tstring: %s", netif.Name, addr.Network(), addr.String())

if err != nil {
log.Fatalf("Unable to get network index for %s: %s", iname, err)
_, ipNet, err := net.ParseCIDR(addr.String())
if err != nil {
log.Debugf("%s: Unable to parse CIDR: %s (%s)", netif.Name, addr.String(), addr.Network())
continue
}
if ipNet.IP.To4() == nil {
continue // Skip non-IPv4 addresses
}
// calc broadcast
ip := make(net.IP, len(ipNet.IP.To4()))
bcastbin := binary.BigEndian.Uint32(ipNet.IP.To4()) | ^binary.BigEndian.Uint32(net.IP(ipNet.Mask).To4())
binary.BigEndian.PutUint32(ip, bcastbin)
bcastaddr = ip.String()
}
log.Debugf("%s: ifIndex: %d", iname, netif.Index)

// check if interface has broadcast capabilities, when yes promisc = true
hasBroadcast := (netif.Flags & net.FlagBroadcast) != 0

new := Listen{
iname: iname,
iface: netif,
filter: bpf_filter,
ports: ports,
ipaddr: ipaddr,
timeout: to,
promisc: hasBroadcast,
handle: nil,
sendpkt: make(chan Send, SendBufferSize),
// promisc interfaces should have a bcast/ipv4 config
if len(bcastaddr) == 0 && promisc {
log.Fatalf("%s does not have a valid IPv4 configuration", netif.Name)
}
ret = append(ret, new)
}
return ret
}

// takes list of interfaces to listen on, if we should listen promiscuously,
// the BPF filter, list of ports and timeout and returns a list of processListener
func initializeListeners(inames []string, bpf_filter string, ports []int32, timeout time.Duration) []Listen {
// process our promisc and listen interfaces
var interfaces = []string{}
var listeners []Listen
a := processListener(&interfaces, inames, bpf_filter, ports, timeout)
for _, x := range a {
listeners = append(listeners, x)
new := Listen{
iname: netif.Name,
netif: netif,
ports: ports,
ipaddr: bcastaddr,
timeout: to,
promisc: promisc,
handle: nil,
sendpkt: make(chan Send, SendBufferSize),
clients: make(map[string]time.Time),
}
return listeners
log.Debugf("Listen: %v", new)
return new
}

// Our goroutine for processing packets
Expand All @@ -108,7 +100,7 @@ func (l *Listen) handlePackets(s *SendPktFeed, wg *sync.WaitGroup) {
for {
select {
case s := <-l.sendpkt: // packet arrived from another interface
l.sendPacket(s)
l.sendPackets(s)
case packet := <-packets: // packet arrived on this interfaces
// is it legit?
if packet.NetworkLayer() == nil || packet.TransportLayer() == nil || packet.TransportLayer().LayerType() != layers.LayerTypeUDP {
Expand All @@ -118,16 +110,28 @@ func (l *Listen) handlePackets(s *SendPktFeed, wg *sync.WaitGroup) {
log.Errorf("%s: Unable to decode: %s", l.iname, errx.Error())
}

// if our interface is non-promisc, learn the client IP
if l.promisc {
l.learnClientIP(packet)
}

log.Debugf("%s: received packet and fowarding onto other interfaces", l.iname)
s.Send(packet, l.iname, l.handle.LinkType())
case <-ticker: // our timer
log.Debugf("handlePackets(%s) ticker", l.iname)
// clean client cache
for k, v := range l.clients {
if v.Before(time.Now()) {
log.Debugf("%s removing %s after %dsec", l.iname, k, l.clientTTL)
delete(l.clients, k)
}
}
}
}
}

// Does the heavy lifting of editing & sending the packet onwards
func (l *Listen) sendPacket(sndpkt Send) {
func (l *Listen) sendPackets(sndpkt Send) {
var eth layers.Ethernet
var loop layers.Loopback // BSD NULL/Loopback used for OpenVPN tunnels/etc
var ip4 layers.IPv4 // we only support v4
Expand All @@ -146,7 +150,6 @@ func (l *Listen) sendPacket(sndpkt Send) {
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, &eth, &ip4, &udp, &payload)
default:
log.Fatalf("Unsupported source linktype: 0x%02x", sndpkt.linkType)
return
}

// try decoding our packet
Expand All @@ -156,7 +159,7 @@ func (l *Listen) sendPacket(sndpkt Send) {
return
}

// packet was decoded
// was packet decoded? In theory, this should never happen because our BPF filter...
found_udp := false
found_ipv4 := false
for _, layerType := range decoded {
Expand All @@ -172,6 +175,27 @@ func (l *Listen) sendPacket(sndpkt Send) {
return
}

if !l.promisc {
// send one packet to broadcast IP
dstip := net.ParseIP(l.ipaddr).To4()
if err, bytes := l.sendPacket(dstip, eth, loop, ip4, udp, payload); err != nil {
log.Warnf("Unable to send %d bytes from %s out %s: %s",
bytes, sndpkt.srcif, l.iname, err)
}
} else {
// sent packet to every client
for ip, _ := range l.clients {
dstip := net.ParseIP(ip).To4()
if err, bytes := l.sendPacket(dstip, eth, loop, ip4, udp, payload); err != nil {
log.Warnf("Unable to send %d bytes from %s out %s: %s",
bytes, sndpkt.srcif, l.iname, err)
}
}
}
}

func (l *Listen) sendPacket(dstip net.IP, eth layers.Ethernet, loop layers.Loopback,
ip4 layers.IPv4, udp layers.UDP, payload gopacket.Payload) (error, int) {
// Build our packet to send
buffer := gopacket.NewSerializeBuffer()
csum_opts := gopacket.SerializeOptions{
Expand Down Expand Up @@ -215,15 +239,15 @@ func (l *Listen) sendPacket(sndpkt Send) {
Protocol: ip4.Protocol,
Checksum: 0, // reset to calc checksums
SrcIP: ip4.SrcIP,
DstIP: net.ParseIP(l.ipaddr).To4(),
DstIP: dstip,
Options: ip4.Options,
}
if err := new_ip4.SerializeTo(buffer, csum_opts); err != nil {
log.Fatalf("can't serialize IP header: %v", new_ip4)
}

// Loopback or Ethernet
if (l.iface.Flags & net.FlagLoopback) > 0 {
if (l.netif.Flags & net.FlagLoopback) > 0 {
loop := layers.Loopback{
Family: layers.ProtocolFamilyIPv4,
}
Expand All @@ -235,7 +259,7 @@ func (l *Listen) sendPacket(sndpkt Send) {
new_eth := layers.Ethernet{
BaseLayer: layers.BaseLayer{},
DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
SrcMAC: l.iface.HardwareAddr,
SrcMAC: l.netif.HardwareAddr,
EthernetType: layers.EthernetTypeIPv4,
}
if err := new_eth.SerializeTo(buffer, opts); err != nil {
Expand All @@ -244,11 +268,45 @@ func (l *Listen) sendPacket(sndpkt Send) {
}

outgoingPacket := buffer.Bytes()
log.Debugf("%s: packet len: %d: %v", l.iname, len(outgoingPacket), outgoingPacket)
err := l.handle.WritePacketData(outgoingPacket)
if err != nil {
log.Warnf("Unable to send %d bytes from %s out %s: %s",
len(outgoingPacket), sndpkt.srcif, l.iname, err)
log.Debugf("%s => %s: packet len: %d: %v", l.iname, dstip.String(), len(outgoingPacket), outgoingPacket)
return l.handle.WritePacketData(outgoingPacket), len(outgoingPacket)
}

func (l *Listen) learnClientIP(packet gopacket.Packet) {
var eth layers.Ethernet
var loop layers.Loopback
var ip4 layers.IPv4
var udp layers.UDP
var payload gopacket.Payload
var parser *gopacket.DecodingLayerParser

switch l.handle.LinkType() {
case layers.LinkTypeNull:
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeLoopback, &loop, &ip4, &udp, &payload)
case layers.LinkTypeLoop:
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeLoopback, &loop, &ip4, &udp, &payload)
case layers.LinkTypeEthernet:
parser = gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, &eth, &ip4, &udp, &payload)
default:
log.Fatalf("Unsupported source linktype: 0x%02x", l.handle.LinkType())
}

decoded := []gopacket.LayerType{}
if err := parser.DecodeLayers(packet.Data(), &decoded); err != nil {
log.Debugf("Unable to decoded client IP on %s: %s", l.iname, err)
}

found_ipv4 := false
for _, layerType := range decoded {
switch layerType {
case layers.LayerTypeIPv4:
// found our v4 header
found_ipv4 = true
}
}

if found_ipv4 {
l.clients[ip4.SrcIP.String()] = time.Now().Add(l.clientTTL)
}
}

Expand Down
Loading

0 comments on commit 9c36680

Please sign in to comment.