Skip to content

Commit dcf2059

Browse files
committed
Update Dockerfile and backend dependencies; add NetworkGraph component for visualizing network topology
- Updated Dockerfile to use Go 1.24-alpine. - Updated Go module version to 1.24.0 and added new indirect dependencies. - Refactored Tailscale service to utilize the new Tailscale client for device and network log retrieval. - Introduced a new NetworkGraph component in the frontend for visualizing network nodes and links, replacing the previous D3 implementation in NetworkView. - Enhanced NetworkView to integrate the new NetworkGraph component and manage node/link selection.
1 parent 7fa3eca commit dcf2059

File tree

7 files changed

+488
-366
lines changed

7 files changed

+488
-366
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ COPY frontend/ ./
1212
RUN npm run build
1313

1414
# Backend build stage
15-
FROM golang:1.23-alpine AS backend-build
15+
FROM golang:1.24-alpine AS backend-build
1616

1717
WORKDIR /app/backend
1818

backend/backend

339 KB
Binary file not shown.

backend/go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/rajsinghtech/tsflow/backend
22

3-
go 1.23.0
3+
go 1.24.0
44

55
toolchain go1.24.5
66

@@ -29,13 +29,16 @@ require (
2929
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3030
github.com/modern-go/reflect2 v1.0.2 // indirect
3131
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
32+
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 // indirect
3233
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
3334
github.com/ugorji/go/codec v1.2.12 // indirect
3435
golang.org/x/arch v0.15.0 // indirect
3536
golang.org/x/crypto v0.36.0 // indirect
3637
golang.org/x/net v0.38.0 // indirect
3738
golang.org/x/sys v0.31.0 // indirect
3839
golang.org/x/text v0.23.0 // indirect
40+
google.golang.org/appengine v1.6.8 // indirect
3941
google.golang.org/protobuf v1.36.6 // indirect
4042
gopkg.in/yaml.v3 v3.0.1 // indirect
43+
tailscale.com/client/tailscale/v2 v2.0.0-20250820140259-740bf1718a90 // indirect
4144
)

backend/go.sum

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
2020
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
2121
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
2222
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
23-
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
24-
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
2523
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
2624
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
2725
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -40,6 +38,7 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
4038
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
4139
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
4240
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
41+
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
4342
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
4443
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4544
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -92,38 +91,60 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
9291
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
9392
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
9493
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
94+
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 h1:erxeiTyq+nw4Cz5+hLDkOwNF5/9IQWCQPv0gpb3+QHU=
95+
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
9596
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
9697
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
9798
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
9899
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
99100
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
100101
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
102+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
101103
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
102104
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
105+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
103106
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
107+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
104108
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
105109
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
110+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
111+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
106112
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
113+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
107114
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
108115
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
109116
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
110117
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
118+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
119+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
120+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
111121
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
112122
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
113123
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
114124
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
125+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
126+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
115127
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
116128
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
117129
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
118130
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
131+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
132+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
119133
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
120134
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
135+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
136+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
121137
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
122138
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
123139
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
140+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
141+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
142+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
124143
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
125144
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
145+
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
126146
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
147+
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
127148
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
128149
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
129150
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
@@ -138,3 +159,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
138159
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
139160
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
140161
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
162+
tailscale.com/client/tailscale/v2 v2.0.0-20250820140259-740bf1718a90 h1:1VhCUpCjAa/z5zB+1w+uqAsUmoPhDTOQUi9YKQFAIN8=
163+
tailscale.com/client/tailscale/v2 v2.0.0-20250820140259-740bf1718a90/go.mod h1:4akEJPbysqHWAP+t7CZLQ5ZH8/vZWeH6+Hv+fEJUMp0=

backend/internal/services/tailscale.go

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/rajsinghtech/tsflow/backend/internal/config"
1414
"github.com/rajsinghtech/tsflow/backend/internal/utils"
15+
tailscale "tailscale.com/client/tailscale/v2"
1516
"golang.org/x/oauth2/clientcredentials"
1617
)
1718

@@ -22,6 +23,7 @@ type TailscaleService struct {
2223
baseURL string
2324
client *http.Client
2425
useOAuth bool
26+
tsClient *tailscale.Client
2527
}
2628

2729
type Device struct {
@@ -73,16 +75,26 @@ func NewTailscaleService(cfg *config.Config) *TailscaleService {
7375
ClientID: cfg.TailscaleOAuthClientID,
7476
ClientSecret: cfg.TailscaleOAuthClientSecret,
7577
Scopes: cfg.TailscaleOAuthScopes,
76-
TokenURL: cfg.TailscaleAPIURL + "/api/v2/oauth/token",
78+
TokenURL: cfg.TailscaleAPIURL + "/oauth/token",
7779
}
78-
ts.client = ts.oauthConfig.Client(context.Background())
79-
ts.client.Timeout = 5 * time.Minute
80+
oauthClient := ts.oauthConfig.Client(context.Background())
81+
oauthClient.Timeout = 5 * time.Minute
82+
83+
ts.tsClient = &tailscale.Client{
84+
HTTP: oauthClient,
85+
Tailnet: cfg.TailscaleTailnet,
86+
}
87+
ts.client = oauthClient
8088
ts.useOAuth = true
8189
} else if cfg.TailscaleAPIKey != "" {
8290
ts.apiKey = cfg.TailscaleAPIKey
8391
ts.client = &http.Client{
8492
Timeout: 5 * time.Minute,
8593
}
94+
ts.tsClient = &tailscale.Client{
95+
APIKey: cfg.TailscaleAPIKey,
96+
Tailnet: cfg.TailscaleTailnet,
97+
}
8698
ts.useOAuth = false
8799
} else {
88100
ts.client = &http.Client{
@@ -175,6 +187,44 @@ func (ts *TailscaleService) isRetryableError(err error) bool {
175187
}
176188

177189
func (ts *TailscaleService) GetDevices() (*DevicesResponse, error) {
190+
if ts.tsClient != nil {
191+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
192+
defer cancel()
193+
194+
devices, err := ts.tsClient.Devices().List(ctx)
195+
if err != nil {
196+
return nil, fmt.Errorf("failed to get devices from tailscale client: %w", err)
197+
}
198+
199+
// Convert tailscale client devices to our format
200+
var ourDevices []Device
201+
for _, device := range devices {
202+
ourDevices = append(ourDevices, Device{
203+
ID: device.ID,
204+
Name: device.Name,
205+
Hostname: device.Hostname,
206+
User: device.User,
207+
OS: device.OS,
208+
Addresses: device.Addresses,
209+
Online: !device.LastSeen.IsZero() && time.Since(device.LastSeen.Time) < 2*time.Minute,
210+
LastSeen: device.LastSeen.Time.Format(time.RFC3339),
211+
Authorized: device.Authorized,
212+
KeyExpiryDisabled: device.KeyExpiryDisabled,
213+
Created: device.Created.Time.Format(time.RFC3339),
214+
MachineKey: device.MachineKey,
215+
NodeKey: device.NodeKey,
216+
ClientVersion: device.ClientVersion,
217+
UpdateAvailable: device.UpdateAvailable,
218+
Blocksincomingnonnodes: device.BlocksIncomingConnections,
219+
EnabledRoutes: device.EnabledRoutes,
220+
AdvertisedRoutes: device.AdvertisedRoutes,
221+
})
222+
}
223+
224+
return &DevicesResponse{Devices: ourDevices}, nil
225+
}
226+
227+
// Fallback to old implementation
178228
endpoint := fmt.Sprintf("/tailnet/%s/devices", ts.tailnet)
179229

180230
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
@@ -194,6 +244,40 @@ func (ts *TailscaleService) GetDevices() (*DevicesResponse, error) {
194244
}
195245

196246
func (ts *TailscaleService) GetNetworkLogs(start, end string) (interface{}, error) {
247+
if ts.tsClient != nil {
248+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
249+
defer cancel()
250+
251+
startTime, err := time.Parse(time.RFC3339, start)
252+
if err != nil {
253+
return nil, fmt.Errorf("invalid start time: %w", err)
254+
}
255+
256+
endTime, err := time.Parse(time.RFC3339, end)
257+
if err != nil {
258+
return nil, fmt.Errorf("invalid end time: %w", err)
259+
}
260+
261+
var logs []tailscale.NetworkFlowLog
262+
263+
err = ts.tsClient.Logging().GetNetworkFlowLogs(ctx, tailscale.NetworkFlowLogsRequest{
264+
Start: startTime,
265+
End: endTime,
266+
}, func(log tailscale.NetworkFlowLog) error {
267+
logs = append(logs, log)
268+
return nil
269+
})
270+
271+
if err != nil {
272+
return nil, fmt.Errorf("failed to fetch network logs from tailscale client: %w", err)
273+
}
274+
275+
return map[string]interface{}{
276+
"logs": logs,
277+
}, nil
278+
}
279+
280+
// Fallback to old implementation
197281
endpoint := fmt.Sprintf("/tailnet/%s/logging/network", ts.tailnet)
198282

199283
if start != "" && end != "" {

0 commit comments

Comments
 (0)