Skip to content

Commit b78d0f1

Browse files
feat: improve filtering for IPv6 based on MAC, IP, and source
1 parent aca7324 commit b78d0f1

13 files changed

Lines changed: 816 additions & 110 deletions

File tree

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,15 @@ For a complete reference with all available options and more providers, check th
140140
tasks:
141141
# Whichever name you like for this task, it is only for reference
142142
my_public_web_server:
143-
# Only update IPv6 addresses within these subnets ("2000::/3" covers all Global Unicast Addresses)
144-
subnets:
145-
- 2000::/3
146-
# MAC addresses of the hosts to monitor
147-
mac_address:
148-
- 00:11:22:33:44:55
149-
- 00:11:22:33:44:56
143+
filter:
144+
- mac:
145+
address: "00:11:22:33:44:55"
146+
ip:
147+
# Only use Global Unicast Addresses (publicly routable)
148+
type:
149+
- global
150+
# Optional: Exclude privacy extensions (temporary addresses) by ensuring it is EUI64 derived
151+
# - eui64
150152
endpoints:
151153
# "example-cloudflare" refers to a credential block defined below
152154
example-cloudflare:
@@ -185,7 +187,8 @@ credentials:
185187
# Optional: Discover hosts reading from network devices (pfSense, OPNsense, Mikrotik, etc.)
186188
discovery:
187189
plugins:
188-
- type: mikrotik
190+
mikrotik-router:
191+
type: mikrotik
189192
params: mikrotik:90s,192.168.88.1:8729,admin,password,true,
190193

191194
# For more info on available plugins and their configuration refer to the ipv6disc project

cmd/ipv6ddns/example.config.yaml

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,41 @@ tasks:
22
myhome:
33
ipv4:
44
args:
5-
- -s
6-
- --ipv4
7-
- ifconfig.me
5+
- -s
6+
- --ipv4
7+
- ifconfig.me
88
command: curl
99
interval: 3m
1010
lifetime: 10m
11-
mac_address:
12-
- 00:11:22:33:44:55
13-
subnets:
14-
- 2000::/3
11+
filter:
12+
mac:
13+
address: 00:11:22:33:44:55
14+
ip:
15+
prefix: 2000::/3
1516
endpoints:
1617
mycloudflaresettings:
17-
- ""
18+
- ""
1819
mylocalonlyserver:
1920
endpoints:
2021
mylocaldns:
2122
- myserver
2223
- "*.myserver"
2324
ipv4:
2425
args:
25-
- '%s\n'
26-
- 192.168.1.123
26+
- '%s\n'
27+
- 192.168.1.123
2728
command: printf
2829
interval: 60s
2930
lifetime: 4h
30-
mac_address:
31-
- 00:11:22:33:44:56
32-
subnets:
33-
- fc00::/7
31+
filter:
32+
mac:
33+
address: 00:11:22:33:44:56
34+
ip:
35+
prefix: fc00::/7
36+
type:
37+
- eui64
38+
source:
39+
- mikrotik-lan
3440
credentials:
3541
duckdns:
3642
debounce_time: 10s
@@ -122,9 +128,12 @@ discovery:
122128
listen: true
123129
active: true
124130
plugins:
125-
- params: mikrotik:90s,192.168.88.1:8729,admin,password,true,<optional>a1b2c3d4...
131+
mikrotik-lan:
126132
type: mikrotik
127-
- params: freebsd:90s,pfsense.local,admin,,/path/to/id_rsa
133+
params: mikrotik:90s,192.168.88.1:8729,admin,password,true,<optional>a1b2c3d4...
134+
pfsense-box:
128135
type: freebsd
129-
- params: linux:90s,[2001:db8::1]:2222,user,,/path/to/id_rsa
130-
type: linux
136+
params: freebsd:90s,pfsense.local,admin,,/path/to/id_rsa
137+
linux-server:
138+
type: linux
139+
params: linux:90s,[2001:db8::1]:2222,user,,/path/to/id_rsa

cmd/ipv6ddns/ipv6ddns.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/miguelangel-nubla/ipv6ddns"
1313
"github.com/miguelangel-nubla/ipv6ddns/config"
14+
"github.com/miguelangel-nubla/ipv6disc"
1415
"github.com/miguelangel-nubla/ipv6disc/pkg/plugins"
1516
_ "github.com/miguelangel-nubla/ipv6disc/pkg/plugins/mikrotik"
1617
"github.com/miguelangel-nubla/ipv6disc/pkg/terminal"
@@ -52,11 +53,18 @@ func main() {
5253
rediscover := lifetime / 3
5354
worker := ipv6ddns.NewWorker(sugar, rediscover, lifetime, config)
5455

55-
for _, pCfg := range config.Discovery.Plugins {
56+
for name, pCfg := range config.Discovery.Plugins {
5657
p, err := plugins.Create(pCfg.Type, pCfg.Params, lifetime)
5758
if err != nil {
5859
sugar.Fatalf("can't create plugin %s: %s", pCfg.Type, err)
5960
}
61+
62+
// Wrap the plugin to override the name with the instance name from config
63+
p = &PluginInstance{
64+
Plugin: p,
65+
name: name,
66+
}
67+
6068
worker.RegisterPlugin(p)
6169
}
6270

@@ -139,3 +147,12 @@ var (
139147
func PrintVersion() string {
140148
return fmt.Sprintf("ipv6ddns %s, commit %s, built at %s\n", version, commit, date)
141149
}
150+
151+
type PluginInstance struct {
152+
ipv6disc.Plugin
153+
name string
154+
}
155+
156+
func (p *PluginInstance) Name() string {
157+
return p.name
158+
}

config/config.go

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ type Config struct {
2525
}
2626

2727
type Discovery struct {
28-
Listen bool `json:"listen"`
29-
Active bool `json:"active"`
30-
Plugins []PluginConfig `json:"plugins"`
28+
Listen bool `json:"listen"`
29+
Active bool `json:"active"`
30+
Plugins map[string]PluginConfig `json:"plugins"`
3131
}
3232

3333
type PluginConfig struct {
@@ -57,16 +57,42 @@ func (c *Config) PrettyPrint(prefix string, hideSensible bool) string {
5757
result.WriteString(task.IPv4.PrettyPrint(prefix + " "))
5858
}
5959

60-
macAddresses := make([]string, len(task.MACAddresses))
61-
for i, mac := range task.MACAddresses {
62-
macAddresses[i] = mac.String()
63-
}
64-
result.WriteString(prefix + " MAC Addresses: " + strings.Join(macAddresses, ", ") + "\n")
65-
subnets := make([]string, len(task.Subnets))
66-
for i, subnet := range task.Subnets {
67-
subnets[i] = subnet.String()
60+
for _, filter := range task.Filters {
61+
result.WriteString(prefix + " - Filter Set:\n")
62+
if filter.MAC.Address != "" || len(filter.MAC.Mask) > 0 || len(filter.MAC.Type) > 0 {
63+
result.WriteString(prefix + " MAC:\n")
64+
if filter.MAC.Address != "" {
65+
result.WriteString(prefix + " Address: " + filter.MAC.Address + "\n")
66+
}
67+
if len(filter.MAC.Mask) > 0 {
68+
result.WriteString(prefix + " Mask: " + strings.Join(filter.MAC.Mask, ", ") + "\n")
69+
}
70+
if len(filter.MAC.Type) > 0 {
71+
result.WriteString(prefix + " Type: " + strings.Join(filter.MAC.Type, ", ") + "\n")
72+
}
73+
}
74+
75+
if filter.IP.Prefix.IsValid() || filter.IP.Suffix != "" || len(filter.IP.Mask) > 0 || len(filter.IP.Type) > 0 {
76+
result.WriteString(prefix + " IP:\n")
77+
if filter.IP.Prefix.IsValid() {
78+
result.WriteString(prefix + " Prefix: " + filter.IP.Prefix.String() + "\n")
79+
}
80+
if filter.IP.Suffix != "" {
81+
result.WriteString(prefix + " Suffix: " + filter.IP.Suffix + "\n")
82+
}
83+
if len(filter.IP.Mask) > 0 {
84+
result.WriteString(prefix + " Mask: " + strings.Join(filter.IP.Mask, ", ") + "\n")
85+
}
86+
if len(filter.IP.Type) > 0 {
87+
result.WriteString(prefix + " Type: " + strings.Join(filter.IP.Type, ", ") + "\n")
88+
}
89+
}
90+
91+
if len(filter.Source) > 0 {
92+
result.WriteString(prefix + " Source: " + strings.Join(filter.Source, ", ") + "\n")
93+
}
6894
}
69-
result.WriteString(prefix + " Subnets: " + strings.Join(subnets, ", ") + "\n")
95+
7096
result.WriteString(prefix + " Hostnames:\n")
7197

7298
// Sort endpoint keys
@@ -126,12 +152,22 @@ func (c *Config) PrettyPrint(prefix string, hideSensible bool) string {
126152
result.WriteString(prefix + " Active: " + fmt.Sprintf("%t", c.Discovery.Active) + "\n")
127153
if len(c.Discovery.Plugins) > 0 {
128154
result.WriteString(prefix + " Plugins:\n")
129-
for _, plugin := range c.Discovery.Plugins {
130-
result.WriteString(prefix + " Type: " + plugin.Type + "\n")
155+
156+
// Sort plugin keys
157+
pluginKeys := make([]string, 0, len(c.Discovery.Plugins))
158+
for k := range c.Discovery.Plugins {
159+
pluginKeys = append(pluginKeys, k)
160+
}
161+
sort.Strings(pluginKeys)
162+
163+
for _, name := range pluginKeys {
164+
plugin := c.Discovery.Plugins[name]
165+
result.WriteString(prefix + " " + name + ":\n")
166+
result.WriteString(prefix + " Type: " + plugin.Type + "\n")
131167
if !hideSensible {
132-
result.WriteString(prefix + " Params: " + plugin.Params + "\n")
168+
result.WriteString(prefix + " Params: " + plugin.Params + "\n")
133169
} else {
134-
result.WriteString(prefix + " Params: <sensible data hidden>\n")
170+
result.WriteString(prefix + " Params: <sensible data hidden>\n")
135171
}
136172
}
137173
}

config/config_test.go

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ func TestNewConfig(t *testing.T) {
2020
jsonContent := `{
2121
"tasks": {
2222
"my_task": {
23-
"subnets": ["2001:db8::/64"],
24-
"mac_address": ["00:11:22:33:44:55"],
23+
"filter": [
24+
{
25+
"ip": {
26+
"prefix": "2001:db8::/64"
27+
},
28+
"mac": {
29+
"address": "00:11:22:33:44:55"
30+
}
31+
}
32+
],
2533
"endpoints": {
2634
"my_credential": [
2735
"sub.domain.com"
@@ -43,10 +51,11 @@ func TestNewConfig(t *testing.T) {
4351
yamlContent := `
4452
tasks:
4553
my_task:
46-
subnets:
47-
- "2001:db8::/64"
48-
mac_address:
49-
- "00:11:22:33:44:55"
54+
filter:
55+
- ip:
56+
prefix: "2001:db8::/64"
57+
mac:
58+
address: "00:11:22:33:44:55"
5059
endpoints:
5160
my_credential:
5261
- sub.domain.com
@@ -95,4 +104,72 @@ credentials:
95104
t.Error("Expected 'my_task' to exist")
96105
}
97106
})
107+
108+
t.Run("Load Config Plugins Map", func(t *testing.T) {
109+
yamlContent := `
110+
tasks: {}
111+
credentials: {}
112+
discovery:
113+
plugins:
114+
mikrotik-lan:
115+
type: mikrotik
116+
params: param1
117+
`
118+
path := filepath.Join(tempDir, "config_plugins.yaml")
119+
_ = os.WriteFile(path, []byte(yamlContent), 0644)
120+
121+
cfg, err := NewConfig(path)
122+
if err != nil {
123+
t.Fatalf("NewConfig failed: %v", err)
124+
}
125+
126+
if len(cfg.Discovery.Plugins) != 1 {
127+
t.Errorf("Expected 1 plugin, got %d", len(cfg.Discovery.Plugins))
128+
}
129+
if _, ok := cfg.Discovery.Plugins["mikrotik-lan"]; !ok {
130+
t.Error("Expected plugin 'mikrotik-lan'")
131+
}
132+
})
133+
134+
t.Run("Load Config Nested Filters", func(t *testing.T) {
135+
yamlContent := `
136+
tasks:
137+
new_task:
138+
filter:
139+
- mac:
140+
address: "00:11:22:33:44:66"
141+
ip:
142+
type: ["global", "eui64"]
143+
source: ["mikrotik-lan"]
144+
endpoints:
145+
creds: ["host"]
146+
credentials:
147+
creds:
148+
provider: test
149+
settings: {}
150+
`
151+
path := filepath.Join(tempDir, "config_filters.yaml")
152+
_ = os.WriteFile(path, []byte(yamlContent), 0644)
153+
154+
cfg, err := NewConfig(path)
155+
if err != nil {
156+
t.Fatalf("NewConfig failed: %v", err)
157+
}
158+
159+
// Check filters
160+
task := cfg.Tasks["new_task"]
161+
if len(task.Filters) == 0 {
162+
t.Fatal("No filters found")
163+
}
164+
f := task.Filters[0]
165+
if f.MAC.Address != "00:11:22:33:44:66" {
166+
t.Error("Filter MAC mismatch")
167+
}
168+
if len(f.IP.Type) != 2 {
169+
t.Error("Filter IPType mismatch")
170+
}
171+
if len(f.Source) != 1 || f.Source[0] != "mikrotik-lan" {
172+
t.Error("Filter Source mismatch")
173+
}
174+
})
98175
}

0 commit comments

Comments
 (0)