Skip to content
This repository was archived by the owner on Jun 3, 2025. It is now read-only.

Commit d4ca398

Browse files
committed
feat: initial project
0 parents  commit d4ca398

File tree

7 files changed

+235
-0
lines changed

7 files changed

+235
-0
lines changed

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.go]
12+
indent_style = tab
13+
14+
[*.{y{a,}ml,md}]
15+
indent_size = 2

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM golang:1.22 as builder
2+
WORKDIR /build
3+
4+
COPY go.mod go.sum ./
5+
RUN go mod download
6+
7+
COPY . /build
8+
RUN CGO_ENABLED=0 go build -o /udp-proxy
9+
10+
FROM alpine:3.20 AS runner
11+
WORKDIR /data
12+
13+
COPY --from=builder /udp-proxy /bin/udp-proxy
14+
ENTRYPOINT [ "/bin/udp-proxy" ]

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2024 Netresearch DTT GmbH
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# udp-proxy
2+
3+
A small proxy that proxies UDP packets from one port to another depending on the source IP.
4+
5+
## Installation
6+
7+
You can download the binary for your platform from the [releases page](https://github.com/netresearch/udp-proxy/releases) or alternatively use the [Docker image](https://github.com/netresearch/udp-proxy/pkgs/container/udp-proxy).
8+
9+
## Usage
10+
11+
With the binary:
12+
13+
```
14+
--port <port> The port to listen on
15+
--forward <source-ip>:<remote-ip>:<remote-port>
16+
Forward packets from <source-ip> to <remote-ip>:<remote-port>, you can specify --forward multiple times
17+
```
18+
19+
```sh
20+
./udp-proxy --port 5000 --forward <source-ip>:<remote-ip>:<remote-port>
21+
```
22+
23+
Or with Docker:
24+
25+
```sh
26+
docker run --rm -p <listen-port>:<listen-port>/udp ghcr.io/netresearch/udp-proxy --port <listen-port> --forward <source-ip>:<remote-ip>:<remote-port>
27+
```
28+
29+
You can also use the following SystemD service file to run the proxy as a service:
30+
31+
```ini
32+
[Unit]
33+
Description=UDP Proxy
34+
Documentation=https://github.com/netresearch/udp-proxy
35+
After=network.target
36+
Requires=network.target
37+
38+
[Service]
39+
Type=simple
40+
ExecStart=/usr/bin/udp-proxy --port 5000 --forward <source-ip>:<remote-ip>:<remote-port>
41+
Restart=always
42+
43+
[Install]
44+
WantedBy=multi-user.target
45+
```
46+
47+
## License
48+
49+
`udp-proxy` is licensed under the MIT license. See the included [LICENSE file](./LICENSE) for more information.

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/netresearch/udp-proxy
2+
3+
go 1.22
4+
5+
require (
6+
github.com/rs/zerolog v1.33.0
7+
github.com/spf13/pflag v1.0.5
8+
)
9+
10+
require (
11+
github.com/mattn/go-colorable v0.1.13 // indirect
12+
github.com/mattn/go-isatty v0.0.19 // indirect
13+
golang.org/x/sys v0.15.0 // indirect
14+
)

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
2+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
3+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
4+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
5+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
6+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
7+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
8+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9+
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
10+
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
11+
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
12+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
13+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
14+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
17+
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
18+
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

main.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"net"
5+
"os"
6+
"strings"
7+
8+
"github.com/rs/zerolog"
9+
"github.com/rs/zerolog/log"
10+
"github.com/spf13/pflag"
11+
)
12+
13+
func main() {
14+
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
15+
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.TraceLevel)
16+
17+
config := parseOptions()
18+
19+
listener, err := net.ListenUDP("udp", &net.UDPAddr{Port: config.Port})
20+
if err != nil {
21+
log.Fatal().Err(err).Msg("Failed to listen")
22+
}
23+
24+
log.Info().Int("port", config.Port).Msg("Listening on UDP port")
25+
log.Info().Msg("Forward targets:")
26+
for k, v := range config.Forwards {
27+
log.Info().Msgf(" %s -> %s", net.IP(k), v)
28+
}
29+
30+
for {
31+
buf := make([]byte, 1024)
32+
n, addr, err := listener.ReadFromUDP(buf)
33+
if err != nil {
34+
log.Error().Err(err).Msg("Failed to read from UDP")
35+
continue
36+
}
37+
38+
forward(config, addr, buf[:n])
39+
}
40+
}
41+
42+
type Config struct {
43+
Port int
44+
Forwards map[string]*net.UDPAddr
45+
}
46+
47+
func parseOptions() *Config {
48+
port := pflag.Int("port", 5000, "Port to listen on")
49+
forwards := pflag.StringArray("forward", []string{}, "Forwards (can be specified multiple times, format: sourceIP:targetIP:targetPort)")
50+
51+
pflag.Parse()
52+
53+
return &Config{
54+
Port: *port,
55+
Forwards: parseForwards(*forwards),
56+
}
57+
}
58+
59+
func forward(config *Config, addr *net.UDPAddr, buf []byte) {
60+
remoteIP := addr.IP.String()
61+
62+
l := log.With().Str("remote", remoteIP).Logger()
63+
64+
target, ok := config.Forwards[string(addr.IP.To16())]
65+
if !ok {
66+
l.Warn().Msg("No forward for remote IP")
67+
return
68+
}
69+
targetAddr := target.String()
70+
71+
l = l.With().Str("target", targetAddr).Logger()
72+
73+
conn, err := net.DialUDP("udp", nil, target)
74+
if err != nil {
75+
l.Error().Err(err).Msg("Failed to connect to target")
76+
return
77+
}
78+
defer conn.Close()
79+
80+
if _, err = conn.Write(buf); err != nil {
81+
l.Error().Err(err).Msg("Failed to write to target")
82+
return
83+
}
84+
85+
l.Info().Int("bytes", len(buf)).Msg("Forwarded")
86+
}
87+
88+
func parseForwards(raw []string) map[string]*net.UDPAddr {
89+
forwards := make(map[string]*net.UDPAddr)
90+
91+
for _, rawForward := range raw {
92+
l := log.With().Str("forward", rawForward).Logger()
93+
94+
parts := strings.SplitN(rawForward, ":", 2)
95+
if len(parts) != 2 {
96+
l.Error().Msg("Failed to parse forward, expected 2 parts separated by :")
97+
continue
98+
}
99+
100+
rawSourceIP := parts[0]
101+
sourceIP := net.ParseIP(rawSourceIP)
102+
if sourceIP == nil {
103+
l.Error().Msg("Failed to parse source IP")
104+
continue
105+
}
106+
107+
rawTarget := parts[1]
108+
target, err := net.ResolveUDPAddr("udp", rawTarget)
109+
if err != nil {
110+
l.Error().Err(err).Msg("Failed to resolve target")
111+
continue
112+
}
113+
114+
forwards[string(sourceIP.To16())] = target
115+
}
116+
117+
return forwards
118+
}

0 commit comments

Comments
 (0)