├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── go.mod ├── go.sum └── replicator.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine as builder 2 | 3 | # Setup 4 | RUN mkdir -p /go/src/github.com/thomseddon/udp-replicator 5 | WORKDIR /go/src/github.com/thomseddon/udp-replicator 6 | 7 | # Add libraries 8 | RUN apk add --no-cache git 9 | 10 | # Copy & build 11 | ADD . /go/src/github.com/thomseddon/udp-replicator/ 12 | RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -installsuffix nocgo -o /udp-replicator github.com/thomseddon/udp-replicator 13 | 14 | # Copy into scratch container 15 | FROM scratch 16 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 17 | COPY --from=builder /udp-replicator ./ 18 | ENTRYPOINT ["./udp-replicator"] 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2019] [Thom Seddon] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | format: 2 | gofmt -w -s *.go 3 | 4 | .PHONY: format 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # UDP Replicator [![Go Report Card](https://goreportcard.com/badge/github.com/thomseddon/udp-replicator)](https://goreportcard.com/report/github.com/thomseddon/udp-replicator) 3 | 4 | A tiny UDP proxy that can replicate traffic to one or more endpoints. 5 | 6 | ## Why? 7 | 8 | We needed a way to take a single netflow stream and send it to multiple endpoints. As this is a generic UDP replicator, it can be used for any traffic such as netflow, syslog etc. 9 | 10 | You could use iptables, [like Zapier](https://zapier.com/engineering/iptables-replication/), but we wanted something we could easily deploy into our kubernetes cluster. There are also a number of existing UDP proxies, but non of the popular ones support replication. There are also a small number of replicators, but these all seem to be mostly unmaintained/untested toy projects with little documentation. 11 | 12 | ## Usage 13 | 14 | ### Direct 15 | 16 | ``` 17 | ./replicator --listen-port=9500 --forward=192.0.2.1 --forward=198.51.100.5:9100 18 | time="2019-09-23T14:29:50+01:00" level=info msg="Server started" ip=0.0.0.0 port=9500 19 | time="2019-09-23T14:29:50+01:00" level=info msg="Forwarding target configured" addr="192.0.2.1:9500" num=1 total=2 20 | time="2019-09-23T14:29:50+01:00" level=info msg="Forwarding target configured" addr="198.51.100.5:9100" num=2 total=2 21 | ``` 22 | 23 | The above command will: 24 | 25 | - Start a UDP server listening on `0.0.0.0` (the default) port `9500` 26 | - Add a forward target of `192.0.2.1:9500` (uses `listen-port` for destination port as not specified in configuration) 27 | - Add another forward target of `198.51.100.5:9100` 28 | 29 | The server will start listening on `0.0.0.0:9500`, any packet it receives will be replicated and sent to both `192.0.2.1:9500` and `198.51.100.5:9100` 30 | 31 | 32 | ### Docker 33 | 34 | ``` 35 | docker run -e FORWARDS=$'192.0.2.1\n198.51.100.5:9100' thomseddon/udp-replicator:1 36 | ``` 37 | 38 | ### Docker Compose 39 | 40 | ``` 41 | version: '3' 42 | 43 | services: 44 | udp-replicator: 45 | run: thomseddon/udp-replicator:1 46 | environment: 47 | DEBUG: "true" 48 | FORWARD: "192.0.2.1\n198.51.100.5:9100" 49 | ``` 50 | 51 | 52 | ## Configuration 53 | 54 | ``` 55 | usage: replicator [] 56 | 57 | Flags: 58 | --help Show context-sensitive help (also try --help-long and --help-man). 59 | --debug Enable debug mode 60 | --listen-ip=0.0.0.0 IP to listen in 61 | --listen-port=9000 Port to listen on 62 | --body-size=4096 Size of body to read 63 | --forward=ip:port ... ip:port to forward traffic to (port defaults to listen-port) 64 | ``` 65 | 66 | All configuration params can be passed as arguments as shown above, or as environment varaibles (as shown in usage above) - where environment variables are uppercased snake case e.g. `LISTEN_IP` 67 | 68 | ## Copyright 69 | 70 | 2019 Thom Seddon 71 | 72 | ## License 73 | 74 | [MIT](https://github.com/thomseddon/udp-replicator/blob/master/LICENSE.md) 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thomseddon/udp-replicator 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 7 | github.com/alecthomas/units v0.0.0-20190910110746-680d30ca3117 // indirect 8 | github.com/sirupsen/logrus v1.4.2 // indirect 9 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20190910110746-680d30ca3117 h1:aUo+WrWZtRRfc6WITdEKzEczFRlEpfW15NhNeLRc17U= 4 | github.com/alecthomas/units v0.0.0-20190910110746-680d30ca3117/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 10 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 14 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 15 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 16 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 18 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 21 | -------------------------------------------------------------------------------- /replicator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "gopkg.in/alecthomas/kingpin.v2" 10 | ) 11 | 12 | var ( 13 | debug = kingpin.Flag("debug", "Enable debug mode").Envar("DEBUG").Bool() 14 | listenIP = kingpin.Flag("listen-ip", "IP to listen in").Default("0.0.0.0").Envar("LISTEN_IP").IP() 15 | listenPort = kingpin.Flag("listen-port", "Port to listen on").Default("9000").Envar("LISTEN_PORT").Int() 16 | bodySize = kingpin.Flag("body-size", "Size of body to read").Default("4096").Envar("BODY_SIZE").Int() 17 | 18 | forwards = kingpin.Flag("forward", "ip:port to forward traffic to (port defaults to listen-port)").PlaceHolder("ip:port").Envar("FORWARD").Strings() 19 | 20 | pretty = kingpin.Flag("pretty", "").Default("true").Envar("PRETTY").Hidden().Bool() 21 | 22 | targets []*net.UDPConn 23 | ) 24 | 25 | func main() { 26 | // CLI 27 | kingpin.Parse() 28 | 29 | // Log setup 30 | if *debug { 31 | log.SetLevel(log.DebugLevel) 32 | } else { 33 | log.SetLevel(log.InfoLevel) 34 | } 35 | if !*pretty { 36 | log.SetFormatter(&log.TextFormatter{ 37 | DisableColors: true, 38 | FullTimestamp: true, 39 | }) 40 | } 41 | 42 | if len(*forwards) <= 0 { 43 | log.Fatal("Must specify at least one forward target") 44 | } 45 | 46 | // Clients 47 | for _, forward := range *forwards { 48 | // Check for port 49 | if strings.Index(forward, ":") < 0 { 50 | forward = fmt.Sprintf("%s:%d", forward, *listenPort) 51 | } 52 | 53 | // Resolve 54 | addr, err := net.ResolveUDPAddr("udp", forward) 55 | if err != nil { 56 | log.Fatalf("Could not ResolveUDPAddr: %s (%s)", forward, err) 57 | } 58 | 59 | // Setup conn 60 | conn, err := net.DialUDP("udp", nil, addr) 61 | if err != nil { 62 | log.Fatalf("Could not DialUDP: %+v (%s)", addr, err) 63 | } 64 | defer conn.Close() 65 | 66 | targets = append(targets, conn) 67 | } 68 | 69 | // Server 70 | conn, err := net.ListenUDP("udp", &net.UDPAddr{ 71 | Port: *listenPort, 72 | IP: *listenIP, 73 | }) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | defer conn.Close() 79 | 80 | // Startup status 81 | log.WithFields(log.Fields{ 82 | "ip": *listenIP, 83 | "port": *listenPort, 84 | }).Infof("Server started") 85 | for i, target := range targets { 86 | log.WithFields(log.Fields{ 87 | "num": i + 1, 88 | "total": len(targets), 89 | "addr": target.RemoteAddr(), 90 | }).Info("Forwarding target configured") 91 | } 92 | 93 | for { 94 | // Read 95 | b := make([]byte, *bodySize) 96 | n, addr, err := conn.ReadFromUDP(b) 97 | if err != nil { 98 | log.Error(err) 99 | continue 100 | } 101 | 102 | // Log receive 103 | ctxLog := log.WithFields(log.Fields{ 104 | "source": addr.String(), 105 | "body": string(b[:n]), 106 | }) 107 | ctxLog.Debugf("Recieved packet") 108 | 109 | // Proxy 110 | for _, target := range targets { 111 | _, err := target.Write(b[:n]) 112 | 113 | // Log proxy 114 | ctxLog := ctxLog.WithFields(log.Fields{ 115 | "target": target.RemoteAddr(), 116 | }) 117 | 118 | if err != nil { 119 | ctxLog.Warn("Could not forward packet", err) 120 | } else { 121 | ctxLog.Debug("Wrote to target") 122 | } 123 | } 124 | } 125 | } 126 | --------------------------------------------------------------------------------