Initial commit
This commit is contained in:
commit
36baaf0caf
10 changed files with 961 additions and 0 deletions
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
|
||||
} // }}}
|
||||
*/
|
||||
Loading…
Add table
Add a link
Reference in a new issue