fix: improve response ip and rtype, better ip view for logs, requires regeneration of database

This commit is contained in:
pommee
2025-06-12 20:43:41 +02:00
parent 4f29d7b0bc
commit 2fa0073a62
7 changed files with 282 additions and 91 deletions
+17 -11
View File
@@ -133,19 +133,25 @@ func NewSourcesTable(db *sql.DB) error {
func NewRequestLogTable(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS request_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
domain TEXT NOT NULL,
ip TEXT NOT NULL,
blocked BOOLEAN NOT NULL,
cached BOOLEAN NOT NULL,
response_time_ns INTEGER NOT NULL,
client_ip TEXT,
client_name TEXT,
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
domain TEXT NOT NULL,
blocked BOOLEAN NOT NULL,
cached BOOLEAN NOT NULL,
response_time_ns INTEGER NOT NULL,
client_ip TEXT,
client_name TEXT,
status TEXT,
query_type TEXT,
response_size_bytes TEXT
);
response_size_bytes INTEGER
);
CREATE TABLE IF NOT EXISTS request_log_ips (
id SERIAL PRIMARY KEY,
request_log_id INTEGER REFERENCES request_log(id) ON DELETE CASCADE,
ip TEXT NOT NULL,
rtype TEXT NOT NULL
);
`)
if err != nil {
return err
+58 -23
View File
@@ -114,11 +114,16 @@ func GetUniqueQueryTypes(db *sql.DB) ([]interface{}, error) {
func FetchQueries(db *sql.DB, q models.QueryParams) ([]model.RequestLogEntry, error) {
query := fmt.Sprintf(`
SELECT timestamp, domain, ip, blocked, cached, response_time_ns, client_ip, client_name, status, query_type, response_size_bytes
FROM request_log
WHERE domain LIKE ?
SELECT rl.timestamp, rl.domain, rl.blocked, rl.cached, rl.response_time_ns,
rl.client_ip, rl.client_name, rl.status, rl.query_type, rl.response_size_bytes,
COALESCE(STRING_AGG(rli.ip || ':' || rli.rtype, ','), '') as resolved_ips
FROM request_log rl
LEFT JOIN request_log_ips rli ON rl.id = rli.request_log_id
WHERE rl.domain LIKE $1
GROUP BY rl.id, rl.timestamp, rl.domain, rl.blocked, rl.cached, rl.response_time_ns,
rl.client_ip, rl.client_name, rl.status, rl.query_type, rl.response_size_bytes
ORDER BY %s %s
LIMIT ? OFFSET ?`, q.Column, q.Direction)
LIMIT $2 OFFSET $3`, q.Column, q.Direction)
rows, err := db.Query(query, "%"+q.Search+"%", q.PageSize, q.Offset)
if err != nil {
@@ -132,14 +137,14 @@ func FetchQueries(db *sql.DB, q models.QueryParams) ([]model.RequestLogEntry, er
var queries []model.RequestLogEntry
for rows.Next() {
var query model.RequestLogEntry
var ipString string
var resolvedIPsString string
var timestamp string
query.ClientInfo = &model.Client{}
if err := rows.Scan(
&timestamp, &query.Domain, &ipString,
&query.Blocked, &query.Cached, &query.ResponseTime,
&query.ClientInfo.IP, &query.ClientInfo.Name, &query.Status, &query.QueryType, &query.ResponseSizeBytes,
&timestamp, &query.Domain, &query.Blocked, &query.Cached, &query.ResponseTime,
&query.ClientInfo.IP, &query.ClientInfo.Name, &query.Status, &query.QueryType,
&query.ResponseSizeBytes, &resolvedIPsString,
); err != nil {
return nil, err
}
@@ -149,13 +154,37 @@ func FetchQueries(db *sql.DB, q models.QueryParams) ([]model.RequestLogEntry, er
return nil, fmt.Errorf("error: %v", err)
}
query.Timestamp = time.Unix(parsedTime, 0)
query.IP = strings.Split(ipString, ",")
query.IP = parseResolvedIPs(resolvedIPsString)
queries = append(queries, query)
}
return queries, nil
}
func parseResolvedIPs(ipString string) []model.ResolvedIP {
if ipString == "" {
return []model.ResolvedIP{}
}
parts := strings.Split(ipString, ",")
resolvedIPs := make([]model.ResolvedIP, 0, len(parts))
for _, part := range parts {
if part == "" {
continue
}
ipParts := strings.Split(part, ":")
if len(ipParts) == 2 {
resolvedIPs = append(resolvedIPs, model.ResolvedIP{
IP: ipParts[0],
RType: ipParts[1],
})
}
}
return resolvedIPs
}
func FetchAllClients(db *sql.DB) (map[string]dbModel.Client, error) {
uniqueClients := make(map[string]dbModel.Client)
@@ -364,7 +393,6 @@ func SaveRequestLog(db *sql.DB, entries []model.RequestLogEntry) error {
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer func() {
if p := recover(); p != nil {
_ = tx.Rollback()
@@ -376,15 +404,15 @@ func SaveRequestLog(db *sql.DB, entries []model.RequestLogEntry) error {
}
}()
valueStrings := make([]string, 0, len(entries))
valueArgs := make([]interface{}, 0, len(entries)*11)
for _, entry := range entries {
valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
valueArgs = append(valueArgs,
var logID int64
err = tx.QueryRow(`
INSERT INTO request_log (timestamp, domain, blocked, cached, response_time_ns,
client_ip, client_name, status, query_type, response_size_bytes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id`,
entry.Timestamp.Unix(),
entry.Domain,
strings.Join(entry.IP, ","),
entry.Blocked,
entry.Cached,
entry.ResponseTime,
@@ -393,15 +421,22 @@ func SaveRequestLog(db *sql.DB, entries []model.RequestLogEntry) error {
entry.Status,
entry.QueryType,
entry.ResponseSizeBytes,
)
}
).Scan(&logID)
query := fmt.Sprintf("INSERT INTO request_log (timestamp, domain, ip, blocked, cached, response_time_ns, client_ip, client_name, status, query_type, response_size_bytes) VALUES %s",
strings.Join(valueStrings, ","))
if err != nil {
return fmt.Errorf("could not save request log: %v", err)
}
_, err = tx.Exec(query, valueArgs...)
if err != nil {
return fmt.Errorf("could not save request log. Reason: %v", err)
for _, resolvedIP := range entry.IP {
_, err = tx.Exec(`
INSERT INTO request_log_ips (request_log_id, ip, rtype)
VALUES ($1, $2, $3)`,
logID, resolvedIP.IP, resolvedIP.RType)
if err != nil {
return fmt.Errorf("could not save resolved IP: %v", err)
}
}
}
return nil
+58 -21
View File
@@ -18,9 +18,6 @@ import (
var (
blackholeIPv4 = net.ParseIP("0.0.0.0")
blackholeIPv6 = net.ParseIP("::")
resolvedIPv4 = []string{"0.0.0.0"}
resolvedIPv6 = []string{"::"}
)
func trimDomainDot(name string) string {
@@ -41,6 +38,10 @@ func (s *DNSServer) processQuery(request *Request) model.RequestLogEntry {
s.Config.DNS.Status.Paused = false
}
// TODO
// IsBlacklisted will not work if the trailing dot is removed
// Trailing dot shall exist to be fully quallyfied
// However many blacklisted domains miss the trailing dot
if !s.Config.DNS.Status.Paused && s.Blacklist.IsBlacklisted(domainName) && !s.Whitelist.IsWhitelisted(domainName) {
return s.handleBlacklisted(request)
}
@@ -187,10 +188,15 @@ func (s *DNSServer) respondWithLocalhost(request *Request) model.RequestLogEntry
_ = request.W.WriteMsg(request.Msg)
return model.RequestLogEntry{
Timestamp: request.Sent,
Domain: request.Question.Name,
Status: dns.RcodeToString[dns.RcodeSuccess],
IP: []string{"localhost.lan"},
Timestamp: request.Sent,
Domain: request.Question.Name,
Status: dns.RcodeToString[dns.RcodeSuccess],
IP: []model.ResolvedIP{
{
IP: "localhost.lan",
RType: "PTR",
},
},
Blocked: false,
Cached: false,
ResponseTime: time.Since(request.Sent),
@@ -220,10 +226,15 @@ func (s *DNSServer) respondWithHostname(request *Request, hostname string) model
_ = request.W.WriteMsg(request.Msg)
return model.RequestLogEntry{
Domain: request.Question.Name,
Status: dns.RcodeToString[dns.RcodeSuccess],
QueryType: dns.TypeToString[request.Question.Qtype],
IP: []string{hostname},
Domain: request.Question.Name,
Status: dns.RcodeToString[dns.RcodeSuccess],
QueryType: dns.TypeToString[request.Question.Qtype],
IP: []model.ResolvedIP{
{
IP: hostname,
RType: "PTR",
},
},
ResponseSizeBytes: request.Msg.Len(),
Timestamp: request.Sent,
ResponseTime: time.Since(request.Sent),
@@ -247,10 +258,13 @@ func (s *DNSServer) forwardPTRQueryUpstream(request *Request) model.RequestLogEn
request.Msg.Authoritative = false
request.Msg.RecursionAvailable = true
var resolvedHostnames []string
var resolvedHostnames []model.ResolvedIP
for _, answer := range answers {
if ptr, ok := answer.(*dns.PTR); ok {
resolvedHostnames = append(resolvedHostnames, ptr.Ptr)
resolvedHostnames = append(resolvedHostnames, model.ResolvedIP{
IP: ptr.Ptr,
RType: "PTR",
})
}
}
@@ -271,7 +285,7 @@ func (s *DNSServer) forwardPTRQueryUpstream(request *Request) model.RequestLogEn
func (s *DNSServer) handleStandardQuery(req *Request) model.RequestLogEntry {
answers, cached, status := s.Resolve(req)
resolved := make([]string, 0, len(answers))
resolved := make([]model.ResolvedIP, 0, len(answers))
req.Msg.Answer = answers
if req.Msg.RecursionDesired {
@@ -281,13 +295,25 @@ func (s *DNSServer) handleStandardQuery(req *Request) model.RequestLogEntry {
for _, a := range answers {
switch rr := a.(type) {
case *dns.A:
resolved = append(resolved, rr.A.String())
resolved = append(resolved, model.ResolvedIP{
IP: rr.A.String(),
RType: "A",
})
case *dns.AAAA:
resolved = append(resolved, rr.AAAA.String())
resolved = append(resolved, model.ResolvedIP{
IP: rr.AAAA.String(),
RType: "AAAA",
})
case *dns.PTR:
resolved = append(resolved, rr.Ptr)
resolved = append(resolved, model.ResolvedIP{
IP: rr.Ptr,
RType: "PTR",
})
case *dns.CNAME:
resolved = append(resolved, rr.Target)
resolved = append(resolved, model.ResolvedIP{
IP: rr.Target,
RType: "CNAME",
})
}
}
@@ -482,7 +508,7 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
request.Msg.RecursionAvailable = true
request.Msg.Rcode = dns.RcodeSuccess
var resolved []string
var resolved []model.ResolvedIP
cacheTTL := uint32(s.Config.DNS.CacheTTL)
switch request.Question.Qtype {
@@ -514,7 +540,12 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
A: blackholeIPv4,
}}
}
resolved = resolvedIPv4
resolved = []model.ResolvedIP{
{
IP: blackholeIPv4.String(),
RType: "A",
},
}
case dns.TypeAAAA:
if len(request.Msg.Answer) == 1 {
@@ -544,7 +575,12 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
AAAA: blackholeIPv6,
}}
}
resolved = resolvedIPv6
resolved = []model.ResolvedIP{
{
IP: blackholeIPv6.String(),
RType: "AAAA",
},
}
default:
request.Msg.Rcode = dns.RcodeNameError
@@ -567,6 +603,7 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
Timestamp: request.Sent,
ResponseTime: time.Since(request.Sent),
Blocked: true,
Cached: false,
ClientInfo: request.Client,
}
}
+6 -1
View File
@@ -6,7 +6,7 @@ type RequestLogEntry struct {
Domain string `json:"domain"`
Status string `json:"status"`
QueryType string `json:"queryType"`
IP []string `json:"ip"`
IP []ResolvedIP `json:"ip"`
ResponseSizeBytes int `json:"responseSizeBytes"`
Timestamp time.Time `json:"timestamp"`
ResponseTime time.Duration `json:"responseTimeNS"`
@@ -15,6 +15,11 @@ type RequestLogEntry struct {
ClientInfo *Client `json:"client"`
}
type ResolvedIP struct {
IP string `json:"ip"`
RType string `json:"rtype"`
}
type RequestLogIntervalSummary struct {
IntervalStart time.Time `json:"start"`
BlockedCount int `json:"blocked"`
+34 -4
View File
@@ -1,4 +1,12 @@
import { Check, Lightning, ShieldSlash } from "@phosphor-icons/react";
import { IPEntry } from "@/pages/logs";
import { DeleteRequest, GetRequest, PostRequest } from "@/util";
import {
Check,
CheckIcon,
Lightning,
ShieldSlash,
TrashIcon
} from "@phosphor-icons/react";
import { Checkbox } from "@radix-ui/react-checkbox";
import { ColumnDef } from "@tanstack/react-table";
@@ -13,7 +21,7 @@ export type Queries = {
cached: boolean;
client: Client;
domain: string;
ip: Array<string>;
ip: IPEntry[];
queryType: string;
responseTimeNS: number;
status: string;
@@ -74,8 +82,30 @@ export const columns: ColumnDef<Queries>[] = [
},
{
accessorKey: "ip",
header: "IP",
cell: ({ row }) => <div>{row.getValue("ip")}</div>
header: "IP(s)",
cell: ({ getValue }) => {
const value = getValue() as IPEntry[];
if (Array.isArray(value)) {
return (
<div className="flex flex-col">
{value.map((entry, i) => {
if (entry && typeof entry === "object" && entry.ip) {
const ip = String(entry.ip || "");
const rtype = String(entry.rtype || "");
return (
<span key={i}>
{ip} {rtype && `(${rtype})`}
</span>
);
} else {
return <span key={i}>{String(entry || "")}</span>;
}
})}
</div>
);
}
return <div>{String(value || "")}</div>;
}
},
{
id: "client",
+2 -2
View File
@@ -395,8 +395,8 @@ docker pull pommee/goaway:version`;
log.includes("[info]")
? "text-green-400"
: log.includes("[error]")
? "text-red-400"
: ""
? "text-red-400"
: ""
}
>
{log}
+107 -29
View File
@@ -59,12 +59,36 @@ import {
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
type QueryResponse = {
details: Queries[];
interface Client {
ip: string;
name: string;
mac: string;
}
export interface IPEntry {
ip: string;
rtype: string;
}
interface QueryDetail {
domain: string;
status: string;
queryType: string;
ip: IPEntry[];
responseSizeBytes: number;
timestamp: string;
responseTimeNS: number;
blocked: boolean;
cached: boolean;
client: Client;
}
interface QueryResponse {
details: QueryDetail[];
draw: string;
recordsFiltered: number;
recordsTotal: number;
};
}
async function fetchQueries(
page: number,
@@ -83,8 +107,9 @@ async function fetchQueries(
return {
details: response.details.map(
(item: {
client: { ip: string; name: string; mac: string };
ip: string;
client: { ip?: string; name?: string; mac?: string };
ip?: { ip?: string; rtype?: string }[];
[key: string]: string;
}) => ({
...item,
client: {
@@ -92,7 +117,12 @@ async function fetchQueries(
name: item.client?.name || "",
mac: item.client?.mac || ""
},
ip: Array.isArray(item.ip) ? item.ip : []
ip: Array.isArray(item.ip)
? item.ip.map((entry) => ({
ip: String(entry?.ip || ""),
rtype: String(entry?.rtype || "")
}))
: []
})
),
draw: response.draw || "1",
@@ -127,9 +157,9 @@ export function Logs() {
const [rowSelection, setRowSelection] = useState({});
const totalPages = Math.ceil(totalRecords / pageSize);
const debounce = (func: { (value) }, delay: number | undefined) => {
let timeoutId: string | number | NodeJS.Timeout | undefined;
return (...args) => {
const debounce = (func: (...args: unknown[]) => void, delay: number) => {
let timeoutId: NodeJS.Timeout | undefined;
return (...args: unknown[]) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
@@ -139,7 +169,7 @@ export function Logs() {
const debouncedSetDomainFilter = useMemo(
() =>
debounce((value) => {
debounce((value: string) => {
setDomainFilter(value);
setPageIndex(0);
}, 500),
@@ -159,14 +189,19 @@ export function Logs() {
try {
const newQuery = JSON.parse(event.data);
const formattedQuery = {
const formattedQuery: Queries = {
...newQuery,
client: {
ip: newQuery.client?.ip || "",
name: newQuery.client?.name || "",
mac: newQuery.client?.mac || ""
},
ip: Array.isArray(newQuery.ip) ? newQuery.ip : []
ip: Array.isArray(newQuery.ip)
? newQuery.ip.map((entry: IPEntry) => ({
ip: String(entry?.ip || ""),
rtype: String(entry?.rtype || "")
}))
: []
};
if (
@@ -189,16 +224,16 @@ export function Logs() {
} catch (error) {
console.error("Error handling WebSocket message:", error);
}
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
setWsConnected(false);
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
setWsConnected(false);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
setWsConnected(false);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
setWsConnected(false);
};
return () => {
@@ -394,17 +429,60 @@ export function Logs() {
}}
className="block truncate"
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
{(() => {
if (cell.column.id === "ip") {
const ipValue = cell.getValue() as IPEntry[];
if (
Array.isArray(ipValue) &&
ipValue.length > 0
) {
return (
<div className="flex items-center gap-1">
<span>{ipValue[0]?.ip || ""}</span>
{ipValue.length > 1 && (
<span className="text-xs text-stone-400 border-1 ml-1 px-1 rounded border-green-600/60">
+{ipValue.length - 1}
</span>
)}
</div>
);
}
return "";
}
return flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
})()}
</span>
</TooltipTrigger>
<TooltipContent className="bg-stone-800 border-1 border-stone-700 text-white p-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
<TooltipContent className="bg-stone-800 border border-stone-700 text-white text-sm p-3 rounded-md shadow-md font-mono">
{(() => {
if (cell.column.id === "ip") {
const ipValue = cell.getValue() as IPEntry[];
return Array.isArray(ipValue) ? (
<div className="space-y-1">
{ipValue.map((entry, i) => (
<div key={i} className="flex gap-2">
<span className="inline-block w-[80px] text-stone-400">
{entry?.rtype
? `[${entry.rtype}]`
: ""}
</span>
<span>{entry?.ip || ""}</span>
</div>
))}
</div>
) : (
""
);
}
return flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
})()}
</TooltipContent>
</Tooltip>
</TooltipProvider>