├── .env.sample ├── LICENSE ├── README.md ├── docker-compose.yml └── docker ├── grafana └── provisioning │ ├── dashboards │ ├── dashboard.yml │ └── tailscale_monitor.json │ ├── datasources │ └── influxdb.yml │ └── grafana-sample.png └── tailscale ├── Dockerfile ├── index.js ├── package-lock.json └── package.json /.env.sample: -------------------------------------------------------------------------------- 1 | GRAFANA_PORT=3000 2 | TAILSCALE_INTERVAL=3600 3 | TAILSCALE_HOST=local 4 | INFLUXDB_HOST=locahost 5 | TAILSCALE_SKEY=tsky-xxx-xxxxx 6 | TAILSCALE_USER=xxxx@xxxx.com 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Jonas Friedmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-tailscale-monitor 2 | 3 | ![](/docker/grafana/provisioning/grafana-sample.png) 4 | 5 | Docker setup consisting out of Grafana incl. a pre-configured dashboard, InfluxDB and Tailscale incl. a excelletn VPN network solution. 6 | 7 | ## Installation 8 | 9 | 1. Make sure you've installed all requirements 10 | 2. Clone this repository: 11 | 12 | ```shell 13 | git clone https://github.com/xinmans/docker-tailscale-monitor.git 14 | ``` 15 | 16 | 3. Create a copy of the sample `.env` file and adjust it at will: 17 | 18 | ```shell 19 | cd docker-tailscale-monitor 20 | cp .env.sample .env 21 | ``` 22 | 4. edit .env, change TAILSCALE_SKEY & TAILSCALE_USER to your value 23 | login https://login.tailscale.com/admin/settings/keys to enable the API keys 24 | 25 | ``` 26 | TAILSCALE_SKEY=tsky-xxx-xxxxx 27 | TAILSCALE_USER=xxxx@xxxx.com 28 | ``` 29 | 30 | 5. Spin up the containers: 31 | 32 | ```shell 33 | docker-compose up -d 34 | ``` 35 | 36 | ## Configuration 37 | 38 | You can make use of the following environment variables / configurations: 39 | 40 | | Environment variable | Default value | Description 41 | |----------------------|---------------|------------| 42 | | `GRAFANA_PORT` | `3000` | Port to bind Grafana webinterface on the host system | 43 | | `TAILSCALE_INTERVAL` | `3600` | Interval/pause (in seconds) between monitor | 44 | | `TAILSCALE_HOST` | `local` | Display name of the client | 45 | | `TAILSCALE_SKEY` | your tailscale skey | like tskey-XXXX-XXXXXXXX| 46 | | `TAILSCALE_USER` | your tailscale userid| email like xxx@xx.com| 47 | | `INFLUXDB_DB` | `tailscale` | Database to save tailscale results | 48 | | `INFLUXDB_HOST` | `influxdb` | Name of the InfluxDB host/containers | 49 | | `INFLUXDB_USERNAME` | `root` | Username for InfluxDB authentication | 50 | | `INFLUXDB_PASSWORD` | `root` | Password for InfluxDB authentication | 51 | 52 | ## Usage 53 | 54 | ### Services 55 | 56 | #### Start/create services 57 | 58 | 59 | ```shell 60 | $ docker-compose up -d 61 | Creating docker-tailscale_influxdb_1 ... done 62 | Creating docker-tailscale_grafana_1 ... done 63 | Creating docker-tailscale_monitor ... done 64 | 65 | ``` 66 | 67 | #### Stop services 68 | 69 | ```shell 70 | $ docker-compose stop 71 | Stopping docker-tailscale_influxdb_1 ... done 72 | Stopping docker-tailscale_grafana_1 ... done 73 | Stopping docker-tailscale_monitor ... done 74 | ``` 75 | 76 | #### Upgrade services 77 | 78 | ```shell 79 | $ docker-compose stop 80 | $ docker-compose pull 81 | $ docker-compose rm 82 | $ docker-compose up -d 83 | ``` 84 | 85 | #### Check logs 86 | 87 | ```shell 88 | $ docker-compose logs -f 89 | ``` 90 | 91 | ```shell 92 | $ docker-compose logs -f grafana 93 | ``` 94 | 95 | ### Grafana 96 | 97 | #### Dashboard 98 | 99 | By default the dashboard shows all speedtest results. To filter for a specifc host, simply add a `and host = 'local'` statement in the `WHERE` clause of the SQL select. 100 | 101 | Example (Download Time Serie): 102 | 103 | ``` 104 | SELECT hostname,os,id,clientVersion,addresses,Relay901latencyMs FROM "devicestatus" WHERE $timeFilter ORDER BY time DESC 105 | ``` 106 | 107 | #### Administrative access 108 | 109 | Access `http://${HOSTNAME}:${GRAFANA_PORT}` ([http://localhost:3000](`http://localhost:3000`) by default) and login using the following default credentials: 110 | 111 | * Username: `admin` 112 | * Password: `admin` 113 | 114 | ## Contributing 115 | 116 | 1. Fork it 117 | 2. Create your feature branch: 118 | 119 | ```shell 120 | git checkout -b feature/my-new-feature 121 | ``` 122 | 123 | 3. Commit your changes: 124 | 125 | ```shell 126 | git commit -am 'Add some feature' 127 | ``` 128 | 129 | 4. Push to the branch: 130 | 131 | ```shell 132 | git push origin feature/my-new-feature 133 | ``` 134 | 135 | 5. Submit a pull request 136 | 137 | ## Requirements / Dependencies 138 | 139 | * Docker (incl. `docker-compose`) 140 | 141 | ## Version 142 | 143 | 1.0.0 144 | 145 | ## License 146 | 147 | [MIT](LICENSE) 148 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | influxdb: 5 | restart: always 6 | image: influxdb:1.8 7 | ports: 8 | - "8086:8086" 9 | volumes: 10 | - "./data/influxdb/:/var/lib/influxdb" 11 | environment: 12 | - "INFLUXDB_ADMIN_USER=root" 13 | - "INFLUXDB_ADMIN_PASSWORD=root" 14 | - "INFLUXDB_DB=tailscale" 15 | 16 | grafana: 17 | restart: always 18 | image: grafana/grafana:6.7.2 19 | volumes: 20 | - "./docker/grafana/provisioning:/etc/grafana/provisioning" 21 | ports: 22 | - "${GRAFANA_PORT}:3000" 23 | environment: 24 | - "GF_SERVER_ROOT_URL=http://localhost" 25 | - "GF_SECURITY_ADMIN_PASSWORD=admin" 26 | - "GF_AUTH_ANONYMOUS_ENABLED=true" 27 | depends_on: 28 | - influxdb 29 | 30 | tailscalemonitor: 31 | restart: always 32 | image: tailscale-monitor:latest 33 | container_name: tailscale-monitor 34 | environment: 35 | - "TAILSCALE_INTERVAL=${TAILSCALE_INTERVAL}" 36 | - "STAILSCALE_HOST=${TAILSCALE_HOST}" 37 | - "INFLUXDB_HOST=${INFLUXDB_HOST}" 38 | depends_on: 39 | - influxdb 40 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'tailscale' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | allowUiUpdates: true 11 | options: 12 | path: /etc/grafana/provisioning/dashboards 13 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/dashboards/tailscale_monitor.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "$$hashKey": "object:51", 6 | "builtIn": 1, 7 | "datasource": "-- Grafana --", 8 | "enable": true, 9 | "hide": true, 10 | "iconColor": "rgba(0, 211, 255, 1)", 11 | "name": "Annotations & Alerts", 12 | "target": { 13 | "limit": 100, 14 | "matchAny": false, 15 | "tags": [], 16 | "type": "dashboard" 17 | }, 18 | "type": "dashboard" 19 | } 20 | ] 21 | }, 22 | "description": "api.tailscale.com results (host, id, os, version, derper latency)", 23 | "editable": true, 24 | "fiscalYearStartMonth": 0, 25 | "graphTooltip": 2, 26 | "id": 42, 27 | "iteration": 1645967789645, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "aliasColors": {}, 33 | "bars": false, 34 | "dashLength": 10, 35 | "dashes": false, 36 | "datasource": { 37 | "type": "influxdb", 38 | "uid": "2L-Vpefnz" 39 | }, 40 | "fieldConfig": { 41 | "defaults": { 42 | "displayName": "Latency(ms)", 43 | "links": [], 44 | "unit": "short" 45 | }, 46 | "overrides": [] 47 | }, 48 | "fill": 1, 49 | "fillGradient": 0, 50 | "gridPos": { 51 | "h": 8, 52 | "w": 24, 53 | "x": 0, 54 | "y": 0 55 | }, 56 | "hiddenSeries": false, 57 | "id": 4, 58 | "legend": { 59 | "alignAsTable": false, 60 | "avg": false, 61 | "current": false, 62 | "max": false, 63 | "min": false, 64 | "show": true, 65 | "total": false, 66 | "values": false 67 | }, 68 | "lines": true, 69 | "linewidth": 2, 70 | "links": [], 71 | "nullPointMode": "connected", 72 | "options": { 73 | "alertThreshold": true 74 | }, 75 | "percentage": false, 76 | "pluginVersion": "8.4.0-beta1", 77 | "pointradius": 5, 78 | "points": false, 79 | "renderer": "flot", 80 | "seriesOverrides": [], 81 | "spaceLength": 10, 82 | "stack": false, 83 | "steppedLine": false, 84 | "targets": [ 85 | { 86 | "$$hashKey": "object:547", 87 | "alias": "Download", 88 | "datasource": { 89 | "type": "influxdb", 90 | "uid": "2L-Vpefnz" 91 | }, 92 | "groupBy": [ 93 | { 94 | "params": [ 95 | "$__interval" 96 | ], 97 | "type": "time" 98 | }, 99 | { 100 | "params": [ 101 | "null" 102 | ], 103 | "type": "fill" 104 | } 105 | ], 106 | "measurement": "download", 107 | "orderByTime": "ASC", 108 | "policy": "default", 109 | "query": "SELECT hostname,Relay901latencyMs FROM \"devicestatus\"", 110 | "rawQuery": true, 111 | "refId": "A", 112 | "resultFormat": "table", 113 | "select": [ 114 | [ 115 | { 116 | "params": [ 117 | "value" 118 | ], 119 | "type": "field" 120 | }, 121 | { 122 | "params": [], 123 | "type": "mean" 124 | } 125 | ] 126 | ], 127 | "tags": [ 128 | { 129 | "key": "host", 130 | "operator": "=~", 131 | "value": "/^$host$/" 132 | } 133 | ] 134 | } 135 | ], 136 | "thresholds": [ 137 | { 138 | "$$hashKey": "object:195", 139 | "colorMode": "critical", 140 | "fill": false, 141 | "line": true, 142 | "op": "gt", 143 | "value": 200, 144 | "yaxis": "left" 145 | } 146 | ], 147 | "timeRegions": [], 148 | "title": "Relay#901 Latency(ms)", 149 | "tooltip": { 150 | "shared": true, 151 | "sort": 0, 152 | "value_type": "individual" 153 | }, 154 | "type": "graph", 155 | "xaxis": { 156 | "mode": "time", 157 | "show": true, 158 | "values": [] 159 | }, 160 | "yaxes": [ 161 | { 162 | "$$hashKey": "object:618", 163 | "format": "short", 164 | "logBase": 1, 165 | "show": true 166 | }, 167 | { 168 | "$$hashKey": "object:619", 169 | "format": "Mbits", 170 | "logBase": 1, 171 | "show": false 172 | } 173 | ], 174 | "yaxis": { 175 | "align": false 176 | } 177 | }, 178 | { 179 | "datasource": { 180 | "type": "influxdb", 181 | "uid": "2L-Vpefnz" 182 | }, 183 | "description": "Shows the client status", 184 | "fieldConfig": { 185 | "defaults": { 186 | "custom": { 187 | "align": "left", 188 | "displayMode": "color-text", 189 | "minWidth": 10 190 | }, 191 | "mappings": [], 192 | "thresholds": { 193 | "mode": "absolute", 194 | "steps": [ 195 | { 196 | "color": "green", 197 | "value": null 198 | } 199 | ] 200 | }, 201 | "unit": "none" 202 | }, 203 | "overrides": [ 204 | { 205 | "matcher": { 206 | "id": "byName", 207 | "options": "Ping" 208 | }, 209 | "properties": [ 210 | { 211 | "id": "unit", 212 | "value": "ms" 213 | }, 214 | { 215 | "id": "thresholds", 216 | "value": { 217 | "mode": "absolute", 218 | "steps": [ 219 | { 220 | "color": "green", 221 | "value": null 222 | }, 223 | { 224 | "color": "#EAB839", 225 | "value": 8 226 | }, 227 | { 228 | "color": "red", 229 | "value": 25 230 | } 231 | ] 232 | } 233 | }, 234 | { 235 | "id": "custom.displayMode", 236 | "value": "gradient-gauge" 237 | }, 238 | { 239 | "id": "max", 240 | "value": 30 241 | }, 242 | { 243 | "id": "custom.width", 244 | "value": 150 245 | } 246 | ] 247 | }, 248 | { 249 | "matcher": { 250 | "id": "byName", 251 | "options": "Test Count" 252 | }, 253 | "properties": [ 254 | { 255 | "id": "unit", 256 | "value": "none" 257 | }, 258 | { 259 | "id": "custom.displayMode", 260 | "value": "gradient-gauge" 261 | }, 262 | { 263 | "id": "max", 264 | "value": 25 265 | }, 266 | { 267 | "id": "custom.width", 268 | "value": 120 269 | } 270 | ] 271 | }, 272 | { 273 | "matcher": { 274 | "id": "byName", 275 | "options": "Location" 276 | }, 277 | "properties": [ 278 | { 279 | "id": "custom.displayMode", 280 | "value": "color-text" 281 | } 282 | ] 283 | }, 284 | { 285 | "matcher": { 286 | "id": "byName", 287 | "options": "Download Avg" 288 | }, 289 | "properties": [ 290 | { 291 | "id": "custom.displayMode", 292 | "value": "gradient-gauge" 293 | }, 294 | { 295 | "id": "thresholds", 296 | "value": { 297 | "mode": "absolute", 298 | "steps": [ 299 | { 300 | "color": "red", 301 | "value": null 302 | }, 303 | { 304 | "color": "yellow", 305 | "value": 650 306 | }, 307 | { 308 | "color": "green", 309 | "value": 800 310 | } 311 | ] 312 | } 313 | }, 314 | { 315 | "id": "min", 316 | "value": 600 317 | }, 318 | { 319 | "id": "max", 320 | "value": 950 321 | } 322 | ] 323 | }, 324 | { 325 | "matcher": { 326 | "id": "byName", 327 | "options": "Upload Avg" 328 | }, 329 | "properties": [ 330 | { 331 | "id": "custom.displayMode", 332 | "value": "gradient-gauge" 333 | }, 334 | { 335 | "id": "min", 336 | "value": 500 337 | }, 338 | { 339 | "id": "max", 340 | "value": 949 341 | }, 342 | { 343 | "id": "thresholds", 344 | "value": { 345 | "mode": "absolute", 346 | "steps": [ 347 | { 348 | "color": "red", 349 | "value": null 350 | }, 351 | { 352 | "color": "yellow", 353 | "value": 650 354 | }, 355 | { 356 | "color": "green", 357 | "value": 800 358 | } 359 | ] 360 | } 361 | }, 362 | { 363 | "id": "custom.width", 364 | "value": 171 365 | } 366 | ] 367 | } 368 | ] 369 | }, 370 | "gridPos": { 371 | "h": 8, 372 | "w": 24, 373 | "x": 0, 374 | "y": 8 375 | }, 376 | "id": 114, 377 | "options": { 378 | "footer": { 379 | "fields": "", 380 | "reducer": [ 381 | "sum" 382 | ], 383 | "show": false 384 | }, 385 | "showHeader": true, 386 | "sortBy": [ 387 | { 388 | "desc": true, 389 | "displayName": "Relay901latencyMs" 390 | } 391 | ] 392 | }, 393 | "pluginVersion": "8.4.0-beta1", 394 | "targets": [ 395 | { 396 | "datasource": { 397 | "type": "influxdb", 398 | "uid": "2L-Vpefnz" 399 | }, 400 | "groupBy": [ 401 | { 402 | "params": [ 403 | "server_name" 404 | ], 405 | "type": "tag" 406 | }, 407 | { 408 | "params": [ 409 | "server_location" 410 | ], 411 | "type": "tag" 412 | }, 413 | { 414 | "params": [ 415 | "server_id" 416 | ], 417 | "type": "tag" 418 | } 419 | ], 420 | "measurement": "download", 421 | "orderByTime": "ASC", 422 | "policy": "default", 423 | "query": "SELECT hostname,os,id,clientVersion,addresses,Relay901latencyMs FROM \"devicestatus\" WHERE $timeFilter ORDER BY time DESC", 424 | "rawQuery": true, 425 | "refId": "A", 426 | "resultFormat": "table", 427 | "select": [ 428 | [ 429 | { 430 | "params": [ 431 | "value" 432 | ], 433 | "type": "field" 434 | }, 435 | { 436 | "params": [], 437 | "type": "mean" 438 | }, 439 | { 440 | "params": [ 441 | "Download Avg" 442 | ], 443 | "type": "alias" 444 | } 445 | ], 446 | [ 447 | { 448 | "params": [ 449 | "value" 450 | ], 451 | "type": "field" 452 | }, 453 | { 454 | "params": [], 455 | "type": "count" 456 | }, 457 | { 458 | "params": [ 459 | "Test Count" 460 | ], 461 | "type": "alias" 462 | } 463 | ] 464 | ], 465 | "tags": [] 466 | } 467 | ], 468 | "title": "Tailscale Derper Status", 469 | "transformations": [ 470 | { 471 | "id": "seriesToColumns", 472 | "options": { 473 | "byField": "server_id" 474 | } 475 | }, 476 | { 477 | "id": "organize", 478 | "options": { 479 | "excludeByName": { 480 | "Time": true, 481 | "server_id": true 482 | }, 483 | "indexByName": { 484 | "Download Avg": 3, 485 | "Time": 2, 486 | "Upload Avg": 4, 487 | "server_location": 1, 488 | "server_name": 0 489 | }, 490 | "renameByName": { 491 | "Time": "", 492 | "server_location": "Location", 493 | "server_name": "Server" 494 | } 495 | } 496 | } 497 | ], 498 | "type": "table" 499 | } 500 | ], 501 | "refresh": "", 502 | "schemaVersion": 35, 503 | "style": "dark", 504 | "tags": [], 505 | "templating": { 506 | "list": [ 507 | { 508 | "current": { 509 | "isNone": true, 510 | "selected": false, 511 | "text": "None", 512 | "value": "" 513 | }, 514 | "datasource": { 515 | "type": "influxdb", 516 | "uid": "2L-Vpefnz" 517 | }, 518 | "definition": "show tag values from download with key=\"host\"", 519 | "hide": 0, 520 | "includeAll": false, 521 | "multi": false, 522 | "name": "host", 523 | "options": [], 524 | "query": "show tag values from download with key=\"host\"", 525 | "refresh": 1, 526 | "regex": "", 527 | "skipUrlSync": false, 528 | "sort": 0, 529 | "tagValuesQuery": "", 530 | "tagsQuery": "", 531 | "type": "query", 532 | "useTags": false 533 | } 534 | ] 535 | }, 536 | "time": { 537 | "from": "now-24h", 538 | "to": "now" 539 | }, 540 | "timepicker": { 541 | "refresh_intervals": [ 542 | "5s", 543 | "10s", 544 | "30s", 545 | "1m", 546 | "5m", 547 | "15m", 548 | "30m", 549 | "1h", 550 | "2h", 551 | "1d" 552 | ], 553 | "time_options": [ 554 | "5m", 555 | "15m", 556 | "1h", 557 | "6h", 558 | "12h", 559 | "24h", 560 | "2d", 561 | "7d", 562 | "30d" 563 | ] 564 | }, 565 | "timezone": "browser", 566 | "title": "Tailscale Monitor", 567 | "uid": "MVtdt6Bnz", 568 | "version": 17, 569 | "weekStart": "monday" 570 | } 571 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/datasources/influxdb.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: InfluxDB 5 | 6 | datasources: 7 | - name: InfluxDB 8 | type: influxdb 9 | access: proxy 10 | database: tailscale 11 | user: admin 12 | password: password 13 | url: http://influxdb:8086 14 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/grafana-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinmans/docker-tailscale-monitor/4fea88d61d2673d368e17ac23bc05d86b44f2187/docker/grafana/provisioning/grafana-sample.png -------------------------------------------------------------------------------- /docker/tailscale/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine as install-dependencies 2 | RUN apk add --no-cache npm 3 | WORKDIR /build 4 | COPY . . 5 | RUN npm ci 6 | 7 | FROM alpine as tailscale-monitor 8 | RUN apk add --no-cache nodejs curl 9 | LABEL maintainer="xinmans " 10 | 11 | WORKDIR /usr/src/app 12 | 13 | CMD /bin/sh 14 | CMD [ "node", "index.js" ] 15 | 16 | COPY --from=install-dependencies /build . 17 | -------------------------------------------------------------------------------- /docker/tailscale/index.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const Influx = require("influx"); 3 | const delay = require("delay"); 4 | const moment = require('moment'); 5 | const os = require("os"); 6 | 7 | process.env.INFLUXDB_HOST = (process.env.INFLUXDB_HOST) ? process.env.INFLUXDB_HOST : 'localhost'; 8 | process.env.INFLUXDB_DB = (process.env.INFLUXDB_DB) ? process.env.INFLUXDB_DB : 'tailscale'; 9 | process.env.INFLUXDB_USERNAME = (process.env.INFLUXDB_USERNAME) ? process.env.INFLUXDB_USERNAME : 'root'; 10 | process.env.INFLUXDB_PASSWORD = (process.env.INFLUXDB_PASSWORD) ? process.env.INFLUXDB_PASSWORD : 'root'; 11 | process.env.TAILSCALE_HOST = (process.env.TAILSCALE_HOST) ? process.env.TAILSCALE_HOST : 'ubuntu'; 12 | process.env.TAILSCALE_INTERVAL = (process.env.TAILSCALE_INTERVAL) ? process.env.TAILSCALE_INTERVAL : 3600; 13 | const TAILSCALE_SKEY = process.env.TAILSCALE_SKEY + ':'; 14 | const TAILSCALE_USER = process.env.TAILSCALE_USER; 15 | 16 | const log = (message, severity = "Info") => 17 | console.log(`[${severity.toUpperCase()}][${(moment(new Date).format("YYYY-MM-DD HH:mm:ss"))}] ${message}`); 18 | 19 | async function getLatencybyid(id) { 20 | const args = [ "https://api.tailscale.com/api/v2/device/" + id + "?fields=all", "-u", TAILSCALE_SKEY ]; 21 | var { stdout } = await execa.sync("curl", args); 22 | var result = JSON.parse(stdout); 23 | var latency = result.clientConnectivity.latency; 24 | var relay = Object.keys(latency)[0]; 25 | if (relay == "Relay #901") { 26 | var latencyMs = latency[Object.keys(latency)[0]].latencyMs; 27 | } else { 28 | var latencyMs = 0; 29 | } 30 | return { 31 | hostname: result.hostname, 32 | addresses: result.addresses[0], 33 | id: result.id, 34 | os: result.os, 35 | clientVersion: result.clientVersion, 36 | Relay901latencyMs: latencyMs.toFixed(2) 37 | }; 38 | }; 39 | 40 | async function getDeviceStatus(influx, ids){ 41 | await influx.getDatabaseNames().then(names => { 42 | if (!names.includes(process.env.INFLUXDB_DB)) { 43 | return influx.createDatabase(process.env.INFLUXDB_DB); 44 | } 45 | }); 46 | await ids.forEach(async function(id,index){ 47 | var res = await getLatencybyid(id) 48 | var tailscaleMetricsStr = JSON.stringify(res); 49 | var tailscaleMetrics = JSON.parse(tailscaleMetricsStr); 50 | log( 51 | `Tailscale results - hostname: ${tailscaleMetrics.hostname}, addresses: ${tailscaleMetrics.addresses}, id: ${tailscaleMetrics.id}, os: ${tailscaleMetrics.os}, clientVersion: ${tailscaleMetrics.clientVersion}, Relay901latencyMs: ${tailscaleMetrics.Relay901latencyMs}` 52 | ); 53 | await influx.writePoints([ 54 | { 55 | measurement: 'devicestatus', 56 | tags: { host: os.hostname() }, 57 | fields: { 58 | hostname: tailscaleMetrics.hostname, 59 | addresses: tailscaleMetrics.addresses, 60 | id: tailscaleMetrics.id, 61 | os: tailscaleMetrics.os, 62 | clientVersion: tailscaleMetrics.clientVersion, 63 | Relay901latencyMs: tailscaleMetrics.Relay901latencyMs 64 | }, 65 | } 66 | ]); 67 | 68 | await delay(process.env.TAILSCALE_INTERVAL * 60); 69 | }); 70 | }; 71 | 72 | const getDevices = async () => { 73 | var args = [ "https://api.tailscale.com/api/v2/tailnet/" + TAILSCALE_USER + "/devices", "-u", TAILSCALE_SKEY ]; 74 | var { stdout } = await execa("curl", args); 75 | var devices = JSON.parse(stdout).devices; 76 | var ids = []; 77 | devices.forEach(function(devices){ 78 | ids.push(devices['id']); 79 | }); 80 | return ids; 81 | }; 82 | 83 | 84 | (async () => { 85 | try { 86 | const influx = new Influx.InfluxDB({ 87 | host: process.env.INFLUXDB_HOST, 88 | database: process.env.INFLUXDB_DB, 89 | username: process.env.INFLUXDB_USERNAME, 90 | password: process.env.INFLUXDB_PASSWORD, 91 | schema: [ 92 | { 93 | measurement: 'devicestatus', 94 | fields: { 95 | hostname: Influx.FieldType.STRING, 96 | addresses: Influx.FieldType.STRING, 97 | id: Influx.FieldType.STRING, 98 | os: Influx.FieldType.STRING, 99 | clientVersion: Influx.FieldType.STRING, 100 | Relay901latencyMs: Influx.FieldType.FLOAT 101 | }, 102 | tags: [ 103 | 'host' 104 | ] 105 | } 106 | ] 107 | }); 108 | while (true) { 109 | log("Starting get tailscale derper status from api.tailscale.com ..."); 110 | const deviceids = await getDevices(); 111 | await getDeviceStatus(influx, deviceids); 112 | log(`Sleeping for ${process.env.TAILSCALE_INTERVAL} seconds...`); 113 | await delay(process.env.TAILSCALE_INTERVAL * 1000); 114 | } 115 | } catch (err) { 116 | console.error(err.message); 117 | process.exit(1); 118 | } 119 | })(); 120 | -------------------------------------------------------------------------------- /docker/tailscale/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailscale_monitor", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tailscale_monitor", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "delay": "^4.3.0", 13 | "execa": "^4.1.0", 14 | "influx": "^5.9.3", 15 | "moment": "^2.29.1", 16 | "os": "^0.1.2" 17 | } 18 | }, 19 | "node_modules/cross-spawn": { 20 | "version": "7.0.1", 21 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", 22 | "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", 23 | "dependencies": { 24 | "path-key": "^3.1.0", 25 | "shebang-command": "^2.0.0", 26 | "which": "^2.0.1" 27 | }, 28 | "engines": { 29 | "node": ">= 8" 30 | } 31 | }, 32 | "node_modules/delay": { 33 | "version": "4.3.0", 34 | "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz", 35 | "integrity": "sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA==", 36 | "engines": { 37 | "node": ">=6" 38 | } 39 | }, 40 | "node_modules/end-of-stream": { 41 | "version": "1.4.4", 42 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 43 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 44 | "dependencies": { 45 | "once": "^1.4.0" 46 | } 47 | }, 48 | "node_modules/execa": { 49 | "version": "4.1.0", 50 | "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", 51 | "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", 52 | "dependencies": { 53 | "cross-spawn": "^7.0.0", 54 | "get-stream": "^5.0.0", 55 | "human-signals": "^1.1.1", 56 | "is-stream": "^2.0.0", 57 | "merge-stream": "^2.0.0", 58 | "npm-run-path": "^4.0.0", 59 | "onetime": "^5.1.0", 60 | "signal-exit": "^3.0.2", 61 | "strip-final-newline": "^2.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=10" 65 | }, 66 | "funding": { 67 | "url": "https://github.com/sindresorhus/execa?sponsor=1" 68 | } 69 | }, 70 | "node_modules/get-stream": { 71 | "version": "5.1.0", 72 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", 73 | "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", 74 | "dependencies": { 75 | "pump": "^3.0.0" 76 | }, 77 | "engines": { 78 | "node": ">=8" 79 | } 80 | }, 81 | "node_modules/human-signals": { 82 | "version": "1.1.1", 83 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", 84 | "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", 85 | "engines": { 86 | "node": ">=8.12.0" 87 | } 88 | }, 89 | "node_modules/influx": { 90 | "version": "5.9.3", 91 | "resolved": "https://registry.npmjs.org/influx/-/influx-5.9.3.tgz", 92 | "integrity": "sha512-QQU9CgwnaEV6zMrK8+vhVItsdoKFqDioXJrjJhRQaff9utvT3N0jcrQJT9qnxFLktqgJ5ngbDY68Zh4eo4uD/w==" 93 | }, 94 | "node_modules/is-stream": { 95 | "version": "2.0.0", 96 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 97 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 98 | "engines": { 99 | "node": ">=8" 100 | } 101 | }, 102 | "node_modules/isexe": { 103 | "version": "2.0.0", 104 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 105 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 106 | }, 107 | "node_modules/merge-stream": { 108 | "version": "2.0.0", 109 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 110 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" 111 | }, 112 | "node_modules/mimic-fn": { 113 | "version": "2.1.0", 114 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 115 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 116 | "engines": { 117 | "node": ">=6" 118 | } 119 | }, 120 | "node_modules/moment": { 121 | "version": "2.29.1", 122 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", 123 | "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", 124 | "engines": { 125 | "node": "*" 126 | } 127 | }, 128 | "node_modules/npm-run-path": { 129 | "version": "4.0.1", 130 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 131 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 132 | "dependencies": { 133 | "path-key": "^3.0.0" 134 | }, 135 | "engines": { 136 | "node": ">=8" 137 | } 138 | }, 139 | "node_modules/once": { 140 | "version": "1.4.0", 141 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 142 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 143 | "dependencies": { 144 | "wrappy": "1" 145 | } 146 | }, 147 | "node_modules/onetime": { 148 | "version": "5.1.0", 149 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 150 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 151 | "dependencies": { 152 | "mimic-fn": "^2.1.0" 153 | }, 154 | "engines": { 155 | "node": ">=6" 156 | } 157 | }, 158 | "node_modules/os": { 159 | "version": "0.1.2", 160 | "resolved": "https://registry.npmjs.org/os/-/os-0.1.2.tgz", 161 | "integrity": "sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==" 162 | }, 163 | "node_modules/path-key": { 164 | "version": "3.1.1", 165 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 166 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 167 | "engines": { 168 | "node": ">=8" 169 | } 170 | }, 171 | "node_modules/pump": { 172 | "version": "3.0.0", 173 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 174 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 175 | "dependencies": { 176 | "end-of-stream": "^1.1.0", 177 | "once": "^1.3.1" 178 | } 179 | }, 180 | "node_modules/shebang-command": { 181 | "version": "2.0.0", 182 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 183 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 184 | "dependencies": { 185 | "shebang-regex": "^3.0.0" 186 | }, 187 | "engines": { 188 | "node": ">=8" 189 | } 190 | }, 191 | "node_modules/shebang-regex": { 192 | "version": "3.0.0", 193 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 194 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 195 | "engines": { 196 | "node": ">=8" 197 | } 198 | }, 199 | "node_modules/signal-exit": { 200 | "version": "3.0.2", 201 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 202 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 203 | }, 204 | "node_modules/strip-final-newline": { 205 | "version": "2.0.0", 206 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 207 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 208 | "engines": { 209 | "node": ">=6" 210 | } 211 | }, 212 | "node_modules/which": { 213 | "version": "2.0.2", 214 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 215 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 216 | "dependencies": { 217 | "isexe": "^2.0.0" 218 | }, 219 | "bin": { 220 | "node-which": "bin/node-which" 221 | }, 222 | "engines": { 223 | "node": ">= 8" 224 | } 225 | }, 226 | "node_modules/wrappy": { 227 | "version": "1.0.2", 228 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 229 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 230 | } 231 | }, 232 | "dependencies": { 233 | "cross-spawn": { 234 | "version": "7.0.1", 235 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", 236 | "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", 237 | "requires": { 238 | "path-key": "^3.1.0", 239 | "shebang-command": "^2.0.0", 240 | "which": "^2.0.1" 241 | } 242 | }, 243 | "delay": { 244 | "version": "4.3.0", 245 | "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz", 246 | "integrity": "sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA==" 247 | }, 248 | "end-of-stream": { 249 | "version": "1.4.4", 250 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 251 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 252 | "requires": { 253 | "once": "^1.4.0" 254 | } 255 | }, 256 | "execa": { 257 | "version": "4.1.0", 258 | "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", 259 | "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", 260 | "requires": { 261 | "cross-spawn": "^7.0.0", 262 | "get-stream": "^5.0.0", 263 | "human-signals": "^1.1.1", 264 | "is-stream": "^2.0.0", 265 | "merge-stream": "^2.0.0", 266 | "npm-run-path": "^4.0.0", 267 | "onetime": "^5.1.0", 268 | "signal-exit": "^3.0.2", 269 | "strip-final-newline": "^2.0.0" 270 | } 271 | }, 272 | "get-stream": { 273 | "version": "5.1.0", 274 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", 275 | "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", 276 | "requires": { 277 | "pump": "^3.0.0" 278 | } 279 | }, 280 | "human-signals": { 281 | "version": "1.1.1", 282 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", 283 | "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" 284 | }, 285 | "influx": { 286 | "version": "5.9.3", 287 | "resolved": "https://registry.npmjs.org/influx/-/influx-5.9.3.tgz", 288 | "integrity": "sha512-QQU9CgwnaEV6zMrK8+vhVItsdoKFqDioXJrjJhRQaff9utvT3N0jcrQJT9qnxFLktqgJ5ngbDY68Zh4eo4uD/w==" 289 | }, 290 | "is-stream": { 291 | "version": "2.0.0", 292 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 293 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" 294 | }, 295 | "isexe": { 296 | "version": "2.0.0", 297 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 298 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 299 | }, 300 | "merge-stream": { 301 | "version": "2.0.0", 302 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 303 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" 304 | }, 305 | "mimic-fn": { 306 | "version": "2.1.0", 307 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 308 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" 309 | }, 310 | "moment": { 311 | "version": "2.29.1", 312 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", 313 | "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" 314 | }, 315 | "npm-run-path": { 316 | "version": "4.0.1", 317 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 318 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 319 | "requires": { 320 | "path-key": "^3.0.0" 321 | } 322 | }, 323 | "once": { 324 | "version": "1.4.0", 325 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 326 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 327 | "requires": { 328 | "wrappy": "1" 329 | } 330 | }, 331 | "onetime": { 332 | "version": "5.1.0", 333 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 334 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 335 | "requires": { 336 | "mimic-fn": "^2.1.0" 337 | } 338 | }, 339 | "os": { 340 | "version": "0.1.2", 341 | "resolved": "https://registry.npmjs.org/os/-/os-0.1.2.tgz", 342 | "integrity": "sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==" 343 | }, 344 | "path-key": { 345 | "version": "3.1.1", 346 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 347 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 348 | }, 349 | "pump": { 350 | "version": "3.0.0", 351 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 352 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 353 | "requires": { 354 | "end-of-stream": "^1.1.0", 355 | "once": "^1.3.1" 356 | } 357 | }, 358 | "shebang-command": { 359 | "version": "2.0.0", 360 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 361 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 362 | "requires": { 363 | "shebang-regex": "^3.0.0" 364 | } 365 | }, 366 | "shebang-regex": { 367 | "version": "3.0.0", 368 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 369 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 370 | }, 371 | "signal-exit": { 372 | "version": "3.0.2", 373 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 374 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 375 | }, 376 | "strip-final-newline": { 377 | "version": "2.0.0", 378 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 379 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" 380 | }, 381 | "which": { 382 | "version": "2.0.2", 383 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 384 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 385 | "requires": { 386 | "isexe": "^2.0.0" 387 | } 388 | }, 389 | "wrappy": { 390 | "version": "1.0.2", 391 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 392 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /docker/tailscale/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailscale_monitor", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "xinmans", 10 | "license": "ISC", 11 | "dependencies": { 12 | "delay": "^4.3.0", 13 | "execa": "^4.1.0", 14 | "influx": "^5.9.3", 15 | "moment": "^2.29.1", 16 | "os": "^0.1.2" 17 | } 18 | } 19 | --------------------------------------------------------------------------------