-
-
Notifications
You must be signed in to change notification settings - Fork 164
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Mikrotik | ||
|
||
## Configuration | ||
|
||
### Example | ||
|
||
```json | ||
{ | ||
"settings": [ | ||
{ | ||
"provider": "mikrotik", | ||
"router_ip": "192.168.0.1", | ||
"address_list": "AddressListName", | ||
"username": "user", | ||
"password": "secret", | ||
"ip_version": "ipv4" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
### Parameters | ||
|
||
- `"router_ip"` is the IP address of your router | ||
- `"address_list"` is the name of the address list | ||
- `"username"` is the username to authenticate with | ||
- `"password"` is the user's password | ||
|
||
## Domain setup | ||
|
||
- Create a user with read, write, and api access | ||
- Optionally create an entry in `/ip firewall address-list` to assign your public IP, an entry will be created for you otherwise | ||
- You can then use this address list in your hairpin NAT firewall rules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package mikrotik | ||
|
||
import ( | ||
"github.com/go-routeros/routeros" //nolint:misspell | ||
Check failure on line 4 in internal/provider/providers/mikrotik/api.go GitHub Actions / verify
Check failure on line 4 in internal/provider/providers/mikrotik/api.go GitHub Actions / verify
|
||
) | ||
|
||
type addressListItem struct { | ||
id string | ||
list string | ||
address string | ||
} | ||
|
||
func getAddressListItems(client *routeros.Client, | ||
Check failure on line 13 in internal/provider/providers/mikrotik/api.go GitHub Actions / verify
|
||
addressList string) (items []addressListItem, err error) { | ||
reply, err := client.Run("/ip/firewall/address-list/print", | ||
"?disabled=false", "?list="+addressList) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
items = make([]addressListItem, 0, len(reply.Re)) | ||
for _, re := range reply.Re { | ||
item := addressListItem{ | ||
id: re.Map[".id"], | ||
list: re.Map["list"], | ||
address: re.Map["address"], | ||
} | ||
if item.id == "" || item.address == "" { | ||
continue | ||
} | ||
items = append(items, item) | ||
} | ||
return items, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package mikrotik | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/netip" | ||
"regexp" | ||
|
||
"github.com/go-routeros/routeros" //nolint:misspell | ||
Check failure on line 11 in internal/provider/providers/mikrotik/provider.go GitHub Actions / verify
Check failure on line 11 in internal/provider/providers/mikrotik/provider.go GitHub Actions / verify
|
||
"github.com/qdm12/ddns-updater/internal/models" | ||
"github.com/qdm12/ddns-updater/internal/provider/constants" | ||
"github.com/qdm12/ddns-updater/internal/provider/errors" | ||
"github.com/qdm12/ddns-updater/internal/provider/utils" | ||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion" | ||
) | ||
|
||
type Provider struct { | ||
ipVersion ipversion.IPVersion | ||
ipv6Suffix netip.Prefix | ||
routerIP netip.Addr | ||
username string | ||
password string | ||
addressList string | ||
} | ||
|
||
type settings struct { | ||
RouterIP netip.Addr `json:"router_ip"` | ||
Username string `json:"username"` | ||
Password string `json:"password"` | ||
AddressList string `json:"address_list"` | ||
} | ||
|
||
func New(data json.RawMessage, ipVersion ipversion.IPVersion, | ||
ipv6Suffix netip.Prefix) (p *Provider, err error) { | ||
var providerSpecificSettings settings | ||
err = json.Unmarshal(data, &providerSpecificSettings) | ||
if err != nil { | ||
return nil, fmt.Errorf("json decoding provider specific settings: %w", err) | ||
} | ||
err = validateSettings(providerSpecificSettings) | ||
if err != nil { | ||
return nil, fmt.Errorf("validating settings: %w", err) | ||
} | ||
|
||
return &Provider{ | ||
ipVersion: ipVersion, | ||
ipv6Suffix: ipv6Suffix, | ||
routerIP: providerSpecificSettings.RouterIP, | ||
username: providerSpecificSettings.Username, | ||
password: providerSpecificSettings.Password, | ||
addressList: providerSpecificSettings.AddressList, | ||
}, nil | ||
} | ||
|
||
var addressListRegex = regexp.MustCompile(`^[a-zA-Z]{2,}$`) | ||
|
||
func validateSettings(settings settings) error { | ||
switch { | ||
case !addressListRegex.MatchString(settings.AddressList): | ||
return fmt.Errorf("%w: host %q does not match regex %q", | ||
errors.ErrKeyNotValid, settings.AddressList, addressListRegex) | ||
case !settings.RouterIP.IsValid(): | ||
return fmt.Errorf("%w: router_ip cannot be empty", errors.ErrKeyNotSet) | ||
} | ||
return nil | ||
} | ||
|
||
func (p *Provider) String() string { | ||
return utils.ToString(p.Domain(), p.addressList, constants.Mikrotik, p.ipVersion) | ||
} | ||
|
||
func (p *Provider) Domain() string { | ||
return "N / A" | ||
} | ||
|
||
func (p *Provider) Host() string { | ||
return "N / A" | ||
} | ||
|
||
func (p *Provider) IPVersion() ipversion.IPVersion { | ||
return p.ipVersion | ||
} | ||
|
||
func (p *Provider) IPv6Suffix() netip.Prefix { | ||
return p.ipv6Suffix | ||
} | ||
|
||
func (p *Provider) Proxied() bool { | ||
return false | ||
} | ||
|
||
func (p *Provider) BuildDomainName() string { | ||
return "" | ||
} | ||
|
||
func (p *Provider) HTML() models.HTMLRow { | ||
return models.HTMLRow{ | ||
Domain: p.Domain(), | ||
Host: p.addressList, | ||
Provider: fmt.Sprintf("<a href=\"http://%s\">Mikrotik</a>", p.routerIP), | ||
IPVersion: p.ipVersion.String(), | ||
} | ||
} | ||
|
||
func (p *Provider) Update(_ context.Context, _ *http.Client, ip netip.Addr) ( | ||
newIP netip.Addr, err error) { | ||
client, err := routeros.Dial(p.routerIP.String()+":8728", p.username, p.password) | ||
Check failure on line 109 in internal/provider/providers/mikrotik/provider.go GitHub Actions / verify
|
||
if err != nil { | ||
return netip.Addr{}, fmt.Errorf("authenticating with router: %w", err) | ||
} | ||
defer client.Close() | ||
|
||
addressListItems, err := getAddressListItems(client, p.addressList) | ||
if err != nil { | ||
return netip.Addr{}, fmt.Errorf("getting address list items: %w", err) | ||
} | ||
|
||
if len(addressListItems) == 0 { | ||
_, err = client.Run("/ip/firewall/address-list/add", | ||
"=list="+p.addressList, "=address="+ip.String()) | ||
if err != nil { | ||
return netip.Addr{}, fmt.Errorf("adding address list %q: %w", | ||
p.addressList, err) | ||
} | ||
return ip, nil | ||
} | ||
|
||
for _, addressListItem := range addressListItems { | ||
if addressListItem.address == ip.String() { | ||
continue // already up to date | ||
} | ||
_, err = client.Run("/ip/firewall/address-list/set", | ||
"=.id="+addressListItem.id, "=address="+ip.String()) | ||
if err != nil { | ||
return netip.Addr{}, fmt.Errorf("setting address in address list id %q: %w", | ||
addressListItem.id, err) | ||
} | ||
} | ||
|
||
return ip, nil | ||
} |