Initial commit
This commit is contained in:
commit
36baaf0caf
10 changed files with 961 additions and 0 deletions
40
config.go
Normal file
40
config.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Network struct {
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
Logging struct {
|
||||||
|
URL string
|
||||||
|
System string
|
||||||
|
Instance string
|
||||||
|
LogDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
Device struct {
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Timeout int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfig() (config Config, err error) {
|
||||||
|
var configData []byte
|
||||||
|
configData, err = os.ReadFile(flagConfig)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(configData, &config)
|
||||||
|
return
|
||||||
|
}
|
||||||
109
dns.go
Normal file
109
dns.go
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSRecord struct {
|
||||||
|
ID string `json:".id"`
|
||||||
|
Address string
|
||||||
|
Disabled string
|
||||||
|
Dynamic string
|
||||||
|
Name string
|
||||||
|
TTL string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordsTree struct {
|
||||||
|
Record DNSRecord
|
||||||
|
Children []RecordsTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func SortDNSRecord(a, b DNSRecord) int {
|
||||||
|
aComponents := strings.Split(a.Name, ".")
|
||||||
|
bComponents := strings.Split(b.Name, ".")
|
||||||
|
|
||||||
|
slices.Reverse(aComponents)
|
||||||
|
slices.Reverse(bComponents)
|
||||||
|
|
||||||
|
for i, aComp := range aComponents {
|
||||||
|
if i >= len(bComponents) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
bComp := bComponents[i]
|
||||||
|
if aComp > bComp {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if aComp < bComp {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r DNSRecord) Parts() int {
|
||||||
|
return len(strings.Split(r.Name, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r DNSRecord) Part(numParts int) string {
|
||||||
|
splitName := strings.Split(r.Name, ".")
|
||||||
|
slices.Reverse(splitName)
|
||||||
|
|
||||||
|
parts := []string{}
|
||||||
|
for i := range numParts {
|
||||||
|
if i >= len(splitName) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts = append(parts, splitName[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Reverse(parts)
|
||||||
|
return strings.Join(parts, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r DNSRecord) NameReversed() string {
|
||||||
|
parts := strings.Split(r.Name, ".")
|
||||||
|
slices.Reverse(parts)
|
||||||
|
return strings.Join(parts, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *RecordsTree) BuildTree(records []DNSRecord, startAt, partsLevel int) (tree RecordsTree) {
|
||||||
|
tree = RecordsTree{}
|
||||||
|
|
||||||
|
for i := startAt; i < len(records); i++ {
|
||||||
|
// current: bar.foo.n44.se
|
||||||
|
current := records[i]
|
||||||
|
currentPart := current.Part(partsLevel)
|
||||||
|
|
||||||
|
for j := i; j < len(records); j++ {
|
||||||
|
// next: baz.bar.foo.n44.se
|
||||||
|
next := records[j]
|
||||||
|
nextPart := next.Part(partsLevel)
|
||||||
|
fmt.Printf("%04d %04d: %s ?= %s\n", i, j, currentPart, nextPart)
|
||||||
|
|
||||||
|
if currentPart == nextPart {
|
||||||
|
fmt.Printf("FOUND SOMETHING!\n")
|
||||||
|
nextTree := RecordsTree{
|
||||||
|
Record: next,
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.Children = append(tree.Children, nextTree)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doesn't match anymore, return to other loop after all that are processed
|
||||||
|
if currentPart != nextPart {
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
module routeros_dns
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.gibonuddevalla.se/go/html_template v1.0.0
|
||||||
|
git.gibonuddevalla.se/go/vlog v1.0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require git.gibonuddevalla.se/go/wrappederror v0.3.5 // indirect
|
||||||
6
go.sum
Normal file
6
go.sum
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
git.gibonuddevalla.se/go/html_template v1.0.0 h1:0YoqKiWMxYUsYZomqFlXLR/h0UNBicqw+VOsPyotcD4=
|
||||||
|
git.gibonuddevalla.se/go/html_template v1.0.0/go.mod h1:DGqGMulbZyGoRqWXInXISb6GQLswuUrTaX1WhH7jx/w=
|
||||||
|
git.gibonuddevalla.se/go/vlog v1.0.0 h1:6iu7Wy3V3vTSg1usLSZWX9ipA+SY3FV4pi7HrHqagyc=
|
||||||
|
git.gibonuddevalla.se/go/vlog v1.0.0/go.mod h1:rjS9ZINhZF+Bhrb+fdD4aEKOnLxFYU3PJFMx7nOi4c0=
|
||||||
|
git.gibonuddevalla.se/go/wrappederror v0.3.5 h1:/EzrdXETlZfNpS6TcK1Ix6BaV+Fl7qcGoxUM0GkrIN8=
|
||||||
|
git.gibonuddevalla.se/go/wrappederror v0.3.5/go.mod h1:j4w320Hk1wvhOPjUaK4GgLvmtnjUUM5yVu6JFO1OCSc=
|
||||||
82
main.go
Normal file
82
main.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"git.gibonuddevalla.se/go/vlog"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"flag"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const VERSION = "v1"
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagVersion bool
|
||||||
|
flagDev bool
|
||||||
|
flagDebug bool
|
||||||
|
flagConfig string
|
||||||
|
|
||||||
|
config Config
|
||||||
|
initLogger *slog.Logger
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
|
device RouterosDevice
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {// {{{
|
||||||
|
initLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
|
||||||
|
|
||||||
|
confDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
initLogger.Error("application", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defConfFilename := path.Join(confDir, "routeros_dns.json")
|
||||||
|
|
||||||
|
flag.BoolVar(&flagVersion, "version", false, "Print version and exit")
|
||||||
|
flag.BoolVar(&flagDev, "dev", false, "Load each static file from disk")
|
||||||
|
flag.BoolVar(&flagDebug, "debug", false, "Debugging log level")
|
||||||
|
flag.StringVar(&flagConfig, "config", defConfFilename, "Path to filename for configuration")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
config, err = readConfig()
|
||||||
|
if err != nil {
|
||||||
|
initLogger.Error("application", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = initLogging(config)
|
||||||
|
}// }}}
|
||||||
|
func initLogging(config Config) (*slog.Logger) {// {{{
|
||||||
|
opts := slog.HandlerOptions{}
|
||||||
|
if flagDebug {
|
||||||
|
opts.Level = slog.LevelDebug
|
||||||
|
}
|
||||||
|
handler := vlog.New(
|
||||||
|
os.Stdout,
|
||||||
|
opts,
|
||||||
|
config.Logging.LogDir,
|
||||||
|
config.Logging.URL,
|
||||||
|
"routeros-dns",
|
||||||
|
config.Logging.System,
|
||||||
|
config.Logging.Instance,
|
||||||
|
)
|
||||||
|
return slog.New(handler)
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
initLogger.Info("application", "version", VERSION)
|
||||||
|
|
||||||
|
device.Host = config.Device.Address
|
||||||
|
device.Port = config.Device.Port
|
||||||
|
device.Username = config.Device.Username
|
||||||
|
device.Password = config.Device.Password
|
||||||
|
device.Timeout = config.Device.Timeout
|
||||||
|
device.Init()
|
||||||
|
|
||||||
|
registerWebserverHandlers()
|
||||||
|
startWebserver()
|
||||||
|
}
|
||||||
583
routeros_device.go
Normal file
583
routeros_device.go
Normal file
|
|
@ -0,0 +1,583 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RouterosDevice struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Timeout int
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
auth string
|
||||||
|
added time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouterosIDType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
IPv4 RouterosIDType = iota
|
||||||
|
IPv6
|
||||||
|
)
|
||||||
|
|
||||||
|
type RouterosID struct {
|
||||||
|
Type RouterosIDType
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dev *RouterosDevice) Init() { // {{{
|
||||||
|
cred := fmt.Sprintf("%s:%s", dev.Username, dev.Password)
|
||||||
|
dev.auth = base64.StdEncoding.EncodeToString([]byte(cred))
|
||||||
|
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Timeout: time.Duration(dev.Timeout) * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
dev.httpClient = &client
|
||||||
|
dev.added = time.Now()
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
// query sends a RouterOS REST API query and returns the unparsed body.
|
||||||
|
func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{
|
||||||
|
url := fmt.Sprintf("https://%s/rest%s", dev.Host, path)
|
||||||
|
logger.Info("URL", "method", method, "url", url)
|
||||||
|
|
||||||
|
var request *http.Request
|
||||||
|
request, err = http.NewRequest(method, url, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Header.Add("Authorization", "Basic "+dev.auth)
|
||||||
|
|
||||||
|
var response *http.Response
|
||||||
|
response, err = dev.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
if response == nil {
|
||||||
|
logger.Debug("REST call", "op", "request", "method", method, "path", path, "body", string(reqBody), "error", err)
|
||||||
|
} else {
|
||||||
|
logger.Debug("REST call", "op", "request", "method", method, "path", path, "body", string(reqBody), "code", response.StatusCode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
logger.Debug("REST call", "op", "request", "method", method, "path", path, "body", string(reqBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ = io.ReadAll(response.Body)
|
||||||
|
|
||||||
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||||
|
err = fmt.Errorf("%s", body)
|
||||||
|
logger.Debug("REST call", "op", "response", "method", method, "path", path, "code", response.StatusCode, "body", string(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("REST call", "op", "response", "method", method, "path", path, "code", response.StatusCode, "body", string(body))
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) GetIdentity() (identity string, err error) { // {{{
|
||||||
|
var body []byte
|
||||||
|
body, err = dev.query("GET", "/system/identity", []byte{})
|
||||||
|
|
||||||
|
name := struct{ Name string }{}
|
||||||
|
err = json.Unmarshal(body, &name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identity = name.Name
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (dev *RouterosDevice) StaticDNSEntries() (entries []DNSRecord, err error) {
|
||||||
|
entries = []DNSRecord{}
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
body, err = dev.query("GET", "/ip/dns/static", []byte{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &entries)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router
|
||||||
|
func (dev *RouterosDevice) FillPeerDetails(peer *Peer) (err error) { // {{{
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// Retrieve the wireguard peer to get the IP address
|
||||||
|
body, err = dev.query("GET", "/interface/wireguard/peers?public-key="+strings.TrimSpace(peer.PublicKey), []byte{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devPeer := []struct {
|
||||||
|
ID string `json:".id"`
|
||||||
|
AllowedAddress string `json:"allowed-address"`
|
||||||
|
Comment string
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &devPeer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devPeer) != 1 {
|
||||||
|
err = fmt.Errorf("Invalid number of peers returned (%d peers)", len(devPeer))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.routerosID = devPeer[0].ID
|
||||||
|
peer.AllowedIPs = strings.Split(devPeer[0].AllowedAddress, ",")
|
||||||
|
peer.Comment = devPeer[0].Comment
|
||||||
|
|
||||||
|
var row *sqlx.Row
|
||||||
|
row = service.Db.Conn.QueryRowx(`
|
||||||
|
SELECT
|
||||||
|
dev.config_template,
|
||||||
|
p.id, p.user_id, p.routeros_device_id, p.public_key, p.description,
|
||||||
|
_webservice.pgp_sym_decrypt(
|
||||||
|
DECODE(p.private_key, 'base64'),
|
||||||
|
$3,
|
||||||
|
'compress-algo=1, cipher-algo=aes256'
|
||||||
|
) AS private_key
|
||||||
|
|
||||||
|
FROM public.peer p
|
||||||
|
INNER JOIN routeros_device dev ON p.routeros_device_id = dev.id
|
||||||
|
WHERE
|
||||||
|
p.public_key = $1 OR p.id = $2
|
||||||
|
`,
|
||||||
|
peer.PublicKey,
|
||||||
|
peer.ID,
|
||||||
|
config.Application.Secret,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbPeer Peer
|
||||||
|
err = row.StructScan(&dbPeer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.ConfigTemplate = dbPeer.ConfigTemplate
|
||||||
|
peer.PrivateKey = dbPeer.PrivateKey
|
||||||
|
peer.PublicKey = dbPeer.PublicKey
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) GetPeerStatus(peer *Peer, fillAllowedIPs bool) (allowedIPs []string, listID []RouterosID, exist bool, timeLeft int, err error) { // {{{
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// Retrieve the wireguard peer to get the IP address
|
||||||
|
body, err = dev.query("GET", "/interface/wireguard/peers?public-key="+strings.TrimSpace(peer.PublicKey), []byte{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devPeer := []struct {
|
||||||
|
ID string `json:".id"`
|
||||||
|
AllowedAddress string `json:"allowed-address"`
|
||||||
|
Comment string
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &devPeer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devPeer) != 1 {
|
||||||
|
err = fmt.Errorf("Invalid number of peers returned (%d peers)", len(devPeer))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exist = true
|
||||||
|
peer.routerosID = devPeer[0].ID
|
||||||
|
|
||||||
|
// Finding if it is open or not
|
||||||
|
prefix := regexp.MustCompile("(?i)([0-9.]+/32|[0-9a-f:]/128)$")
|
||||||
|
rxpTime := regexp.MustCompile("(?:([0-9]+)d)?(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s)?")
|
||||||
|
allowedAddresses := strings.Split(devPeer[0].AllowedAddress, ",")
|
||||||
|
for _, ip := range allowedAddresses {
|
||||||
|
if !prefix.Match([]byte(ip)) {
|
||||||
|
logger.Debug("peer", "public-key", peer.PublicKey, "allowed-address", ip, "error", "Invalid address, must end in /30 or /128")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allowedIPs = append(allowedIPs, ip)
|
||||||
|
|
||||||
|
ipParts := strings.Split(ip, "/")
|
||||||
|
listIDEntry := RouterosID{}
|
||||||
|
if len(ip) > 4 && ip[len(ip)-4:] == "/128" {
|
||||||
|
listIDEntry.Type = IPv6
|
||||||
|
body, err = dev.query("GET", "/ipv6/firewall/address-list?list=Wireguard-Authenticated&address="+ip, []byte{})
|
||||||
|
} else if len(ip) > 3 && ip[len(ip)-3:] == "/32" {
|
||||||
|
listIDEntry.Type = IPv4
|
||||||
|
body, err = dev.query("GET", "/ip/firewall/address-list?list=Wireguard-Authenticated&address="+ipParts[0], []byte{})
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Invalid IP (%s)", ip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listEntry := []struct {
|
||||||
|
ID string `json:".id"`
|
||||||
|
Disabled string
|
||||||
|
Timeout string
|
||||||
|
}{}
|
||||||
|
err = json.Unmarshal(body, &listEntry)
|
||||||
|
if err != nil || len(listEntry) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
listIDEntry.ID = listEntry[0].ID
|
||||||
|
listID = append(listID, listIDEntry)
|
||||||
|
|
||||||
|
// Overwrite earlier timeout, as these should be the same value anyway.
|
||||||
|
timeLeft = 0
|
||||||
|
time := rxpTime.FindStringSubmatch(listEntry[0].Timeout)
|
||||||
|
if i, err := strconv.Atoi(time[1]); err == nil {
|
||||||
|
timeLeft = i * 86400
|
||||||
|
}
|
||||||
|
if i, err := strconv.Atoi(time[2]); err == nil {
|
||||||
|
timeLeft += i * 3600
|
||||||
|
}
|
||||||
|
if i, err := strconv.Atoi(time[3]); err == nil {
|
||||||
|
timeLeft += i * 60
|
||||||
|
}
|
||||||
|
if i, err := strconv.Atoi(time[4]); err == nil {
|
||||||
|
timeLeft += i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fillAllowedIPs {
|
||||||
|
peer.AllowedIPs = allowedIPs
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) IsCachedIPInAddressList(peer *Peer) (inList bool, timeLeft int, err error) { // {{{
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// Finding if it is open or not
|
||||||
|
prefix := regexp.MustCompile("(?i)([0-9.]+/32|[0-9a-f:]/128)$")
|
||||||
|
rxpTime := regexp.MustCompile("(?:([0-9]+)d)?(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s)?")
|
||||||
|
allowedAddresses := peer.CachedAddresses
|
||||||
|
for _, ip := range allowedAddresses {
|
||||||
|
// This indicates existence of key and address in GetPeerStatus,
|
||||||
|
// but can't here since it is too expensive to check device wireguard peers.
|
||||||
|
inList = true
|
||||||
|
|
||||||
|
if !prefix.Match([]byte(ip)) {
|
||||||
|
logger.Debug("peer", "public-key", peer.PublicKey, "allowed-address", ip, "error", "Invalid address, must end in /30 or /128")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipParts := strings.Split(ip, "/")
|
||||||
|
listIDEntry := RouterosID{}
|
||||||
|
if len(ip) > 4 && ip[len(ip)-4:] == "/128" {
|
||||||
|
listIDEntry.Type = IPv6
|
||||||
|
body, err = dev.query("GET", "/ipv6/firewall/address-list?list=Wireguard-Authenticated&address="+ip, []byte{})
|
||||||
|
} else if len(ip) > 3 && ip[len(ip)-3:] == "/32" {
|
||||||
|
listIDEntry.Type = IPv4
|
||||||
|
body, err = dev.query("GET", "/ip/firewall/address-list?list=Wireguard-Authenticated&address="+ipParts[0], []byte{})
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Invalid IP (%s)", ip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listEntry := []struct {
|
||||||
|
ID string `json:".id"`
|
||||||
|
Disabled string
|
||||||
|
Timeout string
|
||||||
|
}{}
|
||||||
|
err = json.Unmarshal(body, &listEntry)
|
||||||
|
if err != nil || len(listEntry) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
listIDEntry.ID = listEntry[0].ID
|
||||||
|
|
||||||
|
// Overwrite earlier timeout, as these should be the same value anyway.
|
||||||
|
timeLeft = 0
|
||||||
|
time := rxpTime.FindStringSubmatch(listEntry[0].Timeout)
|
||||||
|
if i, err := strconv.Atoi(time[1]); err == nil {
|
||||||
|
timeLeft = i * 86400
|
||||||
|
}
|
||||||
|
if i, err := strconv.Atoi(time[2]); err == nil {
|
||||||
|
timeLeft += i * 3600
|
||||||
|
}
|
||||||
|
if i, err := strconv.Atoi(time[3]); err == nil {
|
||||||
|
timeLeft += i * 60
|
||||||
|
}
|
||||||
|
if i, err := strconv.Atoi(time[4]); err == nil {
|
||||||
|
timeLeft += i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) ClosePeer(sess *session.T, peer Peer) (err error) { // {{{
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
var listID []RouterosID
|
||||||
|
for _, ip := range peer.CachedAddresses {
|
||||||
|
ipParts := strings.Split(ip, "/")
|
||||||
|
listIDEntry := RouterosID{}
|
||||||
|
if len(ip) > 4 && ip[len(ip)-4:] == "/128" {
|
||||||
|
listIDEntry.Type = IPv6
|
||||||
|
body, err = dev.query("GET", "/ipv6/firewall/address-list?list=Wireguard-Authenticated&address="+ip, []byte{})
|
||||||
|
} else if len(ip) > 3 && ip[len(ip)-3:] == "/32" {
|
||||||
|
listIDEntry.Type = IPv4
|
||||||
|
body, err = dev.query("GET", "/ip/firewall/address-list?list=Wireguard-Authenticated&address="+ipParts[0], []byte{})
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Invalid IP (%s)", ip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listEntry := []struct {
|
||||||
|
ID string `json:".id"`
|
||||||
|
Disabled string
|
||||||
|
Timeout string
|
||||||
|
}{}
|
||||||
|
err = json.Unmarshal(body, &listEntry)
|
||||||
|
if err != nil || len(listEntry) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
listIDEntry.ID = listEntry[0].ID
|
||||||
|
listID = append(listID, listIDEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, listID := range listID {
|
||||||
|
switch listID.Type {
|
||||||
|
case IPv4:
|
||||||
|
body, err = dev.query("DELETE", "/ip/firewall/address-list/"+listID.ID, []byte{})
|
||||||
|
case IPv6:
|
||||||
|
body, err = dev.query("DELETE", "/ipv6/firewall/address-list/"+listID.ID, []byte{})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(body) != "" {
|
||||||
|
devError := struct {
|
||||||
|
Error int
|
||||||
|
Message string
|
||||||
|
Detail string
|
||||||
|
}{}
|
||||||
|
err = json.Unmarshal(body, &devError)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if devError.Message != "" {
|
||||||
|
err = fmt.Errorf("%s", devError.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) OpenPeer(sess *session.T, peer Peer) (err error) { // {{{
|
||||||
|
allowedIPs := peer.CachedAddresses
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
for _, ip := range allowedIPs {
|
||||||
|
req, _ := json.Marshal(struct {
|
||||||
|
List string `json:"list"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Timeout string `json:"timeout"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}{
|
||||||
|
"Wireguard-Authenticated",
|
||||||
|
ip,
|
||||||
|
"08:00:00",
|
||||||
|
sess.Username + ", " + peer.Description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(ip) > 4 && ip[len(ip)-4:] == "/128" {
|
||||||
|
body, err = dev.query("PUT", "/ipv6/firewall/address-list", req)
|
||||||
|
} else if len(ip) > 3 && ip[len(ip)-3:] == "/32" {
|
||||||
|
body, err = dev.query("PUT", "/ip/firewall/address-list", req)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Invalid IP (%s)", ip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
devError := struct {
|
||||||
|
Error int
|
||||||
|
Message string
|
||||||
|
Detail string
|
||||||
|
}{}
|
||||||
|
err = json.Unmarshal(body, &devError)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if devError.Message != "" {
|
||||||
|
err = fmt.Errorf("%s", devError.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) GetPeers() (err error) { // {{{
|
||||||
|
dev.Peers = []*Peer{}
|
||||||
|
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
rows, err = service.Db.Conn.Queryx(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.user_id,
|
||||||
|
p.routeros_device_id,
|
||||||
|
p.description,
|
||||||
|
p.public_key,
|
||||||
|
_webservice.pgp_sym_decrypt(
|
||||||
|
DECODE(p.private_key, 'base64'),
|
||||||
|
$2,
|
||||||
|
'compress-algo=1, cipher-algo=aes256'
|
||||||
|
) AS private_key,
|
||||||
|
cached_addresses
|
||||||
|
FROM public.peer p
|
||||||
|
INNER JOIN _webservice.user u ON p.user_id = u.id
|
||||||
|
WHERE
|
||||||
|
routeros_device_id = $1
|
||||||
|
ORDER BY
|
||||||
|
u.name ASC,
|
||||||
|
p.description ASC
|
||||||
|
`,
|
||||||
|
dev.ID,
|
||||||
|
config.Application.Secret,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var peer Peer
|
||||||
|
if err = rows.StructScan(&peer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dev.Peers = append(dev.Peers, &peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
func (dev *RouterosDevice) UpdatePeer(peer Peer) (err error) { // {{{
|
||||||
|
allowedIPs := peer.AllowedIPs
|
||||||
|
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`UPDATE public.peer
|
||||||
|
SET
|
||||||
|
description = $2,
|
||||||
|
user_id = $3,
|
||||||
|
cached_addresses = $4
|
||||||
|
WHERE
|
||||||
|
id = $1`,
|
||||||
|
peer.ID,
|
||||||
|
peer.Description,
|
||||||
|
peer.UserID,
|
||||||
|
peer.CachedAddresses,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dev.FillPeerDetails(&peer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This isn't necessarily set
|
||||||
|
if allowedIPs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
AllowedAddress string `json:"allowed-address"`
|
||||||
|
}{
|
||||||
|
strings.Join(allowedIPs, ","),
|
||||||
|
}
|
||||||
|
reqJSON, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
_, err = dev.query("PATCH", "/interface/wireguard/peers/"+peer.routerosID, reqJSON)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) CreatePeer(peer Peer) (err error) { // {{{
|
||||||
|
req := struct {
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
PublicKey string `json:"public-key"`
|
||||||
|
AllowedAddresses string `json:"allowed-address"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}{
|
||||||
|
dev.WireguardInterface,
|
||||||
|
peer.PublicKey,
|
||||||
|
strings.Join(peer.AllowedIPs, ","),
|
||||||
|
peer.Comment,
|
||||||
|
}
|
||||||
|
reqJSON, _ := json.Marshal(req)
|
||||||
|
_, err = dev.query("PUT", "/interface/wireguard/peers", reqJSON)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) DeletePeer(peer Peer) (err error) { // {{{
|
||||||
|
if err = dev.FillPeerDetails(&peer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = dev.query("DELETE", "/interface/wireguard/peers/"+peer.routerosID, []byte{})
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (dev *RouterosDevice) AllowedIPs() (peers []*PeerIPs, err error) { // {{{
|
||||||
|
var body []byte
|
||||||
|
body, err = dev.query("GET", "/interface/wireguard/peers?interface="+dev.WireguardInterface, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &peers)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range peers {
|
||||||
|
peer.AllowedIPs = strings.Split(peer.AllowedAddress, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
*/
|
||||||
12
static/css/index.css
Normal file
12
static/css/index.css
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[onClick] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
27
views/layouts/main.gotmpl
Normal file
27
views/layouts/main.gotmpl
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
|
||||||
|
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window._VERSION = "{{ .VERSION }}"
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app" class="page-{{ .PAGE }}">
|
||||||
|
{{ block "page" . }}{{ end }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
7
views/pages/index.gotmpl
Normal file
7
views/pages/index.gotmpl
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{ define "page" }}
|
||||||
|
<h1>{{ .Data.Identity }}</h1>
|
||||||
|
|
||||||
|
{{ range .Data.DNSRecords }}
|
||||||
|
<div class="dns-record">{{ .NameReversed }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
85
webserver.go
Normal file
85
webserver.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"git.gibonuddevalla.se/go/html_template"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
htmlEngine HTMLTemplate.Engine
|
||||||
|
|
||||||
|
//go:embed static
|
||||||
|
staticFS embed.FS
|
||||||
|
|
||||||
|
//go:embed views
|
||||||
|
viewFS embed.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerWebserverHandlers() {
|
||||||
|
var err error
|
||||||
|
htmlEngine, err = HTMLTemplate.NewEngine(viewFS, staticFS, flagDev)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("webserver", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/", rootHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startWebserver() {
|
||||||
|
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
|
||||||
|
logger.Info("webserver", "listen", listen)
|
||||||
|
http.ListenAndServe(listen, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
page := HTMLTemplate.SimplePage{}
|
||||||
|
page.Layout = "main"
|
||||||
|
page.Page = "index"
|
||||||
|
|
||||||
|
var err error
|
||||||
|
data := make(map[string]any)
|
||||||
|
|
||||||
|
data["Identity"], err = device.GetIdentity()
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []DNSRecord
|
||||||
|
entries, err = device.StaticDNSEntries()
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(entries, SortDNSRecord)
|
||||||
|
data["DNSRecords"] = entries
|
||||||
|
|
||||||
|
tree := RecordsTree{}
|
||||||
|
tree = tree.BuildTree(entries, 0, 1)
|
||||||
|
|
||||||
|
j, _ := json.Marshal(tree)
|
||||||
|
os.WriteFile("/tmp/tree.json", j, 0644)
|
||||||
|
|
||||||
|
page.Data = data
|
||||||
|
|
||||||
|
err = htmlEngine.Render(page, w, r)
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlEngine.StaticResource(w, r)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue