Skip to content

Commit 07d2825

Browse files
committed
Merge branch 'release-0.1'
2 parents 3f06cd9 + 11d703e commit 07d2825

18 files changed

+1585
-1
lines changed

Dockerfile

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# ---- Base ----
2+
FROM alpine:3 AS base
3+
WORKDIR /app
4+
5+
6+
# ---- Build ----
7+
FROM golang:1.19-alpine AS build
8+
WORKDIR /build
9+
# Copy sources
10+
ADD . .
11+
# Get dependencies
12+
RUN go get ./cmd/app
13+
# Compile
14+
RUN CGO_ENABLED=0 go build -a -o app ./cmd/app
15+
16+
17+
# ---- Release ----
18+
FROM base AS release
19+
# Copy build-target
20+
COPY --from=build /build/app .
21+
22+
CMD ["./app"]

README.md

+79-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,82 @@
1-
RoutingTableToWGTranslator
1+
RoutingTableToWG
22
--
33

44
Translate Routing-Table Entries to Wireguard AllowedIPs with Filters
5+
6+
<br>
7+
8+
<!-- TOC -->
9+
- [1. Overview](#1-overview)
10+
- [1.1. Usage](#11-usage)
11+
- [1.2. Install](#12-install)
12+
- [2. Behaviour](#2-behaviour)
13+
- [2.1. Adding Route](#21-adding-route)
14+
- [2.2. Deleting Route](#22-deleting-route)
15+
<!-- /TOC -->
16+
17+
<br>
18+
19+
# 1. Overview
20+
21+
## 1.1. Usage
22+
23+
The Program will listen for RoutingTable Changes and can translate the changes to a Wireguard-Interface.
24+
25+
It will detect the Peer to add the Route to using the Gateway from the Route-Entry.<br>
26+
In case routes clash or cant be added to Wireguard, Warnings will be logged.
27+
28+
<br>
29+
30+
### 1.1.1. Examples
31+
32+
- Dynamic Routing with Routing-Protocols (e.g. OSPF)
33+
- Interacting with Wireguard using the familiar Tools like `iproute2`
34+
35+
<br>
36+
37+
## 1.2. Install
38+
39+
40+
41+
<br>
42+
43+
### 1.2.1. Docker
44+
45+
Depending on the needs, the Container can be run in `network_mode: host` to be able to accessrouting-tables and interfaces of the host.
46+
47+
<details><summary><code>docker-compose.yml</code></summary>
48+
49+
```yaml
50+
version: '3'
51+
52+
services:
53+
routingTableWGTranslator:
54+
image: ruakij/RoutingTableWGTranslator
55+
restart: unless-stopped
56+
network_mode: "host"
57+
environment:
58+
- INTERFACE="<wgInterfaceName or empty for wg0>"
59+
```
60+
</details>
61+
62+
<br>
63+
64+
### 1.2.2. Without Docker
65+
66+
Clone the Repository `git clone <URI>` and build the Program with `go build cmd/app`
67+
68+
Then you can run it with `./app`
69+
70+
<br>
71+
72+
# 2. Behaviour
73+
74+
## 2.1. Adding Route
75+
76+
![](doc/add-route.svg)
77+
78+
<br>
79+
80+
## 2.2. Deleting Route
81+
82+
![](doc/del-route.svg)

build/docker-multiarch.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
TAG="ruakij/routingtabletowg"
2+
PLATFORM="linux/amd64,linux/arm64/v8,linux/arm/v7"
3+
EXTRA_ARGS="$@"
4+
5+
docker buildx build \
6+
--platform $PLATFORM \
7+
--tag $TAG \
8+
$EXTRA_ARGS
9+
.

build/docker-ownarch.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
TAG="ruakij/routingtabletowg"
2+
EXTRA_ARGS="$@"
3+
4+
docker build \
5+
--tag $TAG \
6+
$EXTRA_ARGS \
7+
.

cmd/app/filter.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"github.com/vishvananda/netlink"
5+
)
6+
7+
type FilterOptions struct {
8+
Table int
9+
Protocol int
10+
}
11+
12+
func CheckFilter(options FilterOptions, route netlink.Route) bool {
13+
if (options.Table != -1 && options.Table != route.Table) ||
14+
(options.Protocol != -1 && options.Protocol != route.Protocol) {
15+
return false
16+
}
17+
return true
18+
}

cmd/app/logger.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
)
7+
8+
type Log struct {
9+
Info log.Logger
10+
Warn log.Logger
11+
Error log.Logger
12+
}
13+
var logger Log = Log{
14+
Info: *log.New(os.Stdout, "[INFO]\t", log.Ltime|log.Lshortfile),
15+
Warn: *log.New(os.Stderr, "[WARN]\t", log.Ltime|log.Lshortfile),
16+
Error: *log.New(os.Stderr, "[ERROR]\t", log.Ltime|log.Lshortfile),
17+
}

cmd/app/main.go

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package main
2+
3+
import (
4+
"net"
5+
"os"
6+
7+
envChecks "git.ruekov.eu/ruakij/routingtabletowg/lib/environmentchecks"
8+
ip2Map "git.ruekov.eu/ruakij/routingtabletowg/lib/iproute2mapping"
9+
"git.ruekov.eu/ruakij/routingtabletowg/lib/wgchecks"
10+
11+
"github.com/vishvananda/netlink"
12+
"golang.org/x/sys/unix"
13+
14+
"golang.zx2c4.com/wireguard/wgctrl"
15+
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
16+
)
17+
18+
var envRequired = []string{
19+
"INTERFACE",
20+
}
21+
var envDefaults = map[string]string{
22+
"INTERFACE": "wg0",
23+
//"MANAGE_ALL": "true",
24+
25+
"FILTER_PROTOCOL": "-1",
26+
"FILTER_TABLE": "-1",
27+
}
28+
29+
func main() {
30+
// Environment-vars
31+
err := envChecks.HandleRequired(envRequired)
32+
if(err != nil){
33+
logger.Error.Fatal(err)
34+
}
35+
envChecks.HandleDefaults(envDefaults)
36+
37+
iface := os.Getenv("INTERFACE")
38+
//MANAGE_ALL = os.Getenv("MANAGE_ALL")
39+
40+
// Parse filter-env-vars
41+
filterProtocolStr := os.Getenv("FILTER_PROTOCOL")
42+
filterProtocol, err := ip2Map.TryGetId(ip2Map.PROTOCOL, filterProtocolStr)
43+
if err != nil {
44+
logger.Error.Fatalf("Couldn't read FILTER_PROTOCOL '%s': %s", filterProtocolStr, err)
45+
}
46+
47+
filterTableStr := os.Getenv("FILTER_TABLE")
48+
filterTable, err := ip2Map.TryGetId(ip2Map.TABLE, filterTableStr)
49+
if err != nil {
50+
logger.Error.Fatalf("Couldn't read FILTER_TABLE '%s': %s", filterTableStr, err)
51+
}
52+
53+
// Create filter
54+
filterOptions := FilterOptions{
55+
Table: filterTable,
56+
Protocol: filterProtocol,
57+
}
58+
59+
// Get Link-Device
60+
link, err := netlink.LinkByName(iface)
61+
if err != nil {
62+
logger.Error.Fatalf("Couldn't get interface '%s': %s", iface, err)
63+
}
64+
65+
// Test getting wg-client
66+
client, err := wgctrl.New()
67+
if err != nil {
68+
logger.Error.Fatalf("Couldn't create wgctl-client: %s", err)
69+
}
70+
// Test getting wg-device
71+
_, err = client.Device(iface)
72+
if err != nil {
73+
logger.Error.Fatalf("Couldn't get wg-interface '%s': %s", iface, err)
74+
}
75+
76+
77+
// Subscribe to route-change events
78+
routeSubChan, routeSubDoneChan := make(chan netlink.RouteUpdate), make(chan struct{})
79+
80+
netlink.RouteSubscribe(routeSubChan, routeSubDoneChan)
81+
go handleRouteEvents(routeSubChan, filterOptions, iface)
82+
83+
//# Initial Route-setup
84+
// Get routing-table entries from device
85+
routeList, err := netlink.RouteList(link, netlink.FAMILY_ALL)
86+
if err != nil {
87+
logger.Error.Fatalf("Couldn't get route-entries: %s", err)
88+
}
89+
90+
logger.Info.Printf("Initially setting all current routes")
91+
for _, route := range routeList {
92+
// Ignore routes with empty gateway
93+
if(route.Gw == nil){
94+
continue
95+
}
96+
97+
// Send current routes to handler
98+
routeSubChan <- netlink.RouteUpdate{
99+
Type: unix.RTM_NEWROUTE,
100+
Route: route,
101+
}
102+
}
103+
104+
select {}
105+
}
106+
107+
var routeUpdateTypeMapFromId = map[uint16]string{
108+
unix.RTM_NEWROUTE: "+",
109+
unix.RTM_DELROUTE: "-",
110+
}
111+
// TODO: Add proxy to apply filter in channels rather than.. this mess
112+
func handleRouteEvents(routeSubChan <-chan netlink.RouteUpdate, filterOptions FilterOptions, iface string) {
113+
// Create wg-client
114+
client, err := wgctrl.New()
115+
if err != nil {
116+
logger.Error.Fatalf("Couldn't create wgctl-client: %s", err)
117+
}
118+
119+
for {
120+
// Receive Route-Updates
121+
routeUpdate := <-routeSubChan
122+
route := routeUpdate.Route
123+
124+
// Check filter
125+
if(!CheckFilter(filterOptions, routeUpdate.Route)){
126+
continue
127+
}
128+
129+
// Special case for default-route
130+
if route.Dst == nil{
131+
if route.Gw.To4() != nil { // IPv4
132+
route.Dst = &net.IPNet{
133+
IP: net.IPv4zero,
134+
Mask: net.CIDRMask(0, 32),
135+
}
136+
} else { // IPv6
137+
route.Dst = &net.IPNet{
138+
IP: net.IPv6zero,
139+
Mask: net.CIDRMask(0, 128),
140+
}
141+
}
142+
}
143+
144+
logger.Info.Printf("Route-Update: [%s] %s via %s", routeUpdateTypeMapFromId[routeUpdate.Type], route.Dst, route.Gw)
145+
146+
// Get wgDevice
147+
wgDevice, err := client.Device(iface)
148+
if err != nil {
149+
logger.Error.Fatalf("Couldn't get wg-interface '%s' while running: %s", iface, err)
150+
}
151+
152+
// Empty config for filling in switch
153+
var wgConfig wgtypes.Config
154+
155+
switch routeUpdate.Type{
156+
case unix.RTM_NEWROUTE:
157+
// Check if gateway is set
158+
if route.Gw == nil{
159+
logger.Warn.Printf("Gateway unset, ignoring")
160+
continue
161+
}
162+
163+
// Check if other peer already has exact same dst
164+
if peer, err := wgChecks.PeerByIPNet(wgDevice.Peers, *route.Dst); err == nil {
165+
logger.Warn.Printf("dst-IPNet already set for Peer '%s', ignoring", peer.PublicKey)
166+
continue
167+
}
168+
169+
// Get peer containing gateway-addr
170+
peer, err := wgChecks.PeerByIP(wgDevice.Peers, route.Gw)
171+
if(err != nil){
172+
logger.Warn.Printf("No peer found containing gw-IP '%s', ignoring", route.Gw)
173+
continue
174+
}
175+
176+
// Set peerConfig, this will override set values for that peer
177+
wgConfig.Peers = []wgtypes.PeerConfig{
178+
{
179+
PublicKey: peer.PublicKey,
180+
AllowedIPs: append(peer.AllowedIPs, *route.Dst),
181+
},
182+
}
183+
184+
case unix.RTM_DELROUTE:
185+
// Get peer containing dst-NetIP
186+
peerIndex, ipNetIndex, err := wgChecks.PeerIndexByIPNet(wgDevice.Peers, *route.Dst)
187+
if(err != nil){
188+
logger.Warn.Printf("No peer found having dst-IPNet '%s', ignoring", route.Dst)
189+
continue
190+
}
191+
peer := wgDevice.Peers[peerIndex]
192+
193+
// Delete dstNet from allowedIPs
194+
peer.AllowedIPs[ipNetIndex] = peer.AllowedIPs[len(peer.AllowedIPs)-1]
195+
peer.AllowedIPs = peer.AllowedIPs[:len(peer.AllowedIPs)-1]
196+
197+
// Set peerConfig, this will override set values for that peer
198+
wgConfig.Peers = []wgtypes.PeerConfig{
199+
{
200+
PublicKey: peer.PublicKey,
201+
UpdateOnly: true,
202+
203+
ReplaceAllowedIPs: true,
204+
AllowedIPs: peer.AllowedIPs,
205+
},
206+
}
207+
}
208+
209+
err = client.ConfigureDevice(iface, wgConfig)
210+
if(err != nil){
211+
logger.Error.Fatalf("Error configuring wg-device '%s': %s", iface, err)
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)