mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-01 05:47:48 +02:00
feat: Add option to define which DNS server a DNS monitor queries (#2389)
This commit is contained in:
@@ -28,7 +28,7 @@ func networkMonitorProbe(client *http.Client) monitorProbe {
|
|||||||
case "http":
|
case "http":
|
||||||
return monitorHTTP(ctx, client, config.Target)
|
return monitorHTTP(ctx, client, config.Target)
|
||||||
case "dns":
|
case "dns":
|
||||||
return monitorDNS(ctx, config.Target)
|
return monitorDNS(ctx, config.Target, config.Server)
|
||||||
default:
|
default:
|
||||||
return -1, fmt.Errorf("unknown monitor protocol: %s", config.Protocol)
|
return -1, fmt.Errorf("unknown monitor protocol: %s", config.Protocol)
|
||||||
}
|
}
|
||||||
@@ -73,19 +73,43 @@ func monitorTCP(ctx context.Context, target string, port uint16) (int64, error)
|
|||||||
return -1, err
|
return -1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitorDNS measures DNS resolution response time in microseconds. Returns -1 and an error on failure.
|
// monitorDNS measures DNS resolution response time in microseconds. If server is
|
||||||
func monitorDNS(ctx context.Context, target string) (int64, error) {
|
// non-empty, the lookup is sent to that DNS server (host or host:port, default
|
||||||
|
// port 53) instead of the system resolver. Returns -1 and an error on failure.
|
||||||
|
func monitorDNS(ctx context.Context, target, server string) (int64, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
resolver := net.DefaultResolver
|
||||||
|
if server != "" {
|
||||||
|
resolver = dnsResolverForServer(server)
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
ips, err := net.DefaultResolver.LookupHost(ctx, target)
|
ips, err := resolver.LookupHost(ctx, target)
|
||||||
if err != nil || len(ips) == 0 {
|
if err != nil || len(ips) == 0 {
|
||||||
return -1, err
|
return -1, err
|
||||||
}
|
}
|
||||||
return time.Since(start).Microseconds(), nil
|
return time.Since(start).Microseconds(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dnsResolverForServer builds a resolver that sends lookups to the given DNS
|
||||||
|
// server address instead of the system resolver. server may be a bare host or
|
||||||
|
// host:port; when no port is given, the standard DNS port 53 is used.
|
||||||
|
func dnsResolverForServer(server string) *net.Resolver {
|
||||||
|
address := server
|
||||||
|
if _, _, err := net.SplitHostPort(server); err != nil {
|
||||||
|
address = net.JoinHostPort(server, "53")
|
||||||
|
}
|
||||||
|
return &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||||
|
var dialer net.Dialer
|
||||||
|
return dialer.DialContext(ctx, network, address)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// monitorHTTP measures HTTP GET request response in microseconds. Returns -1 and an error on failure.
|
// monitorHTTP measures HTTP GET request response in microseconds. Returns -1 and an error on failure.
|
||||||
func monitorHTTP(ctx context.Context, client *http.Client, url string) (int64, error) {
|
func monitorHTTP(ctx context.Context, client *http.Client, url string) (int64, error) {
|
||||||
if client == nil {
|
if client == nil {
|
||||||
|
|||||||
@@ -376,15 +376,79 @@ func tcpMonitorTestResolver(ips []string) *net.Resolver {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// udpDNSTestServer starts a UDP server on loopback that answers A queries with the
|
||||||
|
// given IPs, and returns its listen address (host:port).
|
||||||
|
func udpDNSTestServer(t *testing.T, ips []string) string {
|
||||||
|
t.Helper()
|
||||||
|
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { conn.Close() })
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 512)
|
||||||
|
for {
|
||||||
|
n, addr, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg dnsmessage.Message
|
||||||
|
if err := msg.Unpack(buf[:n]); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg.Header.Response = true
|
||||||
|
msg.Header.RecursionAvailable = true
|
||||||
|
for _, question := range msg.Questions {
|
||||||
|
if question.Type != dnsmessage.TypeA {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, ip := range ips {
|
||||||
|
msg.Answers = append(msg.Answers, dnsmessage.Resource{
|
||||||
|
Header: dnsmessage.ResourceHeader{Name: question.Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET},
|
||||||
|
Body: &dnsmessage.AResource{A: [4]byte(net.ParseIP(ip).To4())},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packet, err := msg.Pack()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, _ = conn.WriteToUDP(packet, addr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return conn.LocalAddr().String()
|
||||||
|
}
|
||||||
|
|
||||||
func TestMonitorDNS(t *testing.T) {
|
func TestMonitorDNS(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
responseUs, err := monitorDNS(context.Background(), "localhost")
|
responseUs, err := monitorDNS(context.Background(), "localhost", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.GreaterOrEqual(t, responseUs, int64(0))
|
assert.GreaterOrEqual(t, responseUs, int64(0))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("lookup failure", func(t *testing.T) {
|
t.Run("lookup failure", func(t *testing.T) {
|
||||||
responseUs, err := monitorDNS(context.Background(), "")
|
responseUs, err := monitorDNS(context.Background(), "", "")
|
||||||
|
assert.Equal(t, int64(-1), responseUs)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom server", func(t *testing.T) {
|
||||||
|
serverAddr := udpDNSTestServer(t, []string{"192.0.2.10"})
|
||||||
|
responseUs, err := monitorDNS(context.Background(), "example.test.", serverAddr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.GreaterOrEqual(t, responseUs, int64(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom server without port defaults to 53", func(t *testing.T) {
|
||||||
|
resolver := dnsResolverForServer("127.0.0.1")
|
||||||
|
conn, err := resolver.Dial(context.Background(), "udp", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
assert.Equal(t, "127.0.0.1:53", conn.RemoteAddr().String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom server unreachable", func(t *testing.T) {
|
||||||
|
responseUs, err := monitorDNS(context.Background(), "example.test.", "127.0.0.1:1")
|
||||||
assert.Equal(t, int64(-1), responseUs)
|
assert.Equal(t, int64(-1), responseUs)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
@@ -479,7 +543,7 @@ func TestMonitorResolutionCancellation(t *testing.T) {
|
|||||||
case "tcp":
|
case "tcp":
|
||||||
_, err = monitorTCP(ctx, "monitor-cancellation.invalid.", 80)
|
_, err = monitorTCP(ctx, "monitor-cancellation.invalid.", 80)
|
||||||
case "dns":
|
case "dns":
|
||||||
_, err = monitorDNS(ctx, "monitor-cancellation.invalid.")
|
_, err = monitorDNS(ctx, "monitor-cancellation.invalid.", "")
|
||||||
case "icmp":
|
case "icmp":
|
||||||
_, err = monitorICMP(ctx, "monitor-cancellation.invalid.")
|
_, err = monitorICMP(ctx, "monitor-cancellation.invalid.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ type Config struct {
|
|||||||
Protocol string `cbor:"2,keyasint"` // "icmp", "tcp", "http", or "dns"
|
Protocol string `cbor:"2,keyasint"` // "icmp", "tcp", "http", or "dns"
|
||||||
Port uint16 `cbor:"3,keyasint,omitempty"`
|
Port uint16 `cbor:"3,keyasint,omitempty"`
|
||||||
Interval uint16 `cbor:"4,keyasint"` // seconds
|
Interval uint16 `cbor:"4,keyasint"` // seconds
|
||||||
|
// Server is the DNS server to query (host or host:port, default port 53).
|
||||||
|
// Only used when Protocol is "dns"; empty means use the system resolver.
|
||||||
|
Server string `cbor:"5,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertInfo holds details of the leaf TLS certificate presented by a target.
|
// CertInfo holds details of the leaf TLS certificate presented by a target.
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ func generateMonitorID(systemId string, config monitor.Config) string {
|
|||||||
if config.Protocol == "tcp" {
|
if config.Protocol == "tcp" {
|
||||||
args = append(args, strconv.FormatUint(uint64(config.Port), 10))
|
args = append(args, strconv.FormatUint(uint64(config.Port), 10))
|
||||||
}
|
}
|
||||||
|
// only use server for DNS monitors, so the same target queried via different servers gets distinct monitors
|
||||||
|
if config.Protocol == "dns" {
|
||||||
|
args = append(args, config.Server)
|
||||||
|
}
|
||||||
return systems.MakeStableHashId(args...)
|
return systems.MakeStableHashId(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +57,15 @@ func bindNetworkMonitorsEvents(hub *Hub) {
|
|||||||
// record with the new ID and delete the old one. Otherwise, just update the existing monitor on the agent.
|
// record with the new ID and delete the old one. Otherwise, just update the existing monitor on the agent.
|
||||||
hub.OnRecordUpdateRequest("network_monitors").BindFunc(func(e *core.RecordRequestEvent) error {
|
hub.OnRecordUpdateRequest("network_monitors").BindFunc(func(e *core.RecordRequestEvent) error {
|
||||||
systemID := e.Record.GetString("system")
|
systemID := e.Record.GetString("system")
|
||||||
|
protocol := e.Record.GetString("protocol")
|
||||||
// only tcp uses port - set other protocols port to zero
|
// only tcp uses port - set other protocols port to zero
|
||||||
if e.Record.GetString("protocol") != "tcp" {
|
if protocol != "tcp" {
|
||||||
e.Record.Set("port", 0)
|
e.Record.Set("port", 0)
|
||||||
}
|
}
|
||||||
|
// only dns uses server - clear it for other protocols
|
||||||
|
if protocol != "dns" {
|
||||||
|
e.Record.Set("server", "")
|
||||||
|
}
|
||||||
ID := generateMonitorID(systemID, *monitorConfigFromRecord(e.Record))
|
ID := generateMonitorID(systemID, *monitorConfigFromRecord(e.Record))
|
||||||
if ID != e.Record.Id {
|
if ID != e.Record.Id {
|
||||||
newRecord := copyMonitorToNewRecord(e.Record, ID)
|
newRecord := copyMonitorToNewRecord(e.Record, ID)
|
||||||
@@ -103,6 +112,7 @@ func monitorConfigFromRecord(record *core.Record) *monitor.Config {
|
|||||||
Protocol: record.GetString("protocol"),
|
Protocol: record.GetString("protocol"),
|
||||||
Port: uint16(record.GetInt("port")),
|
Port: uint16(record.GetInt("port")),
|
||||||
Interval: uint16(record.GetInt("interval")),
|
Interval: uint16(record.GetInt("interval")),
|
||||||
|
Server: record.GetString("server"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +137,7 @@ func copyMonitorToNewRecord(oldRecord *core.Record, newID string) *core.Record {
|
|||||||
collection := oldRecord.Collection()
|
collection := oldRecord.Collection()
|
||||||
newRecord := core.NewRecord(collection)
|
newRecord := core.NewRecord(collection)
|
||||||
newRecord.Id = newID
|
newRecord.Id = newID
|
||||||
fields := []string{"system", "target", "protocol", "port", "interval", "enabled"}
|
fields := []string{"system", "target", "protocol", "port", "server", "interval", "enabled"}
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
newRecord.Set(field, oldRecord.Get(field))
|
newRecord.Set(field, oldRecord.Get(field))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,39 @@ func TestGenerateMonitorID(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: "84167969",
|
expected: "84167969",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "DNS monitor on example.com with server 1.1.1.1",
|
||||||
|
systemID: "sys999",
|
||||||
|
config: monitor.Config{
|
||||||
|
Protocol: "dns",
|
||||||
|
Target: "example.com",
|
||||||
|
Server: "1.1.1.1",
|
||||||
|
Interval: 30,
|
||||||
|
},
|
||||||
|
expected: "2175898b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DNS monitor on example.com with different server",
|
||||||
|
systemID: "sys999",
|
||||||
|
config: monitor.Config{
|
||||||
|
Protocol: "dns",
|
||||||
|
Target: "example.com",
|
||||||
|
Server: "8.8.8.8",
|
||||||
|
Interval: 30,
|
||||||
|
},
|
||||||
|
expected: "ebcd8b33",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DNS monitor on example.com with no server (system resolver)",
|
||||||
|
systemID: "sys999",
|
||||||
|
config: monitor.Config{
|
||||||
|
Protocol: "dns",
|
||||||
|
Target: "example.com",
|
||||||
|
Server: "",
|
||||||
|
Interval: 30,
|
||||||
|
},
|
||||||
|
expected: "19476a7",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -199,6 +232,7 @@ func TestCopyMonitorToNewRecordDropsResultFields(t *testing.T) {
|
|||||||
"target": "https://example.com",
|
"target": "https://example.com",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"port": 443,
|
"port": 443,
|
||||||
|
"server": "1.1.1.1",
|
||||||
"interval": 60,
|
"interval": 60,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"res": 1200,
|
"res": 1200,
|
||||||
@@ -216,6 +250,7 @@ func TestCopyMonitorToNewRecordDropsResultFields(t *testing.T) {
|
|||||||
assert.Equal(t, "https://example.com", newRecord.GetString("target"))
|
assert.Equal(t, "https://example.com", newRecord.GetString("target"))
|
||||||
assert.Equal(t, "http", newRecord.GetString("protocol"))
|
assert.Equal(t, "http", newRecord.GetString("protocol"))
|
||||||
assert.Equal(t, 443, newRecord.GetInt("port"))
|
assert.Equal(t, 443, newRecord.GetInt("port"))
|
||||||
|
assert.Equal(t, "1.1.1.1", newRecord.GetString("server"))
|
||||||
assert.True(t, newRecord.GetBool("enabled"))
|
assert.True(t, newRecord.GetBool("enabled"))
|
||||||
assert.Contains(t, []string{"", "null"}, newRecord.GetString("certInfo"))
|
assert.Contains(t, []string{"", "null"}, newRecord.GetString("certInfo"))
|
||||||
assert.Zero(t, newRecord.GetFloat("res"))
|
assert.Zero(t, newRecord.GetFloat("res"))
|
||||||
|
|||||||
24
internal/migrations/1790273174_network_monitor_server.go
Normal file
24
internal/migrations/1790273174_network_monitor_server.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("network_monitors")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Fields.Add(&core.TextField{Id: "nm_server", Name: "server", Max: 260})
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("network_monitors")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Fields.RemoveByName("server")
|
||||||
|
return app.Save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ type MonitorValues = {
|
|||||||
target: string
|
target: string
|
||||||
protocol: MonitorProtocol
|
protocol: MonitorProtocol
|
||||||
port: number
|
port: number
|
||||||
|
server: string
|
||||||
interval: string
|
interval: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ type NormalizedMonitorValues = Omit<MonitorValues, "system" | "interval"> & {
|
|||||||
interval: number
|
interval: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkMonitorLineSource = Pick<NetworkMonitorRecord, "target" | "protocol" | "port" | "interval">
|
type BulkMonitorLineSource = Pick<NetworkMonitorRecord, "target" | "protocol" | "port" | "interval" | "server">
|
||||||
|
|
||||||
const defaultInterval = 30
|
const defaultInterval = 30
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ const NormalizedMonitorValuesSchema = v.pipe(
|
|||||||
target: v.pipe(v.string(), v.trim(), v.nonEmpty("target is required")),
|
target: v.pipe(v.string(), v.trim(), v.nonEmpty("target is required")),
|
||||||
protocol: MonitorProtocolSchema,
|
protocol: MonitorProtocolSchema,
|
||||||
port: v.number(),
|
port: v.number(),
|
||||||
|
server: v.pipe(v.string(), v.trim()),
|
||||||
interval: MonitorIntervalSchema,
|
interval: MonitorIntervalSchema,
|
||||||
}),
|
}),
|
||||||
v.transform((input): NormalizedMonitorValues => {
|
v.transform((input): NormalizedMonitorValues => {
|
||||||
@@ -78,6 +80,8 @@ const NormalizedMonitorValuesSchema = v.pipe(
|
|||||||
target: protocol === "http" ? httpTarget : input.target,
|
target: protocol === "http" ? httpTarget : input.target,
|
||||||
protocol,
|
protocol,
|
||||||
port,
|
port,
|
||||||
|
// Only DNS monitors use a custom server; clear it for other protocols.
|
||||||
|
server: protocol === "dns" ? input.server : "",
|
||||||
interval: input.interval,
|
interval: input.interval,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -100,6 +104,7 @@ const BulkMonitorSchema = v.object({
|
|||||||
protocol: v.optional(v.pipe(v.string(), v.trim())),
|
protocol: v.optional(v.pipe(v.string(), v.trim())),
|
||||||
port: v.optional(v.pipe(v.string(), v.trim())),
|
port: v.optional(v.pipe(v.string(), v.trim())),
|
||||||
interval: v.optional(v.pipe(v.string(), v.trim())),
|
interval: v.optional(v.pipe(v.string(), v.trim())),
|
||||||
|
server: v.optional(v.pipe(v.string(), v.trim())),
|
||||||
})
|
})
|
||||||
|
|
||||||
function normalizeHttpTarget(target: string, port = 0) {
|
function normalizeHttpTarget(target: string, port = 0) {
|
||||||
@@ -152,18 +157,19 @@ function buildMonitorPayload(values: MonitorValues, enabled = true) {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorIdentity = Pick<MonitorValues, "system" | "target" | "protocol" | "port">
|
type MonitorIdentity = Pick<MonitorValues, "system" | "target" | "protocol" | "port" | "server">
|
||||||
function getMonitorIdentityKey({ system, target, protocol, port }: MonitorIdentity) {
|
function getMonitorIdentityKey({ system, target, protocol, port, server }: MonitorIdentity) {
|
||||||
return `${system}${target}${protocol}${port}`
|
return `${system}${target}${protocol}${port}${protocol === "dns" ? server : ""}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBulkMonitorLine(line: string, lineNumber: number, system: string) {
|
function parseBulkMonitorLine(line: string, lineNumber: number, system: string) {
|
||||||
const [rawTarget = "", rawProtocol = "", rawPort = "", rawInterval = ""] = line.split(",")
|
const [rawTarget = "", rawProtocol = "", rawPort = "", rawInterval = "", rawServer = ""] = line.split(",")
|
||||||
const parsed = v.safeParse(BulkMonitorSchema, {
|
const parsed = v.safeParse(BulkMonitorSchema, {
|
||||||
target: rawTarget,
|
target: rawTarget,
|
||||||
protocol: rawProtocol,
|
protocol: rawProtocol,
|
||||||
port: rawPort,
|
port: rawPort,
|
||||||
interval: rawInterval,
|
interval: rawInterval,
|
||||||
|
server: rawServer,
|
||||||
})
|
})
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error(`Line ${lineNumber}: ${parsed.issues[0]?.message || "invalid monitor entry"}`)
|
throw new Error(`Line ${lineNumber}: ${parsed.issues[0]?.message || "invalid monitor entry"}`)
|
||||||
@@ -176,6 +182,7 @@ function parseBulkMonitorLine(line: string, lineNumber: number, system: string)
|
|||||||
target: parsed.output.target,
|
target: parsed.output.target,
|
||||||
protocol,
|
protocol,
|
||||||
port: parsed.output.port ? Number(parsed.output.port) : 0,
|
port: parsed.output.port ? Number(parsed.output.port) : 0,
|
||||||
|
server: parsed.output.server || "",
|
||||||
interval: parsed.output.interval || `${defaultInterval}`,
|
interval: parsed.output.interval || `${defaultInterval}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -183,7 +190,8 @@ function parseBulkMonitorLine(line: string, lineNumber: number, system: string)
|
|||||||
export function formatBulkMonitorLine(monitor: BulkMonitorLineSource) {
|
export function formatBulkMonitorLine(monitor: BulkMonitorLineSource) {
|
||||||
const port = monitor.protocol !== "tcp" || monitor.port === 443 ? "" : `${monitor.port}`
|
const port = monitor.protocol !== "tcp" || monitor.port === 443 ? "" : `${monitor.port}`
|
||||||
const interval = monitor.interval === defaultInterval ? "" : `${monitor.interval}`
|
const interval = monitor.interval === defaultInterval ? "" : `${monitor.interval}`
|
||||||
return trimTrailingEmptyFields([monitor.target, monitor.protocol, port, interval]).join(",")
|
const server = monitor.protocol !== "dns" ? "" : monitor.server
|
||||||
|
return trimTrailingEmptyFields([monitor.target, monitor.protocol, port, interval, server]).join(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
function SystemMultiSelect({
|
function SystemMultiSelect({
|
||||||
@@ -498,7 +506,7 @@ export function AddMonitorDialog({ systemId, monitors }: { systemId?: string; mo
|
|||||||
<Trans>Bulk Add {{ foo: t`Network Monitors` }}</Trans>
|
<Trans>Bulk Add {{ foo: t`Network Monitors` }}</Trans>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
<Trans>target[,protocol[,port[,interval]]]</Trans>
|
<Trans>target[,protocol[,port[,interval[,server]]]]</Trans>
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<form ref={bulkFormRef} onSubmit={handleBulkSubmit} className="flex h-full flex-col overflow-hidden">
|
<form ref={bulkFormRef} onSubmit={handleBulkSubmit} className="flex h-full flex-col overflow-hidden">
|
||||||
@@ -532,11 +540,16 @@ export function AddMonitorDialog({ systemId, monitors }: { systemId?: string; mo
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="font-mono grow text-sm bg-card"
|
className="font-mono grow text-sm bg-card"
|
||||||
placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60"].join("\n")}
|
placeholder={[
|
||||||
|
"1.1.1.1",
|
||||||
|
"example.com,tcp",
|
||||||
|
"https://example.com,http,,60",
|
||||||
|
"example.com,dns,,,1.1.1.1",
|
||||||
|
].join("\n")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
<Trans>target[,protocol[,port[,interval]]]</Trans>
|
<Trans>target[,protocol[,port[,interval[,server]]]]</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -591,6 +604,7 @@ function MonitorDialogContent({
|
|||||||
const [protocol, setProtocol] = useState<MonitorProtocol>(monitor?.protocol ?? "icmp")
|
const [protocol, setProtocol] = useState<MonitorProtocol>(monitor?.protocol ?? "icmp")
|
||||||
const [target, setTarget] = useState(monitor?.target ?? "")
|
const [target, setTarget] = useState(monitor?.target ?? "")
|
||||||
const [port, setPort] = useState(monitor?.protocol === "tcp" && monitor.port ? String(monitor.port) : "")
|
const [port, setPort] = useState(monitor?.protocol === "tcp" && monitor.port ? String(monitor.port) : "")
|
||||||
|
const [server, setServer] = useState(monitor?.protocol === "dns" ? (monitor.server ?? "") : "")
|
||||||
const [monitorInterval, setMonitorInterval] = useState(String(monitor?.interval ?? defaultInterval))
|
const [monitorInterval, setMonitorInterval] = useState(String(monitor?.interval ?? defaultInterval))
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [selectedSystemId, setSelectedSystemId] = useState(monitor?.system ?? "")
|
const [selectedSystemId, setSelectedSystemId] = useState(monitor?.system ?? "")
|
||||||
@@ -609,6 +623,7 @@ function MonitorDialogContent({
|
|||||||
setProtocol(monitor?.protocol ?? "icmp")
|
setProtocol(monitor?.protocol ?? "icmp")
|
||||||
setTarget(monitor?.target ?? "")
|
setTarget(monitor?.target ?? "")
|
||||||
setPort(monitor?.protocol === "tcp" && monitor.port ? String(monitor.port) : "")
|
setPort(monitor?.protocol === "tcp" && monitor.port ? String(monitor.port) : "")
|
||||||
|
setServer(monitor?.protocol === "dns" ? (monitor.server ?? "") : "")
|
||||||
setMonitorInterval(String(monitor?.interval ?? defaultInterval))
|
setMonitorInterval(String(monitor?.interval ?? defaultInterval))
|
||||||
setSelectedSystemId(monitor?.system ?? "")
|
setSelectedSystemId(monitor?.system ?? "")
|
||||||
setSelectedSystemIds(new Set())
|
setSelectedSystemIds(new Set())
|
||||||
@@ -629,6 +644,7 @@ function MonitorDialogContent({
|
|||||||
target,
|
target,
|
||||||
protocol,
|
protocol,
|
||||||
port: protocol === "tcp" ? Number(port) : 0,
|
port: protocol === "tcp" ? Number(port) : 0,
|
||||||
|
server: protocol === "dns" ? server.trim() : "",
|
||||||
interval: monitorInterval,
|
interval: monitorInterval,
|
||||||
},
|
},
|
||||||
monitor ? monitor.enabled : true
|
monitor ? monitor.enabled : true
|
||||||
@@ -709,7 +725,7 @@ function MonitorDialogContent({
|
|||||||
<Input
|
<Input
|
||||||
value={target}
|
value={target}
|
||||||
onChange={(e) => setTarget(e.target.value)}
|
onChange={(e) => setTarget(e.target.value)}
|
||||||
placeholder={protocol === "http" ? "http://localhost:8090" : "1.1.1.1"}
|
placeholder={protocol === "http" ? "http://localhost:8090" : protocol === "dns" ? "example.com" : "1.1.1.1"}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -745,6 +761,21 @@ function MonitorDialogContent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{protocol === "dns" && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
<Trans>DNS Server</Trans>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={server}
|
||||||
|
onChange={(e) => setServer(e.target.value)}
|
||||||
|
placeholder="1.1.1.1"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
<Trans>Optional. Defaults to the agent's system resolver.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
<Trans>Interval (seconds)</Trans>
|
<Trans>Interval (seconds)</Trans>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
EthernetPortIcon,
|
EthernetPortIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
GlobeIcon,
|
||||||
LandmarkIcon,
|
LandmarkIcon,
|
||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -720,6 +721,13 @@ function NetworkMonitorSheetContent({
|
|||||||
<span>{monitor.port}</span>
|
<span>{monitor.port}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{monitor.protocol === "dns" && monitor.server && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||||
|
<GlobeIcon className="size-3.5 text-muted-foreground" />
|
||||||
|
<span>{monitor.server}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{monitor.certInfo?.expires ? <CertExpiry cert={monitor.certInfo} /> : null}
|
{monitor.certInfo?.expires ? <CertExpiry cert={monitor.certInfo} /> : null}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async function fetchMonitorStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NETWORK_MONITOR_FIELDS =
|
const NETWORK_MONITOR_FIELDS =
|
||||||
"id,system,target,protocol,port,interval,res,resMin1h,resMax1h,resAvg1h,loss1h,enabled,certInfo,updated"
|
"id,system,target,protocol,port,server,interval,res,resMin1h,resMax1h,resAvg1h,loss1h,enabled,certInfo,updated"
|
||||||
|
|
||||||
interface UseNetworkMonitorsProps {
|
interface UseNetworkMonitorsProps {
|
||||||
systemId?: string
|
systemId?: string
|
||||||
|
|||||||
1
internal/site/src/types.d.ts
vendored
1
internal/site/src/types.d.ts
vendored
@@ -644,6 +644,7 @@ export interface NetworkMonitorRecord {
|
|||||||
target: string
|
target: string
|
||||||
protocol: "icmp" | "tcp" | "http" | "dns"
|
protocol: "icmp" | "tcp" | "http" | "dns"
|
||||||
port: number
|
port: number
|
||||||
|
server: string
|
||||||
res: number
|
res: number
|
||||||
resMin1h: number
|
resMin1h: number
|
||||||
resMax1h: number
|
resMax1h: number
|
||||||
|
|||||||
Reference in New Issue
Block a user