mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-11 04:12:13 +01:00
cmd/netlogfmt: support resolving IP addresses to synonymous labels (#17955)
We now embed node information into network flow logs. By default, netlogfmt still prints out using Tailscale IP addresses. Support a "--resolve-addrs=TYPE" flag that can be used to specify resolving IP addresses as node IDs, hostnames, users, or tags. Updates tailscale/corp#33352 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
parent
c09c95ef67
commit
3b865d7c33
@ -44,24 +44,50 @@ import (
|
|||||||
"github.com/dsnet/try"
|
"github.com/dsnet/try"
|
||||||
jsonv2 "github.com/go-json-experiment/json"
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
"github.com/go-json-experiment/json/jsontext"
|
"github.com/go-json-experiment/json/jsontext"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/bools"
|
||||||
"tailscale.com/types/logid"
|
"tailscale.com/types/logid"
|
||||||
"tailscale.com/types/netlogtype"
|
"tailscale.com/types/netlogtype"
|
||||||
"tailscale.com/util/must"
|
"tailscale.com/util/must"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
resolveNames = flag.Bool("resolve-names", false, "convert tailscale IP addresses to hostnames; must also specify --api-key and --tailnet-id")
|
resolveNames = flag.Bool("resolve-names", false, "This is equivalent to specifying \"--resolve-addrs=name\".")
|
||||||
apiKey = flag.String("api-key", "", "API key to query the Tailscale API with; see https://login.tailscale.com/admin/settings/keys")
|
resolveAddrs = flag.String("resolve-addrs", "", "Resolve each tailscale IP address as a node ID, name, or user.\n"+
|
||||||
tailnetName = flag.String("tailnet-name", "", "tailnet domain name to lookup devices in; see https://login.tailscale.com/admin/settings/general")
|
"If network flow logs do not support embedded node information,\n"+
|
||||||
|
"then --api-key and --tailnet-name must also be provided.\n"+
|
||||||
|
"Valid values include \"nodeId\", \"name\", or \"user\".")
|
||||||
|
apiKey = flag.String("api-key", "", "The API key to query the Tailscale API with.\nSee https://login.tailscale.com/admin/settings/keys")
|
||||||
|
tailnetName = flag.String("tailnet-name", "", "The Tailnet name to lookup nodes within.\nSee https://login.tailscale.com/admin/settings/general")
|
||||||
)
|
)
|
||||||
|
|
||||||
var namesByAddr map[netip.Addr]string
|
var (
|
||||||
|
tailnetNodesByAddr map[netip.Addr]netlogtype.Node
|
||||||
|
tailnetNodesByID map[tailcfg.StableNodeID]netlogtype.Node
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *resolveNames {
|
if *resolveNames {
|
||||||
namesByAddr = mustMakeNamesByAddr()
|
*resolveAddrs = "name"
|
||||||
}
|
}
|
||||||
|
*resolveAddrs = strings.ToLower(*resolveAddrs) // make case-insensitive
|
||||||
|
*resolveAddrs = strings.TrimSuffix(*resolveAddrs, "s") // allow plural form
|
||||||
|
*resolveAddrs = strings.ReplaceAll(*resolveAddrs, " ", "") // ignore spaces
|
||||||
|
*resolveAddrs = strings.ReplaceAll(*resolveAddrs, "-", "") // ignore dashes
|
||||||
|
*resolveAddrs = strings.ReplaceAll(*resolveAddrs, "_", "") // ignore underscores
|
||||||
|
switch *resolveAddrs {
|
||||||
|
case "id", "nodeid":
|
||||||
|
*resolveAddrs = "nodeid"
|
||||||
|
case "name", "hostname":
|
||||||
|
*resolveAddrs = "name"
|
||||||
|
case "user", "tag", "usertag", "taguser":
|
||||||
|
*resolveAddrs = "user" // tag resolution is implied
|
||||||
|
default:
|
||||||
|
log.Fatalf("--resolve-addrs must be \"nodeId\", \"name\", or \"user\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
mustLoadTailnetNodes()
|
||||||
|
|
||||||
// The logic handles a stream of arbitrary JSON.
|
// The logic handles a stream of arbitrary JSON.
|
||||||
// So long as a JSON object seems like a network log message,
|
// So long as a JSON object seems like a network log message,
|
||||||
@ -103,7 +129,7 @@ func processArray(dec *jsontext.Decoder) {
|
|||||||
|
|
||||||
func processObject(dec *jsontext.Decoder) {
|
func processObject(dec *jsontext.Decoder) {
|
||||||
var hasTraffic bool
|
var hasTraffic bool
|
||||||
var rawMsg []byte
|
var rawMsg jsontext.Value
|
||||||
try.E1(dec.ReadToken()) // parse '{'
|
try.E1(dec.ReadToken()) // parse '{'
|
||||||
for dec.PeekKind() != '}' {
|
for dec.PeekKind() != '}' {
|
||||||
// Capture any members that could belong to a network log message.
|
// Capture any members that could belong to a network log message.
|
||||||
@ -111,13 +137,13 @@ func processObject(dec *jsontext.Decoder) {
|
|||||||
case "virtualTraffic", "subnetTraffic", "exitTraffic", "physicalTraffic":
|
case "virtualTraffic", "subnetTraffic", "exitTraffic", "physicalTraffic":
|
||||||
hasTraffic = true
|
hasTraffic = true
|
||||||
fallthrough
|
fallthrough
|
||||||
case "logtail", "nodeId", "logged", "start", "end":
|
case "logtail", "nodeId", "logged", "srcNode", "dstNodes", "start", "end":
|
||||||
if len(rawMsg) == 0 {
|
if len(rawMsg) == 0 {
|
||||||
rawMsg = append(rawMsg, '{')
|
rawMsg = append(rawMsg, '{')
|
||||||
} else {
|
} else {
|
||||||
rawMsg = append(rawMsg[:len(rawMsg)-1], ',')
|
rawMsg = append(rawMsg[:len(rawMsg)-1], ',')
|
||||||
}
|
}
|
||||||
rawMsg = append(append(append(rawMsg, '"'), name.String()...), '"')
|
rawMsg, _ = jsontext.AppendQuote(rawMsg, name.String())
|
||||||
rawMsg = append(rawMsg, ':')
|
rawMsg = append(rawMsg, ':')
|
||||||
rawMsg = append(rawMsg, try.E1(dec.ReadValue())...)
|
rawMsg = append(rawMsg, try.E1(dec.ReadValue())...)
|
||||||
rawMsg = append(rawMsg, '}')
|
rawMsg = append(rawMsg, '}')
|
||||||
@ -145,6 +171,32 @@ type message struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printMessage(msg message) {
|
func printMessage(msg message) {
|
||||||
|
var nodesByAddr map[netip.Addr]netlogtype.Node
|
||||||
|
var tailnetDNS string // e.g., ".acme-corp.ts.net"
|
||||||
|
if *resolveAddrs != "" {
|
||||||
|
nodesByAddr = make(map[netip.Addr]netlogtype.Node)
|
||||||
|
insertNode := func(node netlogtype.Node) {
|
||||||
|
for _, addr := range node.Addresses {
|
||||||
|
nodesByAddr[addr] = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, node := range msg.DstNodes {
|
||||||
|
insertNode(node)
|
||||||
|
}
|
||||||
|
insertNode(msg.SrcNode)
|
||||||
|
|
||||||
|
// Derive the Tailnet DNS of the self node.
|
||||||
|
detectTailnetDNS := func(nodeName string) {
|
||||||
|
if prefix, ok := strings.CutSuffix(nodeName, ".ts.net"); ok {
|
||||||
|
if i := strings.LastIndexByte(prefix, '.'); i > 0 {
|
||||||
|
tailnetDNS = nodeName[i:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detectTailnetDNS(msg.SrcNode.Name)
|
||||||
|
detectTailnetDNS(tailnetNodesByID[msg.NodeID].Name)
|
||||||
|
}
|
||||||
|
|
||||||
// Construct a table of network traffic per connection.
|
// Construct a table of network traffic per connection.
|
||||||
rows := [][7]string{{3: "Tx[P/s]", 4: "Tx[B/s]", 5: "Rx[P/s]", 6: "Rx[B/s]"}}
|
rows := [][7]string{{3: "Tx[P/s]", 4: "Tx[B/s]", 5: "Rx[P/s]", 6: "Rx[B/s]"}}
|
||||||
duration := msg.End.Sub(msg.Start)
|
duration := msg.End.Sub(msg.Start)
|
||||||
@ -175,16 +227,25 @@ func printMessage(msg message) {
|
|||||||
if !a.IsValid() {
|
if !a.IsValid() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if name, ok := namesByAddr[a.Addr()]; ok {
|
name := a.Addr().String()
|
||||||
if a.Port() == 0 {
|
node, ok := tailnetNodesByAddr[a.Addr()]
|
||||||
return name
|
if !ok {
|
||||||
|
node, ok = nodesByAddr[a.Addr()]
|
||||||
}
|
}
|
||||||
|
if ok {
|
||||||
|
switch *resolveAddrs {
|
||||||
|
case "nodeid":
|
||||||
|
name = cmp.Or(string(node.NodeID), name)
|
||||||
|
case "name":
|
||||||
|
name = cmp.Or(strings.TrimSuffix(string(node.Name), tailnetDNS), name)
|
||||||
|
case "user":
|
||||||
|
name = cmp.Or(bools.IfElse(len(node.Tags) > 0, fmt.Sprint(node.Tags), node.User), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.Port() != 0 {
|
||||||
return name + ":" + strconv.Itoa(int(a.Port()))
|
return name + ":" + strconv.Itoa(int(a.Port()))
|
||||||
}
|
}
|
||||||
if a.Port() == 0 {
|
return name
|
||||||
return a.Addr().String()
|
|
||||||
}
|
|
||||||
return a.String()
|
|
||||||
}
|
}
|
||||||
for _, cc := range traffic {
|
for _, cc := range traffic {
|
||||||
row := [7]string{
|
row := [7]string{
|
||||||
@ -279,8 +340,10 @@ func printMessage(msg message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustMakeNamesByAddr() map[netip.Addr]string {
|
func mustLoadTailnetNodes() {
|
||||||
switch {
|
switch {
|
||||||
|
case *apiKey == "" && *tailnetName == "":
|
||||||
|
return // rely on embedded node information in the logs themselves
|
||||||
case *apiKey == "":
|
case *apiKey == "":
|
||||||
log.Fatalf("--api-key must be specified with --resolve-names")
|
log.Fatalf("--api-key must be specified with --resolve-names")
|
||||||
case *tailnetName == "":
|
case *tailnetName == "":
|
||||||
@ -300,57 +363,19 @@ func mustMakeNamesByAddr() map[netip.Addr]string {
|
|||||||
|
|
||||||
// Unmarshal the API response.
|
// Unmarshal the API response.
|
||||||
var m struct {
|
var m struct {
|
||||||
Devices []struct {
|
Devices []netlogtype.Node `json:"devices"`
|
||||||
Name string `json:"name"`
|
|
||||||
Addrs []netip.Addr `json:"addresses"`
|
|
||||||
} `json:"devices"`
|
|
||||||
}
|
}
|
||||||
must.Do(json.Unmarshal(b, &m))
|
must.Do(json.Unmarshal(b, &m))
|
||||||
|
|
||||||
// Construct a unique mapping of Tailscale IP addresses to hostnames.
|
// Construct a mapping of Tailscale IP addresses to node information.
|
||||||
// For brevity, we start with the first segment of the name and
|
tailnetNodesByAddr = make(map[netip.Addr]netlogtype.Node)
|
||||||
// use more segments until we find the shortest prefix that is unique
|
tailnetNodesByID = make(map[tailcfg.StableNodeID]netlogtype.Node)
|
||||||
// for all names in the tailnet.
|
for _, node := range m.Devices {
|
||||||
seen := make(map[string]bool)
|
for _, addr := range node.Addresses {
|
||||||
namesByAddr := make(map[netip.Addr]string)
|
tailnetNodesByAddr[addr] = node
|
||||||
retry:
|
|
||||||
for i := range 10 {
|
|
||||||
clear(seen)
|
|
||||||
clear(namesByAddr)
|
|
||||||
for _, d := range m.Devices {
|
|
||||||
name := fieldPrefix(d.Name, i)
|
|
||||||
if seen[name] {
|
|
||||||
continue retry
|
|
||||||
}
|
}
|
||||||
seen[name] = true
|
tailnetNodesByID[node.NodeID] = node
|
||||||
for _, a := range d.Addrs {
|
|
||||||
namesByAddr[a] = name
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return namesByAddr
|
|
||||||
}
|
|
||||||
panic("unable to produce unique mapping of address to names")
|
|
||||||
}
|
|
||||||
|
|
||||||
// fieldPrefix returns the first n number of dot-separated segments.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// fieldPrefix("foo.bar.baz", 0) returns ""
|
|
||||||
// fieldPrefix("foo.bar.baz", 1) returns "foo"
|
|
||||||
// fieldPrefix("foo.bar.baz", 2) returns "foo.bar"
|
|
||||||
// fieldPrefix("foo.bar.baz", 3) returns "foo.bar.baz"
|
|
||||||
// fieldPrefix("foo.bar.baz", 4) returns "foo.bar.baz"
|
|
||||||
func fieldPrefix(s string, n int) string {
|
|
||||||
s0 := s
|
|
||||||
for i := 0; i < n && len(s) > 0; i++ {
|
|
||||||
if j := strings.IndexByte(s, '.'); j >= 0 {
|
|
||||||
s = s[j+1:]
|
|
||||||
} else {
|
|
||||||
s = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.TrimSuffix(s0[:len(s0)-len(s)], ".")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendRepeatByte(b []byte, c byte, n int) []byte {
|
func appendRepeatByte(b []byte, c byte, n int) []byte {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user