feat: wgctl-conntrack Go daemon
- conntrack/event.go: TrafficEvent type - conntrack/filter.go: WG subnet filter, IsExternal, ProtoName - conntrack/subscriber.go: netlink conntrack DESTROY subscriber - writer/log.go: JSON line writer with mutex - resolver/peers.go: WG IP → peer name from conf files + endpoint index - resolver/services.go: IP:port → service name from services.json - config/config.go: reads wgctl.json, sensible defaults - cmd/root.go: CLI flags - main.go: wires everything together - DESTROY events only: full byte/packet counts per connection - filters to WireGuard subnet, marks external traffic
This commit is contained in:
parent
91593b2576
commit
d314ba376e
13 changed files with 605 additions and 0 deletions
33
daemon/wgctl-conntrack/cmd/root.go
Normal file
33
daemon/wgctl-conntrack/cmd/root.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Flags holds CLI flags
|
||||
type Flags struct {
|
||||
WGDir string
|
||||
Subnet string
|
||||
LogFile string
|
||||
Version bool
|
||||
}
|
||||
|
||||
const Version = "0.1.0"
|
||||
|
||||
func Parse() *Flags {
|
||||
f := &Flags{}
|
||||
flag.StringVar(&f.WGDir, "wg-dir", "/etc/wireguard", "WireGuard base directory")
|
||||
flag.StringVar(&f.Subnet, "subnet", "", "WireGuard subnet override")
|
||||
flag.StringVar(&f.LogFile, "log-file", "", "Accept events log file override")
|
||||
flag.BoolVar(&f.Version, "version", false, "Print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
if f.Version {
|
||||
fmt.Println(Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
42
daemon/wgctl-conntrack/config/config.go
Normal file
42
daemon/wgctl-conntrack/config/config.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config holds wgctl-conntrack runtime configuration
|
||||
type Config struct {
|
||||
WGSubnet string
|
||||
DataDir string
|
||||
ClientsDir string
|
||||
AcceptLogFile string
|
||||
ServicesFile string
|
||||
}
|
||||
|
||||
type wgctlJSON struct {
|
||||
WireGuard struct {
|
||||
Subnet string `json:"subnet"`
|
||||
} `json:"wireguard"`
|
||||
}
|
||||
|
||||
// Load reads config from wgctl.json and applies defaults
|
||||
func Load(wgDir string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
WGSubnet: "10.1.0.0/16",
|
||||
DataDir: wgDir + "/.wgctl/data",
|
||||
ClientsDir: wgDir + "/clients",
|
||||
AcceptLogFile: wgDir + "/.wgctl/daemon/accept_events.log",
|
||||
ServicesFile: wgDir + "/.wgctl/data/services.json",
|
||||
}
|
||||
|
||||
jsonFile := wgDir + "/.wgctl/config/wgctl.json"
|
||||
if data, err := os.ReadFile(jsonFile); err == nil {
|
||||
var wj wgctlJSON
|
||||
if json.Unmarshal(data, &wj) == nil && wj.WireGuard.Subnet != "" {
|
||||
cfg.WGSubnet = wj.WireGuard.Subnet
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
29
daemon/wgctl-conntrack/conntrack/event.go
Normal file
29
daemon/wgctl-conntrack/conntrack/event.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package conntrack
|
||||
|
||||
import "time"
|
||||
|
||||
// EventType represents the type of traffic event
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventAccept EventType = "accept"
|
||||
EventExternal EventType = "external"
|
||||
)
|
||||
|
||||
// TrafficEvent is the normalized event written to the log
|
||||
type TrafficEvent struct {
|
||||
Timestamp time.Time `json:"ts"`
|
||||
Peer string `json:"peer"`
|
||||
SrcIP string `json:"src_ip"`
|
||||
DstIP string `json:"dst_ip"`
|
||||
DstPort uint16 `json:"dst_port"`
|
||||
Proto string `json:"proto"`
|
||||
BytesOrig uint64 `json:"bytes_orig"`
|
||||
BytesReply uint64 `json:"bytes_reply"`
|
||||
PacketsOrig uint64 `json:"packets_orig"`
|
||||
PacketsReply uint64 `json:"packets_reply"`
|
||||
DurationSec float64 `json:"duration_sec"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Event EventType `json:"event"`
|
||||
External bool `json:"external"`
|
||||
}
|
||||
44
daemon/wgctl-conntrack/conntrack/filter.go
Normal file
44
daemon/wgctl-conntrack/conntrack/filter.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package conntrack
|
||||
|
||||
import "net"
|
||||
|
||||
var privateRanges = []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
}
|
||||
|
||||
var privateCIDRs []*net.IPNet
|
||||
|
||||
func init() {
|
||||
for _, cidr := range privateRanges {
|
||||
_, ipnet, _ := net.ParseCIDR(cidr)
|
||||
privateCIDRs = append(privateCIDRs, ipnet)
|
||||
}
|
||||
}
|
||||
|
||||
func IsWGPeer(ip net.IP, wgSubnet *net.IPNet) bool {
|
||||
return wgSubnet.Contains(ip)
|
||||
}
|
||||
|
||||
func IsExternal(ip net.IP) bool {
|
||||
for _, cidr := range privateCIDRs {
|
||||
if cidr.Contains(ip) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ProtoName(proto uint8) string {
|
||||
switch proto {
|
||||
case 6:
|
||||
return "tcp"
|
||||
case 17:
|
||||
return "udp"
|
||||
case 1:
|
||||
return "icmp"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
117
daemon/wgctl-conntrack/conntrack/subscriber.go
Normal file
117
daemon/wgctl-conntrack/conntrack/subscriber.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package conntrack
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
ct "github.com/ti-mo/conntrack"
|
||||
"github.com/ti-mo/netfilter"
|
||||
)
|
||||
|
||||
// Resolver maps IPs and ports to peer/service names
|
||||
type Resolver interface {
|
||||
PeerForIP(ip net.IP) string
|
||||
ServiceForDst(ip net.IP, port uint16, proto string) string
|
||||
}
|
||||
|
||||
// Subscriber listens for conntrack DESTROY events
|
||||
type Subscriber struct {
|
||||
wgSubnet *net.IPNet
|
||||
events chan<- TrafficEvent
|
||||
resolver Resolver
|
||||
}
|
||||
|
||||
func NewSubscriber(wgSubnet *net.IPNet, events chan<- TrafficEvent, resolver Resolver) *Subscriber {
|
||||
return &Subscriber{wgSubnet: wgSubnet, events: events, resolver: resolver}
|
||||
}
|
||||
|
||||
func (s *Subscriber) Run() error {
|
||||
conn, err := ct.Dial(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
evCh := make(chan ct.Event, 256)
|
||||
|
||||
errCh, err := conn.Listen(evCh, 1, []netfilter.NetlinkGroup{
|
||||
netfilter.GroupCTDestroy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("conntrack subscriber started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-evCh:
|
||||
s.processEvent(ev)
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Subscriber) processEvent(ev ct.Event) {
|
||||
flow := ev.Flow
|
||||
if flow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tuple := flow.TupleOrig
|
||||
|
||||
// Skip IPv6
|
||||
if !tuple.IP.SourceAddress.Is4() || !tuple.IP.DestinationAddress.Is4() {
|
||||
return
|
||||
}
|
||||
|
||||
srcBytes := tuple.IP.SourceAddress.As4()
|
||||
dstBytes := tuple.IP.DestinationAddress.As4()
|
||||
srcIP := net.IP(srcBytes[:])
|
||||
dstIP := net.IP(dstBytes[:])
|
||||
|
||||
// Only process WireGuard peer traffic
|
||||
if !IsWGPeer(srcIP, s.wgSubnet) {
|
||||
return
|
||||
}
|
||||
|
||||
proto := ProtoName(tuple.Proto.Protocol)
|
||||
dstPort := tuple.Proto.DestinationPort
|
||||
external := IsExternal(dstIP)
|
||||
|
||||
peer := s.resolver.PeerForIP(srcIP)
|
||||
if peer == "" {
|
||||
return
|
||||
}
|
||||
|
||||
service := s.resolver.ServiceForDst(dstIP, dstPort, proto)
|
||||
|
||||
var durationSec float64
|
||||
if flow.Timestamp.Stop.After(flow.Timestamp.Start) {
|
||||
durationSec = flow.Timestamp.Stop.Sub(flow.Timestamp.Start).Seconds()
|
||||
}
|
||||
|
||||
eventType := EventAccept
|
||||
if external {
|
||||
eventType = EventExternal
|
||||
}
|
||||
|
||||
s.events <- TrafficEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Peer: peer,
|
||||
SrcIP: srcIP.String(),
|
||||
DstIP: dstIP.String(),
|
||||
DstPort: dstPort,
|
||||
Proto: proto,
|
||||
BytesOrig: flow.CountersOrig.Bytes,
|
||||
BytesReply: flow.CountersReply.Bytes,
|
||||
PacketsOrig: flow.CountersOrig.Packets,
|
||||
PacketsReply: flow.CountersReply.Packets,
|
||||
DurationSec: durationSec,
|
||||
Service: service,
|
||||
Event: eventType,
|
||||
External: external,
|
||||
}
|
||||
}
|
||||
16
daemon/wgctl-conntrack/go.mod
Normal file
16
daemon/wgctl-conntrack/go.mod
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module git.krilio.net/nuno/wgctl-conntrack
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/ti-mo/conntrack v0.6.0 // indirect
|
||||
github.com/ti-mo/netfilter v0.5.3 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
||||
20
daemon/wgctl-conntrack/go.sum
Normal file
20
daemon/wgctl-conntrack/go.sum
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/ti-mo/conntrack v0.6.0 h1:laiW2+dzKyS2u0aVr6FeRQs+v7cj4t7q+twolL/ZkjQ=
|
||||
github.com/ti-mo/conntrack v0.6.0/go.mod h1:4HZrFQQLOSuBzgQNid3H/wYyyp1kfGXUYxueXjIGibo=
|
||||
github.com/ti-mo/netfilter v0.5.3 h1:ikzduvnaUMwre5bhbNwWOd6bjqLMVb33vv0XXbK0xGQ=
|
||||
github.com/ti-mo/netfilter v0.5.3/go.mod h1:08SyBCg6hu1qyQk4s3DjjJKNrm3RTb32nm6AzyT972E=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
BIN
daemon/wgctl-conntrack/go1.21.13.linux-amd64.tar.gz
Normal file
BIN
daemon/wgctl-conntrack/go1.21.13.linux-amd64.tar.gz
Normal file
Binary file not shown.
71
daemon/wgctl-conntrack/main.go
Normal file
71
daemon/wgctl-conntrack/main.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.krilio.net/nuno/wgctl-conntrack/cmd"
|
||||
"git.krilio.net/nuno/wgctl-conntrack/config"
|
||||
ctconn "git.krilio.net/nuno/wgctl-conntrack/conntrack"
|
||||
"git.krilio.net/nuno/wgctl-conntrack/resolver"
|
||||
"git.krilio.net/nuno/wgctl-conntrack/writer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flags := cmd.Parse()
|
||||
|
||||
cfg, err := config.Load(flags.WGDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
if flags.Subnet != "" {
|
||||
cfg.WGSubnet = flags.Subnet
|
||||
}
|
||||
if flags.LogFile != "" {
|
||||
cfg.AcceptLogFile = flags.LogFile
|
||||
}
|
||||
|
||||
_, wgSubnet, err := net.ParseCIDR(cfg.WGSubnet)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid WG subnet %q: %v", cfg.WGSubnet, err)
|
||||
}
|
||||
|
||||
log.Printf("wgctl-conntrack v%s starting (subnet: %s, log: %s)",
|
||||
cmd.Version, cfg.WGSubnet, cfg.AcceptLogFile)
|
||||
|
||||
peerResolver := resolver.NewPeerResolver(flags.WGDir)
|
||||
svcResolver := resolver.NewServiceResolver(cfg.ServicesFile)
|
||||
|
||||
res := &combinedResolver{peers: peerResolver, services: svcResolver}
|
||||
events := make(chan ctconn.TrafficEvent, 512)
|
||||
|
||||
go writer.NewLogWriter(cfg.AcceptLogFile).Run(events)
|
||||
|
||||
sub := ctconn.NewSubscriber(wgSubnet, events, res)
|
||||
go func() {
|
||||
if err := sub.Run(); err != nil {
|
||||
log.Fatalf("conntrack subscriber error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sig
|
||||
log.Println("wgctl-conntrack shutting down")
|
||||
}
|
||||
|
||||
type combinedResolver struct {
|
||||
peers *resolver.PeerResolver
|
||||
services *resolver.ServiceResolver
|
||||
}
|
||||
|
||||
func (r *combinedResolver) PeerForIP(ip net.IP) string {
|
||||
return r.peers.PeerForIP(ip)
|
||||
}
|
||||
|
||||
func (r *combinedResolver) ServiceForDst(ip net.IP, port uint16, proto string) string {
|
||||
return r.services.ServiceForDst(ip, port, proto)
|
||||
}
|
||||
93
daemon/wgctl-conntrack/resolver/peers.go
Normal file
93
daemon/wgctl-conntrack/resolver/peers.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PeerResolver maps WireGuard peer IPs to peer names
|
||||
type PeerResolver struct {
|
||||
mu sync.RWMutex
|
||||
ipToName map[string]string
|
||||
wgDir string
|
||||
}
|
||||
|
||||
func NewPeerResolver(wgDir string) *PeerResolver {
|
||||
r := &PeerResolver{wgDir: wgDir, ipToName: make(map[string]string)}
|
||||
r.reload()
|
||||
go r.watchReload()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PeerResolver) PeerForIP(ip net.IP) string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.ipToName[ip.String()]
|
||||
}
|
||||
|
||||
func (r *PeerResolver) reload() {
|
||||
newMap := make(map[string]string)
|
||||
|
||||
// WireGuard IPs from conf files (10.1.x.x → peer name)
|
||||
clientsDir := r.wgDir + "/clients"
|
||||
entries, err := os.ReadDir(clientsDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".conf") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(entry.Name(), ".conf")
|
||||
if ip := parseAddressFromConf(clientsDir + "/" + entry.Name()); ip != "" {
|
||||
newMap[ip] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External IPs from endpoint index (external IP → peer name)
|
||||
indexFile := r.wgDir + "/.wgctl/data/peer-history/endpoint_index.json"
|
||||
if data, err := os.ReadFile(indexFile); err == nil {
|
||||
var index map[string]string
|
||||
if json.Unmarshal(data, &index) == nil {
|
||||
for ip, peer := range index {
|
||||
newMap[ip] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.ipToName = newMap
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *PeerResolver) watchReload() {
|
||||
ticker := time.NewTicker(60 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
r.reload()
|
||||
}
|
||||
}
|
||||
|
||||
func parseAddressFromConf(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Address") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
ip := strings.TrimSpace(parts[1])
|
||||
if idx := strings.Index(ip, "/"); idx != -1 {
|
||||
ip = ip[:idx]
|
||||
}
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
93
daemon/wgctl-conntrack/resolver/services.go
Normal file
93
daemon/wgctl-conntrack/resolver/services.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServiceResolver maps IP:port:proto to service names
|
||||
type ServiceResolver struct {
|
||||
mu sync.RWMutex
|
||||
portToSvc map[string]string
|
||||
servicesFile string
|
||||
}
|
||||
|
||||
func NewServiceResolver(servicesFile string) *ServiceResolver {
|
||||
r := &ServiceResolver{servicesFile: servicesFile, portToSvc: make(map[string]string)}
|
||||
r.reload()
|
||||
go r.watchReload()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ServiceResolver) ServiceForDst(ip net.IP, port uint16, proto string) string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// Try IP:port:proto first
|
||||
if svc, ok := r.portToSvc[fmt.Sprintf("%s:%d:%s", ip.String(), port, proto)]; ok {
|
||||
return svc
|
||||
}
|
||||
// Fall back to IP only
|
||||
if svc, ok := r.portToSvc[ip.String()]; ok {
|
||||
return svc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *ServiceResolver) reload() {
|
||||
data, err := os.ReadFile(r.servicesFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var services map[string]interface{}
|
||||
if json.Unmarshal(data, &services) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newMap := make(map[string]string)
|
||||
for name, svcRaw := range services {
|
||||
svc, ok := svcRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
hosts := map[string]bool{}
|
||||
if hostsRaw, ok := svc["hosts"].(map[string]interface{}); ok {
|
||||
for ip := range hostsRaw {
|
||||
hosts[ip] = true
|
||||
newMap[ip] = name
|
||||
}
|
||||
}
|
||||
|
||||
if portsRaw, ok := svc["ports"].([]interface{}); ok {
|
||||
for _, portRaw := range portsRaw {
|
||||
port, ok := portRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
portNum := fmt.Sprintf("%.0f", port["port"])
|
||||
proto, _ := port["proto"].(string)
|
||||
for ip := range hosts {
|
||||
newMap[fmt.Sprintf("%s:%s:%s", ip, portNum, proto)] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.portToSvc = newMap
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *ServiceResolver) watchReload() {
|
||||
ticker := time.NewTicker(60 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
r.reload()
|
||||
}
|
||||
}
|
||||
BIN
daemon/wgctl-conntrack/wgctl-conntrack
Executable file
BIN
daemon/wgctl-conntrack/wgctl-conntrack
Executable file
Binary file not shown.
47
daemon/wgctl-conntrack/writer/log.go
Normal file
47
daemon/wgctl-conntrack/writer/log.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package writer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"git.krilio.net/nuno/wgctl-conntrack/conntrack"
|
||||
)
|
||||
|
||||
// LogWriter writes TrafficEvents as JSON lines to a file
|
||||
type LogWriter struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewLogWriter(path string) *LogWriter {
|
||||
return &LogWriter{path: path}
|
||||
}
|
||||
|
||||
func (w *LogWriter) Write(ev conntrack.TrafficEvent) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
f, err := os.OpenFile(w.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := json.Marshal(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(append(data, '\n'))
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *LogWriter) Run(events <-chan conntrack.TrafficEvent) {
|
||||
for ev := range events {
|
||||
if err := w.Write(ev); err != nil {
|
||||
log.Printf("error writing event: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue