You've already forked ionscale
mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-04-05 12:32:58 +01:00
Compare commits
195 Commits
v0.1.0-rc2
...
v0.15.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e7370d98a3 | |||
| 2811465206 | |||
| 6173621730 | |||
| a1debdffb8 | |||
| 42702682c9 | |||
| 248b75cd77 | |||
| 27c6a1fa12 | |||
| 62a7290e3d | |||
| 9a60430949 | |||
| d72ea03d9d | |||
| 94d9168eab | |||
| 72ed4c66e3 | |||
| 0ecd0050d0 | |||
| f1285fdc7e | |||
| 1ffafeea79 | |||
| 68127b9a98 | |||
| 69ce610579 | |||
| afe587cb03 | |||
| 91c62ee892 | |||
| 7aeed60fe1 | |||
| e39eb5824b | |||
| 7c2d5f723a | |||
| 84d29fda34 | |||
| 41b64eed71 | |||
| 0eef9faf86 | |||
| 271d99a3ce | |||
| cf8b2be0e8 | |||
| b098562988 | |||
| 46cce89e0e | |||
| 128ed22bde | |||
| 5d1ac326ea | |||
| 7eb808c71c | |||
| d8f0492940 | |||
| b8b1075389 | |||
| 9f3a6bbcec | |||
| cce0fd08b0 | |||
| 58634fc98e | |||
| 280ee7e1b6 | |||
| b8c752d04a | |||
| dfd2fe9fdd | |||
| 25203d3cca | |||
| 0f54539302 | |||
| dea60272b7 | |||
| 5e43014a09 | |||
| 9748955f18 | |||
| 44b6b20361 | |||
| cbde00c9f5 | |||
| 8f2c198bfe | |||
| 8f998b05f7 | |||
| 3fccde2932 | |||
| 7fa31bdf1f | |||
| 980ab1bc46 | |||
| 123ca99665 | |||
| 0c5e586cf9 | |||
| 79bc3bffb1 | |||
| 452c5ee516 | |||
| c1ea283e6d | |||
| 6a5d44882a | |||
| cbcbd61c3e | |||
| b083e2631a | |||
| 4587ed8eaa | |||
| 3118d2e573 | |||
| 7e1d90590d | |||
| 1b66b1e9be | |||
| 35e13a0698 | |||
| 951d0f299e | |||
| d10a022f29 | |||
| 9b5f045849 | |||
| 8a3f47490e | |||
| c76c2f16dd | |||
| dd2e783d8e | |||
| 473c3370ce | |||
| d6cc55cf5b | |||
| 9808860412 | |||
| 2bc03b895b | |||
| 54fa423acd | |||
| a303de71ee | |||
| cdbecf04fc | |||
| 75b58d0784 | |||
| 038c0afa8b | |||
| d9fafdcfd2 | |||
| 9b8782cccf | |||
| ea658a0e81 | |||
| e31ce67f84 | |||
| d5ca503318 | |||
| 4cab4dfb9a | |||
| 515f441dae | |||
| 9ac4c85c99 | |||
| 60a2faec4a | |||
| 339b9cfd37 | |||
| d0eac84271 | |||
| f193afa146 | |||
| cf67f6cf64 | |||
| 1ac3aa36ba | |||
| 9fd4e5fee4 | |||
| 326860c941 | |||
| 4ba540cb2c | |||
| 3577b8b46e | |||
| f24f0973fe | |||
| 12cad15a4e | |||
| d5c3c699dd | |||
| b3b21be50d | |||
| 051650ae4e | |||
| 2fc79ee0a1 | |||
| b7b3796ae6 | |||
| b0074152d1 | |||
| 4550bdbf2a | |||
| d32ece6304 | |||
| ef325dd936 | |||
| 9a55d67c7e | |||
| cbbaa31580 | |||
| 35c46eb2ec | |||
| d6a564b7a9 | |||
| 527fb34560 | |||
| 805a516626 | |||
| 0dbc81d50f | |||
| 40cc7b5648 | |||
| b62db084d1 | |||
| df23c178f9 | |||
| 9f7263abd5 | |||
| 660c684a13 | |||
| 790ef5fe1a | |||
| 61d9b40144 | |||
| a8e8d1aa49 | |||
| b2dbe3b9c5 | |||
| 8c6e9e00b9 | |||
| beb856a85d | |||
| 2345f0b1de | |||
| c8b040fcd6 | |||
| 5481d3bf4b | |||
| aac5414a21 | |||
| e74faa2605 | |||
| 9baf2ec6d1 | |||
| c73b7e13e0 | |||
| e41bac5a41 | |||
| 03abebb847 | |||
| 210cc9c8a2 | |||
| 9e38ffc44d | |||
| 06f02c1235 | |||
| 1de736144a | |||
| 2bfe95219d | |||
| e66fa7eabf | |||
| 4e96f2a5c3 | |||
| 43167c1fae | |||
| cf75b9240c | |||
| ab9439ecfe | |||
| 429798574d | |||
| aad7a8b6e8 | |||
| a2d97183d2 | |||
| af3a5f3a25 | |||
| fea6a10640 | |||
| 11af121126 | |||
| dfb91d2419 | |||
| daf577a0ee | |||
| a364188761 | |||
| ea4fe22e35 | |||
| ddc65d2df9 | |||
| c70a4cfe6a | |||
| 5bf919da12 | |||
| 6d4a7b7014 | |||
| bc1f188816 | |||
| 9522e3531e | |||
| 1e3541e7c8 | |||
| c3e1344199 | |||
| 70b9373df3 | |||
| 58de86a978 | |||
| 2e57338b54 | |||
| 7cadcc9085 | |||
| 22cfe60c7d | |||
| 45572397ea | |||
| e5a3d3c589 | |||
| 2a5fe7f136 | |||
| 7ee4b27688 | |||
| 69f7c22307 | |||
| 4e5f89ab7e | |||
| c1ffe03e81 | |||
| 7ad91c4c20 | |||
| fb04248db4 | |||
| d84bad12d0 | |||
| cadf938e2a | |||
| 980ae6dd85 | |||
| 6e3e22bc72 | |||
| 0051eec355 | |||
| 617575803c | |||
| 8c6ea9041b | |||
| c6ebeb36bc | |||
| d87c7252c2 | |||
| bfcf0c7925 | |||
| aea3d2d6a9 | |||
| 9781e75833 | |||
| 47b15d31f0 | |||
| ec353f7add | |||
| 92ca75b7f4 | |||
| 1702cf135e | |||
| b65119bbba |
@@ -0,0 +1,4 @@
|
||||
.git
|
||||
.idea
|
||||
tests
|
||||
!tests/config/ca.pem
|
||||
@@ -4,19 +4,34 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
buf-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Buf
|
||||
uses: bufbuild/buf-setup-action@v1
|
||||
- name: Buf Lint
|
||||
uses: bufbuild/buf-lint-action@v1
|
||||
with:
|
||||
input: proto
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
- name: Build
|
||||
run: |
|
||||
go test -v -short ./...
|
||||
go build cmd/ionscale/main.go
|
||||
@@ -0,0 +1,23 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths: ['mkdocs/**']
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material
|
||||
- run: cd mkdocs && mkdocs gh-deploy --force
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ts_version:
|
||||
- "v1.60"
|
||||
- "v1.58"
|
||||
- "v1.56"
|
||||
- "v1.54"
|
||||
- "v1.52"
|
||||
- "v1.50"
|
||||
- "v1.48"
|
||||
- "v1.46"
|
||||
- "v1.44"
|
||||
env:
|
||||
IONSCALE_TESTS_TS_TARGET_VERSION: ${{ matrix.ts_version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
go test -v ./tests
|
||||
@@ -1,42 +0,0 @@
|
||||
name: nightly
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v2.5.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --nightly --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
@@ -3,7 +3,7 @@ name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -29,17 +29,17 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v2.5.1
|
||||
uses: sigstore/cosign-installer@v3.1.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,48 @@
|
||||
name: "Security Analysis"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
trivy:
|
||||
name: Trivy
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: ghcr.io/jsiebens/ionscale:latest
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
- name: Upload Trivy scan results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 90 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+3
-4
@@ -1,8 +1,5 @@
|
||||
project_name: ionscale
|
||||
|
||||
nightly:
|
||||
name_template: '{{ incminor .Version }}-dev'
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@@ -44,7 +41,7 @@ docker_manifests:
|
||||
image_templates:
|
||||
- ghcr.io/jsiebens/{{ .ProjectName }}:{{ .Version }}-amd64
|
||||
- ghcr.io/jsiebens/{{ .ProjectName }}:{{ .Version }}-arm64
|
||||
- name_template: ghcr.io/jsiebens/{{ .ProjectName }}:{{ if .IsNightly }}dev{{ else }}latest{{ end }}
|
||||
- name_template: ghcr.io/jsiebens/{{ .ProjectName }}:latest
|
||||
image_templates:
|
||||
- ghcr.io/jsiebens/{{ .ProjectName }}:{{ .Version }}-amd64
|
||||
- ghcr.io/jsiebens/{{ .ProjectName }}:{{ .Version }}-arm64
|
||||
@@ -59,6 +56,7 @@ signs:
|
||||
- '--output-certificate=${certificate}'
|
||||
- '--output-signature=${signature}'
|
||||
- '${artifact}'
|
||||
- '--yes'
|
||||
artifacts: checksum
|
||||
|
||||
docker_signs:
|
||||
@@ -70,6 +68,7 @@ docker_signs:
|
||||
args:
|
||||
- sign
|
||||
- '${artifact}'
|
||||
- '--yes'
|
||||
|
||||
archives:
|
||||
- format: binary
|
||||
|
||||
+6
-1
@@ -1,3 +1,8 @@
|
||||
FROM alpine:3.16.2
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} alpine:3.19.1
|
||||
|
||||
COPY ionscale /usr/local/bin/ionscale
|
||||
|
||||
RUN mkdir -p /data/ionscale
|
||||
WORKDIR /data/ionscale
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/ionscale"]
|
||||
@@ -1,8 +1,19 @@
|
||||
init:
|
||||
go install github.com/a-h/templ/cmd/templ@latest
|
||||
go install github.com/bufbuild/buf/cmd/buf@latest
|
||||
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest
|
||||
|
||||
generate:
|
||||
buf generate proto
|
||||
templ generate
|
||||
buf generate proto
|
||||
|
||||
format:
|
||||
buf format -w proto
|
||||
|
||||
lint:
|
||||
buf lint proto
|
||||
|
||||
breaking:
|
||||
buf breaking proto --against https://github.com/jsiebens/ionscale.git#subdir=proto
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
# ionscale
|
||||
|
||||
> **Note**:
|
||||
> ionscale is currently alpha quality, actively being developed and so subject to changes
|
||||
> ionscale is currently beta quality, actively being developed and so subject to changes
|
||||
|
||||
**What is Tailscale?**
|
||||
|
||||
[Tailscale](https://tailscale.com) is a VPN service that makes the devices and applications you own accessible anywhere in the world, securely and effortlessly.
|
||||
It enables encrypted point-to-point connections using the open source [WireGuard](https://www.wireguard.com/) protocol, which means only devices on your private network can communicate with each other.
|
||||
|
||||
**What is ionscale?**
|
||||
|
||||
While the Tailscale software running on each node is open source, their centralized "coordination server" which act as a shared drop box for public keys is not.
|
||||
|
||||
_ionscale_ aims to implement such lightweight, open source alternative Tailscale control server.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
⭐ If you find _ionscale_ useful, please consider giving it a star on GitHub, or [drop a note](https://github.com/jsiebens/ionscale/discussions/new?category=show-and-tell) on how you are using _ionscale_.
|
||||
|
||||
## Features
|
||||
|
||||
- multi [tailnet](https://tailscale.com/kb/1136/tailnet/) support
|
||||
- multi user support
|
||||
- OIDC integration (not required, although recommended)
|
||||
- [Auth keys](https://tailscale.com/kb/1085/auth-keys/)
|
||||
- [Access control list](https://tailscale.com/kb/1018/acls/) (with support for `autogroups`, `tagOwners`, `autoApprovers`, `nodeAttrs`, `grants` ...)
|
||||
- [DNS](https://tailscale.com/kb/1054/dns/)
|
||||
- nameservers
|
||||
- Split DNS
|
||||
- MagicDNS
|
||||
- [Subnet routers](https://tailscale.com/kb/1019/subnets) and [Exit Nodes](https://tailscale.com/kb/1103/exit-nodes)
|
||||
- [HTTPS Certs](https://tailscale.com/kb/1153/enabling-https/)
|
||||
- [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh/)
|
||||
- [Tailscale Serve](https://tailscale.com/kb/1312/serve)
|
||||
- [Service collection](https://tailscale.com/kb/1100/services/)
|
||||
- [Taildrop](https://tailscale.com/kb/1106/taildrop/)
|
||||
|
||||
## Documentation
|
||||
|
||||
Some documentation can be found [here](https://jsiebens.github.io/ionscale)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is not an official Tailscale or Tailscale Inc. project.
|
||||
@@ -1,60 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/muesli/coral"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := rootCommand()
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func rootCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "pg-ionscale-events",
|
||||
}
|
||||
|
||||
var url string
|
||||
command.Flags().StringVar(&url, "url", "", "")
|
||||
_ = command.MarkFlagRequired("url")
|
||||
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
_, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reportProblem := func(ev pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
minReconn := 10 * time.Second
|
||||
maxReconn := time.Minute
|
||||
listener := pq.NewListener(url, minReconn, maxReconn, reportProblem)
|
||||
err = listener.Listen("ionscale_events")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("listening for events ...")
|
||||
fmt.Println("")
|
||||
for {
|
||||
select {
|
||||
case n, _ := <-listener.Notify:
|
||||
fmt.Println(n.Extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -1,109 +1,220 @@
|
||||
module github.com/jsiebens/ionscale
|
||||
|
||||
go 1.19
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/99designs/keyring v1.2.2
|
||||
github.com/a-h/templ v0.2.543
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/bufbuild/connect-go v0.4.0
|
||||
github.com/caddyserver/certmagic v0.17.1
|
||||
github.com/coreos/go-oidc/v3 v3.3.0
|
||||
github.com/glebarez/sqlite v1.4.6
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.0.2
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-bexpr v0.1.11
|
||||
github.com/hashicorp/go-hclog v1.3.0
|
||||
github.com/bufbuild/connect-go v1.10.0
|
||||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/caddyserver/certmagic v0.20.0
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.1
|
||||
github.com/go-jose/go-jose/v3 v3.0.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/hashicorp/go-bexpr v0.1.13
|
||||
github.com/hashicorp/go-getter v1.7.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/klauspost/compress v1.15.9
|
||||
github.com/labstack/echo-contrib v0.13.0
|
||||
github.com/labstack/echo/v4 v4.9.0
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/imdario/mergo v0.3.16
|
||||
github.com/jsiebens/go-edit v0.1.0
|
||||
github.com/jsiebens/mockoidc v0.1.0-rc2
|
||||
github.com/klauspost/compress v1.17.4
|
||||
github.com/labstack/echo-contrib v0.15.0
|
||||
github.com/labstack/echo/v4 v4.11.4
|
||||
github.com/libdns/azure v0.3.0
|
||||
github.com/libdns/cloudflare v0.1.0
|
||||
github.com/libdns/digitalocean v0.0.0-20230728223659-4f9064657aea
|
||||
github.com/libdns/googleclouddns v1.1.0
|
||||
github.com/libdns/libdns v0.2.1
|
||||
github.com/libdns/route53 v1.3.3
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/pointerstructure v1.2.1
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/muesli/coral v1.0.0
|
||||
github.com/nleeper/goment v1.4.4
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/rodaine/table v1.0.1
|
||||
github.com/sony/sonyflake v1.1.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/xhit/go-str2duration/v2 v2.0.0
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||
google.golang.org/protobuf v1.28.1
|
||||
github.com/ory/dockertest/v3 v3.10.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/rodaine/table v1.1.0
|
||||
github.com/sony/sonyflake v1.2.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
||||
github.com/travisjeffery/certmagic-sqlstorage v1.1.1
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/sync v0.6.0
|
||||
google.golang.org/protobuf v1.32.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.3.9
|
||||
gorm.io/gorm v1.23.8
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||
tailscale.com v1.30.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
gorm.io/plugin/prometheus v0.1.0
|
||||
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a
|
||||
tailscale.com v1.56.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||
cloud.google.com/go v0.111.0 // indirect
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.5 // indirect
|
||||
cloud.google.com/go/storage v1.30.1 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.122 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.36.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/continuity v0.4.3 // indirect
|
||||
github.com/coreos/go-iptables v0.7.0 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.18.1 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240108175832-49174e152ce1 // indirect
|
||||
github.com/digitalocean/godo v1.107.0 // indirect
|
||||
github.com/docker/cli v25.0.2+incompatible // indirect
|
||||
github.com/docker/docker v25.0.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/native v1.0.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.2.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
github.com/libdns/libdns v0.2.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/lib/pq v1.10.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mdlayher/netlink v1.6.0 // indirect
|
||||
github.com/mdlayher/socket v0.2.3 // indirect
|
||||
github.com/mholt/acmez v1.0.4 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mholt/acmez v1.2.0 // indirect
|
||||
github.com/miekg/dns v1.1.57 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
|
||||
github.com/opencontainers/runc v1.1.12 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect
|
||||
github.com/tcnksm/go-httpstat v0.2.0 // indirect
|
||||
github.com/tkuchiki/go-timezone v0.2.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
go4.org/intern v0.0.0-20220617035311-6925f38cc365 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||
go.opentelemetry.io/otel v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
modernc.org/libc v1.18.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.3.0 // indirect
|
||||
modernc.org/sqlite v1.18.1 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/api v0.155.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/grpc v1.60.1 // indirect
|
||||
gotest.tools/v3 v3.4.0 // indirect
|
||||
modernc.org/libc v1.40.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
nhooyr.io/websocket v1.8.10 // indirect
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package provider
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -42,7 +42,7 @@ func (p *OIDCProvider) GetLoginURL(redirectURI, state string) string {
|
||||
ClientSecret: p.clientSecret,
|
||||
RedirectURL: redirectURI,
|
||||
Endpoint: p.provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Scopes: p.scopes,
|
||||
}
|
||||
|
||||
return oauth2Config.AuthCodeURL(state, oauth2.ApprovalForce)
|
||||
@@ -54,7 +54,7 @@ func (p *OIDCProvider) Exchange(redirectURI, code string) (*User, error) {
|
||||
ClientSecret: p.clientSecret,
|
||||
RedirectURL: redirectURI,
|
||||
Endpoint: p.provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Scopes: p.scopes,
|
||||
}
|
||||
|
||||
oauth2Token, err := oauth2Config.Exchange(context.Background(), code)
|
||||
@@ -85,10 +85,14 @@ func (p *OIDCProvider) Exchange(redirectURI, code string) (*User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domain := strings.Split(email, "@")[1]
|
||||
|
||||
return &User{
|
||||
ID: sub,
|
||||
Name: email,
|
||||
Attr: map[string]interface{}{
|
||||
"email": email,
|
||||
"domain": domain,
|
||||
"token": tokenClaims,
|
||||
"userinfo": userInfoClaims,
|
||||
},
|
||||
@@ -1,6 +1,6 @@
|
||||
package provider
|
||||
package auth
|
||||
|
||||
type AuthProvider interface {
|
||||
type Provider interface {
|
||||
GetLoginURL(redirectURI, state string) string
|
||||
Exchange(redirectURI, code string) (*User, error)
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package bind
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/labstack/echo/v4"
|
||||
"io/ioutil"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
type Factory func(c echo.Context) (Binder, error)
|
||||
|
||||
type Binder interface {
|
||||
BindRequest(c echo.Context, v interface{}) error
|
||||
WriteResponse(c echo.Context, code int, v interface{}) error
|
||||
Marshal(compress string, v interface{}) ([]byte, error)
|
||||
Peer() key.MachinePublic
|
||||
}
|
||||
|
||||
func DefaultBinder(machineKey key.MachinePublic) Factory {
|
||||
return func(c echo.Context) (Binder, error) {
|
||||
return &defaultBinder{machineKey: machineKey}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func BoxBinder(controlKey key.MachinePrivate) Factory {
|
||||
return func(c echo.Context) (Binder, error) {
|
||||
idParam := c.Param("id")
|
||||
|
||||
id, err := util.ParseMachinePublicKey(idParam)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &boxBinder{
|
||||
controlKey: controlKey,
|
||||
machineKey: *id,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type defaultBinder struct {
|
||||
machineKey key.MachinePublic
|
||||
}
|
||||
|
||||
func (d *defaultBinder) BindRequest(c echo.Context, v interface{}) error {
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
|
||||
func (d *defaultBinder) WriteResponse(c echo.Context, code int, v interface{}) error {
|
||||
marshalled, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Response().WriteHeader(code)
|
||||
_, err = c.Response().Write(marshalled)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *defaultBinder) Marshal(compress string, v interface{}) ([]byte, error) {
|
||||
var payload []byte
|
||||
|
||||
marshalled, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if compress == "zstd" {
|
||||
encoder, err := zstd.NewWriter(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload = encoder.EncodeAll(marshalled, nil)
|
||||
} else {
|
||||
payload = marshalled
|
||||
}
|
||||
|
||||
data := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(data, uint32(len(payload)))
|
||||
data = append(data, payload...)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (d *defaultBinder) Peer() key.MachinePublic {
|
||||
return d.machineKey
|
||||
}
|
||||
|
||||
type boxBinder struct {
|
||||
controlKey key.MachinePrivate
|
||||
machineKey key.MachinePublic
|
||||
}
|
||||
|
||||
func (b *boxBinder) BindRequest(c echo.Context, v interface{}) error {
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decrypted, ok := b.controlKey.OpenFrom(b.machineKey, body)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to decrypt payload")
|
||||
}
|
||||
|
||||
return json.Unmarshal(decrypted, v)
|
||||
}
|
||||
|
||||
func (b *boxBinder) WriteResponse(c echo.Context, code int, v interface{}) error {
|
||||
marshalled, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encrypted := b.controlKey.SealTo(b.machineKey, marshalled)
|
||||
|
||||
c.Response().WriteHeader(code)
|
||||
_, err = c.Response().Write(encrypted)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *boxBinder) Marshal(compress string, v interface{}) ([]byte, error) {
|
||||
var payload []byte
|
||||
|
||||
marshalled, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if compress == "zstd" {
|
||||
encoder, err := zstd.NewWriter(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoded := encoder.EncodeAll(marshalled, nil)
|
||||
payload = b.controlKey.SealTo(b.machineKey, encoded)
|
||||
} else {
|
||||
payload = b.controlKey.SealTo(b.machineKey, marshalled)
|
||||
}
|
||||
|
||||
data := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(data, uint32(len(payload)))
|
||||
data = append(data, payload...)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (b *boxBinder) Peer() key.MachinePublic {
|
||||
return b.machineKey
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package broker
|
||||
@@ -1,16 +0,0 @@
|
||||
package broker
|
||||
|
||||
type Signal struct {
|
||||
PeerUpdated *uint64
|
||||
PeersRemoved []uint64
|
||||
ACLUpdated bool
|
||||
DNSUpdated bool
|
||||
}
|
||||
|
||||
type Listener chan *Signal
|
||||
|
||||
type Pubsub interface {
|
||||
Subscribe(tailnet uint64, listener Listener) (cancel func(), err error)
|
||||
Publish(tailnet uint64, message *Signal) error
|
||||
Close() error
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type memoryPubsub struct {
|
||||
mut sync.RWMutex
|
||||
listeners map[uint64]map[uuid.UUID]Listener
|
||||
}
|
||||
|
||||
func (m *memoryPubsub) Subscribe(tailnet uint64, listener Listener) (cancel func(), err error) {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
var listeners map[uuid.UUID]Listener
|
||||
var ok bool
|
||||
if listeners, ok = m.listeners[tailnet]; !ok {
|
||||
listeners = map[uuid.UUID]Listener{}
|
||||
m.listeners[tailnet] = listeners
|
||||
}
|
||||
var id uuid.UUID
|
||||
for {
|
||||
id = uuid.New()
|
||||
if _, ok = listeners[id]; !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
listeners[id] = listener
|
||||
return func() {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
listeners := m.listeners[tailnet]
|
||||
delete(listeners, id)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *memoryPubsub) Publish(tailnet uint64, message *Signal) error {
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
listeners, ok := m.listeners[tailnet]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, listener := range listeners {
|
||||
listener <- message
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*memoryPubsub) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPubsubInMemory() Pubsub {
|
||||
return &memoryPubsub{
|
||||
listeners: make(map[uint64]map[uuid.UUID]Listener),
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pgPubsub struct {
|
||||
pgListener *pq.Listener
|
||||
db *sql.DB
|
||||
target Pubsub
|
||||
}
|
||||
|
||||
// NewPubsub creates a new Pubsub implementation using a PostgreSQL connection.
|
||||
func NewPubsub(ctx context.Context, database *sql.DB, connectURL string) (Pubsub, error) {
|
||||
// Creates a new listener using pq.
|
||||
errCh := make(chan error)
|
||||
listener := pq.NewListener(connectURL, time.Second, time.Minute, func(event pq.ListenerEventType, err error) {
|
||||
// This callback gets events whenever the connection state changes.
|
||||
// Don't send if the errChannel has already been closed.
|
||||
select {
|
||||
case <-errCh:
|
||||
return
|
||||
default:
|
||||
errCh <- err
|
||||
close(errCh)
|
||||
}
|
||||
})
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("create pq listener: %w", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
if err := listener.Listen("ionscale_events"); err != nil {
|
||||
return nil, errors.Errorf("listen: %w", err)
|
||||
}
|
||||
|
||||
pubsub := &pgPubsub{
|
||||
db: database,
|
||||
pgListener: listener,
|
||||
target: NewPubsubInMemory(),
|
||||
}
|
||||
go pubsub.listen(ctx)
|
||||
|
||||
return pubsub, nil
|
||||
}
|
||||
|
||||
// Close closes the pubsub instance.
|
||||
func (p *pgPubsub) Close() error {
|
||||
return p.pgListener.Close()
|
||||
}
|
||||
|
||||
func (p *pgPubsub) Subscribe(tailnet uint64, listener Listener) (cancel func(), err error) {
|
||||
return p.target.Subscribe(tailnet, listener)
|
||||
}
|
||||
|
||||
func (p *pgPubsub) Publish(tailnet uint64, message *Signal) error {
|
||||
event := &pgEvent{
|
||||
TailnetID: tailnet,
|
||||
Signal: message,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This is safe because we are calling pq.QuoteLiteral. pg_notify doesn't
|
||||
// support the first parameter being a prepared statement.
|
||||
//nolint:gosec
|
||||
_, err = p.db.ExecContext(context.Background(), `select pg_notify(`+pq.QuoteLiteral("ionscale_events")+`, $1)`, payload)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return errors.Errorf("exec pg_notify: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listen begins receiving messages on the pq listener.
|
||||
func (p *pgPubsub) listen(ctx context.Context) {
|
||||
var (
|
||||
notif *pq.Notification
|
||||
ok bool
|
||||
)
|
||||
defer p.pgListener.Close()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case notif, ok = <-p.pgListener.Notify:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// A nil notification can be dispatched on reconnect.
|
||||
if notif == nil {
|
||||
continue
|
||||
}
|
||||
p.listenReceive(notif)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pgPubsub) listenReceive(notif *pq.Notification) {
|
||||
extra := []byte(notif.Extra)
|
||||
event := &pgEvent{}
|
||||
|
||||
if err := json.Unmarshal(extra, event); err == nil {
|
||||
p.target.Publish(event.TailnetID, event.Signal)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
type pgEvent struct {
|
||||
TailnetID uint64
|
||||
Signal *Signal
|
||||
}
|
||||
+54
-61
@@ -1,55 +1,30 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/go-edit/editor"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"io/ioutil"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tailscale/hujson"
|
||||
"os"
|
||||
)
|
||||
|
||||
func getACLConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func getACLConfigCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "get-acl-policy",
|
||||
Short: "Get the ACL policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var asJson bool
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().BoolVar(&asJson, "json", false, "When enabled, render output as json otherwise yaml")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := tc.Client().GetACLPolicy(cmd.Context(), connect.NewRequest(&api.GetACLPolicyRequest{TailnetId: tc.TailnetID()}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.GetACLPolicy(context.Background(), connect.NewRequest(&api.GetACLPolicyRequest{TailnetId: tailnet.Id}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
marshal, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(marshal))
|
||||
fmt.Println(resp.Msg.Policy)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -57,46 +32,64 @@ func getACLConfigCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func setACLConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set-acl-policy",
|
||||
Short: "Set ACL policy",
|
||||
func editACLConfigCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "edit-acl-policy",
|
||||
Short: "Edit the ACL policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var file string
|
||||
var target = Target{}
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
edit := editor.NewDefaultEditor([]string{"IONSCALE_EDITOR", "EDITOR"})
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the acl configuration")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
rawJson, err := ioutil.ReadFile(file)
|
||||
resp, err := tc.Client().GetACLPolicy(cmd.Context(), connect.NewRequest(&api.GetACLPolicyRequest{TailnetId: tc.TailnetID()}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var policy = &api.ACLPolicy{}
|
||||
if err := json.Unmarshal(rawJson, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
next, s, err := edit.LaunchTempFile("ionscale", ".json", bytes.NewReader([]byte(resp.Msg.Policy)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
defer os.Remove(s)
|
||||
|
||||
next, err = hujson.Standardize(next)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.SetACLPolicy(context.Background(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tailnet.Id, Policy: policy}))
|
||||
_, err = tc.Client().SetACLPolicy(cmd.Context(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(next)}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("ACL policy updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func setACLConfigCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "set-acl-policy",
|
||||
Short: "Set ACL policy",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var file string
|
||||
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the acl configuration")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tc.Client().SetACLPolicy(cmd.Context(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(content)}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+16
-21
@@ -1,16 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func authCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func authCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "auth",
|
||||
}
|
||||
|
||||
@@ -19,25 +18,15 @@ func authCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func authLoginCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func authLoginCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "login",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &api.AuthenticationRequest{}
|
||||
stream, err := client.Authenticate(context.Background(), connect.NewRequest(req))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := &api.AuthenticateRequest{}
|
||||
stream, err := tc.Client().Authenticate(cmd.Context(), connect.NewRequest(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -48,7 +37,13 @@ func authLoginCommand() *coral.Command {
|
||||
if len(resp.Token) != 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("Success.")
|
||||
if err := ionscale.SessionToFile(resp.Token, resp.TailnetId); err != nil {
|
||||
|
||||
tailnetId := uint64(0)
|
||||
if resp.TailnetId != nil {
|
||||
tailnetId = *resp.TailnetId
|
||||
}
|
||||
|
||||
if err := ionscale.StoreAuthToken(tc.Addr(), resp.Token, tailnetId); err != nil {
|
||||
fmt.Println()
|
||||
fmt.Println("Your api token:")
|
||||
fmt.Println()
|
||||
|
||||
+30
-69
@@ -1,22 +1,22 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
str2dur "github.com/xhit/go-str2duration/v2"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func authkeysCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "auth-keys",
|
||||
Short: "Manage ionscale auth keys",
|
||||
func authkeysCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "auth-keys",
|
||||
Aliases: []string{"auth-key"},
|
||||
Short: "Manage ionscale auth keys",
|
||||
}
|
||||
|
||||
command.AddCommand(createAuthkeysCommand())
|
||||
@@ -26,39 +26,24 @@ func authkeysCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func createAuthkeysCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func createAuthkeysCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new auth key in the specified tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var ephemeral bool
|
||||
var preAuthorized bool
|
||||
var tags []string
|
||||
var expiry string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().BoolVar(&ephemeral, "ephemeral", false, "When enabled, machines authenticated by this key will be automatically removed after going offline.")
|
||||
command.Flags().StringSliceVar(&tags, "tag", []string{}, "Machines authenticated by this key will be automatically tagged with these tags")
|
||||
command.Flags().StringVar(&expiry, "expiry", "180d", "Human-readable expiration of the key")
|
||||
command.Flags().BoolVar(&preAuthorized, "pre-authorized", false, "Generate an auth key which is pre-authorized.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
var expiryDur *durationpb.Duration
|
||||
|
||||
if expiry != "" && expiry != "none" {
|
||||
@@ -70,12 +55,13 @@ func createAuthkeysCommand() *coral.Command {
|
||||
}
|
||||
|
||||
req := &api.CreateAuthKeyRequest{
|
||||
TailnetId: tailnet.Id,
|
||||
Ephemeral: ephemeral,
|
||||
Tags: tags,
|
||||
Expiry: expiryDur,
|
||||
TailnetId: tc.TailnetID(),
|
||||
Ephemeral: ephemeral,
|
||||
PreAuthorized: preAuthorized,
|
||||
Tags: tags,
|
||||
Expiry: expiryDur,
|
||||
}
|
||||
resp, err := client.CreateAuthKey(context.Background(), connect.NewRequest(req))
|
||||
resp, err := tc.Client().CreateAuthKey(cmd.Context(), connect.NewRequest(req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -94,26 +80,20 @@ func createAuthkeysCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteAuthKeyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func deleteAuthKeyCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a specified auth key",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var authKeyId uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&authKeyId, "id", 0, "Auth Key ID")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
grpcClient, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DeleteAuthKeyRequest{AuthKeyId: authKeyId}
|
||||
if _, err := grpcClient.DeleteAuthKey(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
if _, err := tc.Client().DeleteAuthKey(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -125,35 +105,16 @@ func deleteAuthKeyCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func listAuthkeysCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func listAuthkeysCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all auth keys for a given tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &api.ListAuthKeysRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.ListAuthKeys(context.Background(), connect.NewRequest(req))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := &api.ListAuthKeysRequest{TailnetId: tc.TailnetID()}
|
||||
resp, err := tc.Client().ListAuthKeys(cmd.Context(), connect.NewRequest(req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func configureCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func configureCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "configure",
|
||||
Short: "Generate a simple config file to get started.",
|
||||
SilenceUsage: true,
|
||||
@@ -33,7 +33,7 @@ func configureCommand() *coral.Command {
|
||||
|
||||
command.MarkFlagRequired("domain")
|
||||
|
||||
command.PreRunE = func(cmd *coral.Command, args []string) error {
|
||||
command.PreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if domain == "" {
|
||||
return errors.New("required flag 'domain' is missing")
|
||||
}
|
||||
@@ -49,13 +49,12 @@ func configureCommand() *coral.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
command.RunE = func(command *cobra.Command, args []string) error {
|
||||
c := &config.Config{}
|
||||
|
||||
c.HttpListenAddr = "0.0.0.0:80"
|
||||
c.HttpsListenAddr = "0.0.0.0:443"
|
||||
c.ListenAddr = "0.0.0.0:443"
|
||||
c.MetricsListenAddr = "127.0.0.1:9090"
|
||||
c.ServerUrl = fmt.Sprintf("https://%s", domain)
|
||||
c.PublicAddr = fmt.Sprintf("%s:443", domain)
|
||||
|
||||
c.Keys = config.Keys{
|
||||
ControlKey: key.NewServerKey().String(),
|
||||
@@ -67,7 +66,6 @@ func configureCommand() *coral.Command {
|
||||
if acme {
|
||||
c.Tls.AcmeEnabled = true
|
||||
c.Tls.AcmeEmail = email
|
||||
c.Tls.AcmePath = filepath.Join(dataDir, "acme")
|
||||
} else {
|
||||
c.Tls.CertFile = certFile
|
||||
c.Tls.KeyFile = keyFile
|
||||
|
||||
+13
-67
@@ -1,49 +1,39 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func derpMapCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "derp-map",
|
||||
Short: "Manage DERP Map configuration",
|
||||
func systemCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "system",
|
||||
Short: "Manage global system configurations",
|
||||
}
|
||||
|
||||
command.AddCommand(getDERPMap())
|
||||
command.AddCommand(setDERPMap())
|
||||
command.AddCommand(getDefaultDERPMap())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func getDERPMap() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "get",
|
||||
Short: "Get the DERP Map configuration",
|
||||
func getDefaultDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "get-derp-map",
|
||||
Short: "Get the default DERP Map configuration",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var asJson bool
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().BoolVar(&asJson, "json", false, "When enabled, render output as json otherwise yaml")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.GetDERPMap(context.Background(), connect.NewRequest(&api.GetDERPMapRequest{}))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := tc.Client().GetDefaultDERPMap(cmd.Context(), connect.NewRequest(&api.GetDefaultDERPMapRequest{}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -63,7 +53,6 @@ func getDERPMap() *coral.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(string(marshal))
|
||||
} else {
|
||||
marshal, err := yaml.Marshal(derpMap)
|
||||
@@ -71,7 +60,6 @@ func getDERPMap() *coral.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(string(marshal))
|
||||
}
|
||||
|
||||
@@ -80,45 +68,3 @@ func getDERPMap() *coral.Command {
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func setDERPMap() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set",
|
||||
Short: "Set the DERP Map configuration",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var file string
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the DERP Map configuration")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
grpcClient, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawJson, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := grpcClient.SetDERPMap(context.Background(), connect.NewRequest(&api.SetDERPMapRequest{Value: rawJson}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
if err := json.Unmarshal(resp.Msg.Value, &derpMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("DERP Map updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
+64
-94
@@ -1,86 +1,32 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
func getDNSConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func getDNSConfigCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "get-dns",
|
||||
Short: "Get DNS configuration",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.GetDNSConfigRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.GetDNSConfig(context.Background(), connect.NewRequest(&req))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.GetDNSConfigRequest{TailnetId: tc.TailnetID()}
|
||||
resp, err := tc.Client().GetDNSConfig(cmd.Context(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := resp.Msg.Config
|
||||
|
||||
var allNameservers = config.Nameservers
|
||||
|
||||
for i, j := range config.Routes {
|
||||
for _, n := range j.Routes {
|
||||
allNameservers = append(allNameservers, fmt.Sprintf("%s:%s", i, n))
|
||||
}
|
||||
}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintf(w, "%s\t\t%v\n", "Override Local DNS", config.OverrideLocalDns)
|
||||
|
||||
if config.MagicDns {
|
||||
fmt.Fprintf(w, "MagicDNS\t%s\t%s\n", config.MagicDnsSuffix, "100.100.100.100")
|
||||
}
|
||||
|
||||
for k, r := range config.Routes {
|
||||
for i, t := range r.Routes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "SplitDNS\t%s\t%s\n", k, t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range config.Nameservers {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "Global", "", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "", "", t)
|
||||
}
|
||||
}
|
||||
printDnsConfig(config)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -88,39 +34,26 @@ func getDNSConfigCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func setDNSConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func setDNSConfigCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "set-dns",
|
||||
Short: "Set DNS config",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var nameservers []string
|
||||
var magicDNS bool
|
||||
var httpsCerts bool
|
||||
var overrideLocalDNS bool
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var target = Target{}
|
||||
var searchDomains []string
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().StringSliceVarP(&nameservers, "nameserver", "", []string{}, "Machines on your network will use these nameservers to resolve DNS queries.")
|
||||
command.Flags().BoolVarP(&magicDNS, "magic-dns", "", false, "Enable MagicDNS for the specified Tailnet")
|
||||
command.Flags().BoolVarP(&httpsCerts, "https-certs", "", false, "Enable HTTPS Certificates for the specified Tailnet")
|
||||
command.Flags().BoolVarP(&overrideLocalDNS, "override-local-dns", "", false, "When enabled, connected clients ignore local DNS settings and always use the nameservers specified for this Tailnet")
|
||||
command.Flags().StringSliceVarP(&searchDomains, "search-domain", "", []string{}, "Custom DNS search domains.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
var globalNameservers []string
|
||||
var routes = make(map[string]*api.Routes)
|
||||
|
||||
@@ -139,15 +72,17 @@ func setDNSConfigCommand() *coral.Command {
|
||||
}
|
||||
|
||||
req := api.SetDNSConfigRequest{
|
||||
TailnetId: tailnet.Id,
|
||||
TailnetId: tc.TailnetID(),
|
||||
Config: &api.DNSConfig{
|
||||
MagicDns: magicDNS,
|
||||
OverrideLocalDns: overrideLocalDNS,
|
||||
Nameservers: globalNameservers,
|
||||
Routes: routes,
|
||||
HttpsCerts: httpsCerts,
|
||||
SearchDomains: searchDomains,
|
||||
},
|
||||
}
|
||||
resp, err := client.SetDNSConfig(context.Background(), connect.NewRequest(&req))
|
||||
resp, err := tc.Client().SetDNSConfig(cmd.Context(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -155,20 +90,55 @@ func setDNSConfigCommand() *coral.Command {
|
||||
|
||||
config := resp.Msg.Config
|
||||
|
||||
var allNameservers = config.Nameservers
|
||||
|
||||
for i, j := range config.Routes {
|
||||
for _, n := range j.Routes {
|
||||
allNameservers = append(allNameservers, fmt.Sprintf("%s:%s", i, n))
|
||||
}
|
||||
if resp.Msg.Message != "" {
|
||||
fmt.Println(resp.Msg.Message)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Printf("%-*v%v\n", 25, "Magic DNS Enabled:", config.MagicDns)
|
||||
fmt.Printf("%-*v%v\n", 25, "Override Local DNS:", config.OverrideLocalDns)
|
||||
fmt.Printf("%-*v%v\n", 25, "Nameservers:", strings.Join(allNameservers, ","))
|
||||
printDnsConfig(config)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func printDnsConfig(config *api.DNSConfig) {
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 1, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintf(w, "%s\t\t%v\n", "MagicDNS", config.MagicDns)
|
||||
fmt.Fprintf(w, "%s\t\t%v\n", "HTTPS Certs", config.HttpsCerts)
|
||||
fmt.Fprintf(w, "%s\t\t%v\n", "Override Local DNS", config.OverrideLocalDns)
|
||||
|
||||
if config.MagicDns {
|
||||
fmt.Fprintf(w, "MagicDNS\t%s\t%s\n", config.MagicDnsSuffix, "100.100.100.100")
|
||||
}
|
||||
|
||||
for k, r := range config.Routes {
|
||||
for i, t := range r.Routes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "SplitDNS\t%s\t%s\n", k, t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range config.Nameservers {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "Global", "", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range config.SearchDomains {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "Search Domains", t, "")
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "", t, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
apiconnect "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"github.com/muesli/coral"
|
||||
)
|
||||
|
||||
func checkRequiredTailnetAndTailnetIdFlags(cmd *coral.Command, args []string) error {
|
||||
savedTailnetID, err := ionscale.TailnetFromFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if savedTailnetID == 0 && !cmd.Flags().Changed("tailnet") && !cmd.Flags().Changed("tailnet-id") {
|
||||
return fmt.Errorf("flag --tailnet or --tailnet-id is required")
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("tailnet") && cmd.Flags().Changed("tailnet-id") {
|
||||
return fmt.Errorf("flags --tailnet and --tailnet-id are mutually exclusive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTailnet(client apiconnect.IonscaleServiceClient, tailnet string, tailnetID uint64) (*api.Tailnet, error) {
|
||||
savedTailnetID, err := ionscale.TailnetFromFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if savedTailnetID == 0 && tailnetID == 0 && tailnet == "" {
|
||||
return nil, fmt.Errorf("requested tailnet not found or you are not authorized for this tailnet")
|
||||
}
|
||||
|
||||
tailnets, err := client.ListTailnets(context.Background(), connect.NewRequest(&api.ListTailnetRequest{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tailnets.Msg.Tailnet {
|
||||
if t.Id == savedTailnetID || t.Id == tailnetID || t.Name == tailnet {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("requested tailnet not found or you are not authorized for this tailnet")
|
||||
}
|
||||
+51
-62
@@ -1,53 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/go-edit/editor"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"io/ioutil"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
)
|
||||
|
||||
func getIAMPolicyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func getIAMPolicyCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "get-iam-policy",
|
||||
Short: "Get the IAM policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := tc.Client().GetIAMPolicy(cmd.Context(), connect.NewRequest(&api.GetIAMPolicyRequest{TailnetId: tc.TailnetID()}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.GetIAMPolicy(context.Background(), connect.NewRequest(&api.GetIAMPolicyRequest{TailnetId: tailnet.Id}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
marshal, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(marshal))
|
||||
fmt.Println(resp.Msg.Policy)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -55,46 +31,59 @@ func getIAMPolicyCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func setIAMPolicyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set-iam-policy",
|
||||
Short: "Set IAM policy",
|
||||
func editIAMPolicyCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "edit-iam-policy",
|
||||
Short: "Edit the IAM policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var file string
|
||||
var target = Target{}
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
edit := editor.NewDefaultEditor([]string{"IONSCALE_EDITOR", "EDITOR"})
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the acl configuration")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
rawJson, err := ioutil.ReadFile(file)
|
||||
resp, err := tc.Client().GetIAMPolicy(cmd.Context(), connect.NewRequest(&api.GetIAMPolicyRequest{TailnetId: tc.TailnetID()}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var policy = &api.IAMPolicy{}
|
||||
if err := json.Unmarshal(rawJson, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
next, s, err := edit.LaunchTempFile("ionscale", ".json", bytes.NewReader([]byte(resp.Msg.Policy)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(s)
|
||||
|
||||
_, err = client.SetIAMPolicy(context.Background(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tailnet.Id, Policy: policy}))
|
||||
_, err = tc.Client().SetIAMPolicy(cmd.Context(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(next)}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("IAM policy updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func setIAMPolicyCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "set-iam-policy",
|
||||
Short: "Set IAM policy",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var file string
|
||||
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the acl configuration")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tc.Client().SetIAMPolicy(cmd.Context(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(content)}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+13
-5
@@ -3,18 +3,26 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func keyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func keyCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "genkey",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
var disableNewLine bool
|
||||
|
||||
command.Flags().BoolVarP(&disableNewLine, "no-newline", "n", false, "do not output a trailing newline")
|
||||
|
||||
command.RunE = func(command *cobra.Command, args []string) error {
|
||||
serverKey := key.NewServerKey()
|
||||
fmt.Println(serverKey.String())
|
||||
if disableNewLine {
|
||||
fmt.Print(serverKey.String())
|
||||
} else {
|
||||
fmt.Println(serverKey.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+188
-175
@@ -1,22 +1,22 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/nleeper/goment"
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
"inet.af/netaddr"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
func machineCommands() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func machineCommands() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "machines",
|
||||
Aliases: []string{"machine", "devices", "device"},
|
||||
Short: "Manage ionscale machines",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
@@ -29,33 +29,29 @@ func machineCommands() *coral.Command {
|
||||
command.AddCommand(enableMachineRoutesCommand())
|
||||
command.AddCommand(disableMachineRoutesCommand())
|
||||
command.AddCommand(enableMachineKeyExpiryCommand())
|
||||
command.AddCommand(enableExitNodeCommand())
|
||||
command.AddCommand(disableExitNodeCommand())
|
||||
command.AddCommand(disableMachineKeyExpiryCommand())
|
||||
command.AddCommand(authorizeMachineCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func getMachineCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func getMachineCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Retrieve detailed information for a machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.GetMachineRequest{MachineId: machineID}
|
||||
resp, err := client.GetMachine(context.Background(), connect.NewRequest(&req))
|
||||
resp, err := tc.Client().GetMachine(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,6 +88,10 @@ func getMachineCommand() *coral.Command {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Tailscale IPv4", m.Ipv4)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Tailscale IPv6", m.Ipv6)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Last seen", lastSeen)
|
||||
fmt.Fprintf(w, "%s\t%v\n", "Ephemeral", m.Ephemeral)
|
||||
if !m.Authorized {
|
||||
fmt.Fprintf(w, "%s\t%v\n", "Authorized", m.Authorized)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Key expiry", expiresAt)
|
||||
|
||||
for i, t := range m.Tags {
|
||||
@@ -126,34 +126,37 @@ func getMachineCommand() *coral.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if m.AdvertisedExitNode {
|
||||
if m.EnabledExitNode {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "enabled")
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "disabled")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "no")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteMachineCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func deleteMachineCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Deletes a machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DeleteMachineRequest{MachineId: machineID}
|
||||
if _, err := client.DeleteMachine(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
if _, err := tc.Client().DeleteMachine(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -165,28 +168,21 @@ func deleteMachineCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func expireMachineCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func expireMachineCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "expire",
|
||||
Short: "Expires a machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.ExpireMachineRequest{MachineId: machineID}
|
||||
if _, err := client.ExpireMachine(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
if _, err := tc.Client().ExpireMachine(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -198,41 +194,48 @@ func expireMachineCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func listMachinesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func authorizeMachineCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "authorize",
|
||||
Short: "Authorizes a machine",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.AuthorizeMachineRequest{MachineId: machineID}
|
||||
if _, err := tc.Client().AuthorizeMachine(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Machine authorized.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func listMachinesCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List machines",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.ListMachinesRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.ListMachines(context.Background(), connect.NewRequest(&req))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.ListMachinesRequest{TailnetId: tc.TailnetID()}
|
||||
resp, err := tc.Client().ListMachines(cmd.Context(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "TAILNET", "NAME", "IPv4", "IPv6", "EPHEMERAL", "LAST_SEEN", "TAGS")
|
||||
tbl := table.New("ID", "TAILNET", "NAME", "IPv4", "IPv6", "AUTHORIZED", "EPHEMERAL", "VERSION", "LAST_SEEN", "TAGS")
|
||||
for _, m := range resp.Msg.Machines {
|
||||
var lastSeen = "N/A"
|
||||
if m.Connected {
|
||||
@@ -243,7 +246,7 @@ func listMachinesCommand() *coral.Command {
|
||||
lastSeen = mom.FromNow()
|
||||
}
|
||||
}
|
||||
tbl.AddRow(m.Id, m.Tailnet.Name, m.Name, m.Ipv4, m.Ipv6, m.Ephemeral, lastSeen, strings.Join(m.Tags, ","))
|
||||
tbl.AddRow(m.Id, m.Tailnet.Name, m.Name, m.Ipv4, m.Ipv6, m.Authorized, m.Ephemeral, m.ClientVersion, lastSeen, strings.Join(m.Tags, ","))
|
||||
}
|
||||
tbl.Print()
|
||||
|
||||
@@ -253,51 +256,26 @@ func listMachinesCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func getMachineRoutesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func getMachineRoutesCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "get-routes",
|
||||
Short: "Show routes advertised and enabled by a given machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
grpcClient, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.GetMachineRoutesRequest{MachineId: machineID}
|
||||
resp, err := grpcClient.GetMachineRoutes(context.Background(), connect.NewRequest(&req))
|
||||
resp, err := tc.Client().GetMachineRoutes(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for i, t := range resp.Msg.AdvertisedRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range resp.Msg.EnabledRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
printMachinesRoutesResponse(resp.Msg.Routes)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -305,30 +283,24 @@ func getMachineRoutesCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func enableMachineRoutesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func enableMachineRoutesCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "enable-routes",
|
||||
Short: "Enable routes for a given machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
var routes []string
|
||||
var replace bool
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
command.Flags().StringSliceVar(&routes, "routes", []string{}, "List of routes to enable")
|
||||
command.Flags().BoolVar(&replace, "replace", false, "Replace current enabled routes with this new list")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
for _, r := range routes {
|
||||
if _, err := netaddr.ParseIPPrefix(r); err != nil {
|
||||
return err
|
||||
@@ -336,30 +308,12 @@ func enableMachineRoutesCommand() *coral.Command {
|
||||
}
|
||||
|
||||
req := api.EnableMachineRoutesRequest{MachineId: machineID, Routes: routes, Replace: replace}
|
||||
resp, err := client.EnableMachineRoutes(context.Background(), connect.NewRequest(&req))
|
||||
resp, err := tc.Client().EnableMachineRoutes(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for i, t := range resp.Msg.AdvertisedRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range resp.Msg.EnabledRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
printMachinesRoutesResponse(resp.Msg.Routes)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -367,28 +321,22 @@ func enableMachineRoutesCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func disableMachineRoutesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func disableMachineRoutesCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "disable-routes",
|
||||
Short: "Disable routes for a given machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
var routes []string
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
command.Flags().StringSliceVar(&routes, "routes", []string{}, "List of routes to enable")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
for _, r := range routes {
|
||||
if _, err := netaddr.ParseIPPrefix(r); err != nil {
|
||||
return err
|
||||
@@ -396,30 +344,12 @@ func disableMachineRoutesCommand() *coral.Command {
|
||||
}
|
||||
|
||||
req := api.DisableMachineRoutesRequest{MachineId: machineID, Routes: routes}
|
||||
resp, err := client.DisableMachineRoutes(context.Background(), connect.NewRequest(&req))
|
||||
resp, err := tc.Client().DisableMachineRoutes(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for i, t := range resp.Msg.AdvertisedRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range resp.Msg.EnabledRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
printMachinesRoutesResponse(resp.Msg.Routes)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -427,8 +357,63 @@ func disableMachineRoutesCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func enableMachineKeyExpiryCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func enableExitNodeCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "enable-exit-node",
|
||||
Short: "Enable given machine as an exit node",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.EnableExitNodeRequest{MachineId: machineID}
|
||||
resp, err := tc.Client().EnableExitNode(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printMachinesRoutesResponse(resp.Msg.Routes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableExitNodeCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "disable-exit-node",
|
||||
Short: "Disable given machine as an exit node",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var machineID uint64
|
||||
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DisableExitNodeRequest{MachineId: machineID}
|
||||
resp, err := tc.Client().DisableExitNode(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printMachinesRoutesResponse(resp.Msg.Routes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableMachineKeyExpiryCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "enable-key-expiry",
|
||||
Short: "Enable machine key expiry",
|
||||
SilenceUsage: true,
|
||||
@@ -437,8 +422,8 @@ func enableMachineKeyExpiryCommand() *coral.Command {
|
||||
return configureSetMachineKeyExpiryCommand(command, false)
|
||||
}
|
||||
|
||||
func disableMachineKeyExpiryCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func disableMachineKeyExpiryCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "disable-key-expiry",
|
||||
Short: "Disable machine key expiry",
|
||||
SilenceUsage: true,
|
||||
@@ -447,22 +432,18 @@ func disableMachineKeyExpiryCommand() *coral.Command {
|
||||
return configureSetMachineKeyExpiryCommand(command, true)
|
||||
}
|
||||
|
||||
func configureSetMachineKeyExpiryCommand(command *coral.Command, v bool) *coral.Command {
|
||||
func configureSetMachineKeyExpiryCommand(cmdTmpl *cobra.Command, disable bool) *cobra.Command {
|
||||
command, tc := prepareCommand(false, cmdTmpl)
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.SetMachineKeyExpiryRequest{MachineId: machineID, Disabled: v}
|
||||
_, err = client.SetMachineKeyExpiry(context.Background(), connect.NewRequest(&req))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.SetMachineKeyExpiryRequest{MachineId: machineID, Disabled: disable}
|
||||
_, err := tc.Client().SetMachineKeyExpiry(cmd.Context(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -472,3 +453,35 @@ func configureSetMachineKeyExpiryCommand(command *coral.Command, v bool) *coral.
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func printMachinesRoutesResponse(msg *api.MachineRoutes) {
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for i, t := range msg.AdvertisedRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range msg.EnabledRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
if msg.AdvertisedExitNode {
|
||||
if msg.EnabledExitNode {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "enabled")
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "disabled")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "no")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Command() *coral.Command {
|
||||
func Command() *cobra.Command {
|
||||
rootCmd := rootCommand()
|
||||
rootCmd.AddCommand(configureCommand())
|
||||
rootCmd.AddCommand(keyCommand())
|
||||
rootCmd.AddCommand(authCommand())
|
||||
rootCmd.AddCommand(derpMapCommand())
|
||||
rootCmd.AddCommand(serverCommand())
|
||||
rootCmd.AddCommand(versionCommand())
|
||||
rootCmd.AddCommand(tailnetCommand())
|
||||
rootCmd.AddCommand(authkeysCommand())
|
||||
rootCmd.AddCommand(machineCommands())
|
||||
rootCmd.AddCommand(userCommands())
|
||||
rootCmd.AddCommand(systemCommand())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
@@ -24,8 +24,8 @@ func Execute() error {
|
||||
return Command().Execute()
|
||||
}
|
||||
|
||||
func rootCommand() *coral.Command {
|
||||
return &coral.Command{
|
||||
func rootCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "ionscale",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package cmd
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/server"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func serverCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func serverCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start an ionscale server",
|
||||
SilenceUsage: true,
|
||||
@@ -15,16 +15,16 @@ func serverCommand() *coral.Command {
|
||||
|
||||
var configFile string
|
||||
|
||||
command.Flags().StringVarP(&configFile, "config", "c", "ionscale.yaml", "Path to the configuration file.")
|
||||
command.Flags().StringVarP(&configFile, "config", "c", "", "Path to the configuration file.")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
command.RunE = func(command *cobra.Command, args []string) error {
|
||||
|
||||
c, err := config.LoadConfig(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.Start(c)
|
||||
return server.Start(command.Context(), c)
|
||||
}
|
||||
|
||||
return command
|
||||
|
||||
+369
-55
@@ -1,18 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
idomain "github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"github.com/jsiebens/ionscale/pkg/defaults"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func tailnetCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "tailnets",
|
||||
Short: "Manage ionscale tailnets",
|
||||
func tailnetCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "tailnets",
|
||||
Aliases: []string{"tailnet"},
|
||||
Short: "Manage ionscale tailnets",
|
||||
}
|
||||
|
||||
command.AddCommand(listTailnetsCommand())
|
||||
@@ -22,30 +30,34 @@ func tailnetCommand() *coral.Command {
|
||||
command.AddCommand(setDNSConfigCommand())
|
||||
command.AddCommand(getACLConfigCommand())
|
||||
command.AddCommand(setACLConfigCommand())
|
||||
command.AddCommand(editACLConfigCommand())
|
||||
command.AddCommand(getIAMPolicyCommand())
|
||||
command.AddCommand(setIAMPolicyCommand())
|
||||
command.AddCommand(editIAMPolicyCommand())
|
||||
command.AddCommand(enableServiceCollectionCommand())
|
||||
command.AddCommand(disableServiceCollectionCommand())
|
||||
command.AddCommand(enableFileSharingCommand())
|
||||
command.AddCommand(disableFileSharingCommand())
|
||||
command.AddCommand(enableSSHCommand())
|
||||
command.AddCommand(disableSSHCommand())
|
||||
command.AddCommand(enableMachineAuthorizationCommand())
|
||||
command.AddCommand(disableMachineAuthorizationCommand())
|
||||
command.AddCommand(getDERPMap())
|
||||
command.AddCommand(setDERPMap())
|
||||
command.AddCommand(resetDERPMap())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func listTailnetsCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func listTailnetsCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List available Tailnets",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.ListTailnets(context.Background(), connect.NewRequest(&api.ListTailnetRequest{}))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := tc.Client().ListTailnets(cmd.Context(), connect.NewRequest(&api.ListTailnetsRequest{}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -63,28 +75,68 @@ func listTailnetsCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func createTailnetsCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func createTailnetsCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new Tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var name string
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
var domain string
|
||||
var email string
|
||||
|
||||
command.Flags().StringVarP(&name, "name", "n", "", "")
|
||||
_ = command.MarkFlagRequired("name")
|
||||
command.Flags().StringVar(&domain, "domain", "", "")
|
||||
command.Flags().StringVar(&email, "email", "", "")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
command.PreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("flag --name is required")
|
||||
}
|
||||
if domain != "" && email != "" {
|
||||
return fmt.Errorf("flags --email and --domain are mutually exclusive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
dnsConfig := defaults.DefaultDNSConfig()
|
||||
aclPolicy := defaults.DefaultACLPolicy().Marshal()
|
||||
iamPolicy := "{}"
|
||||
|
||||
if len(domain) != 0 {
|
||||
domainToLower := strings.ToLower(domain)
|
||||
m, err := json.MarshalIndent(&ionscale.IAMPolicy{
|
||||
Filters: []string{fmt.Sprintf("domain == %s", domainToLower)},
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iamPolicy = string(m)
|
||||
}
|
||||
|
||||
resp, err := client.CreateTailnet(context.Background(), connect.NewRequest(&api.CreateTailnetRequest{Name: name}))
|
||||
if len(email) != 0 {
|
||||
emailToLower := strings.ToLower(email)
|
||||
m, err := json.MarshalIndent(&ionscale.IAMPolicy{
|
||||
Emails: []string{emailToLower},
|
||||
Roles: map[string]string{
|
||||
emailToLower: string(idomain.UserRoleAdmin),
|
||||
},
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iamPolicy = string(m)
|
||||
}
|
||||
|
||||
resp, err := tc.Client().CreateTailnet(cmd.Context(), connect.NewRequest(&api.CreateTailnetRequest{
|
||||
Name: name,
|
||||
IamPolicy: iamPolicy,
|
||||
AclPolicy: aclPolicy,
|
||||
DnsConfig: dnsConfig,
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -100,38 +152,19 @@ func createTailnetsCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteTailnetCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func deleteTailnetCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var force bool
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().BoolVar(&force, "force", false, "When enabled, force delete the specified Tailnet even when machines are still available.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.DeleteTailnet(context.Background(), connect.NewRequest(&api.DeleteTailnetRequest{TailnetId: tailnet.Id, Force: force}))
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
_, err := tc.Client().DeleteTailnet(cmd.Context(), connect.NewRequest(&api.DeleteTailnetRequest{TailnetId: tc.TailnetID(), Force: force}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -143,3 +176,284 @@ func deleteTailnetCommand() *coral.Command {
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func getDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "get-derp-map",
|
||||
Short: "Get the DERP Map configuration",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var asJson bool
|
||||
|
||||
command.Flags().BoolVar(&asJson, "json", false, "When enabled, render output as json otherwise yaml")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := tc.Client().GetDERPMap(cmd.Context(), connect.NewRequest(&api.GetDERPMapRequest{TailnetId: tc.TailnetID()}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var derpMap struct {
|
||||
Regions map[int]*tailcfg.DERPRegion
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(resp.Msg.Value, &derpMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if asJson {
|
||||
marshal, err := json.MarshalIndent(derpMap, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(marshal))
|
||||
} else {
|
||||
marshal, err := yaml.Marshal(derpMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(marshal))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func setDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "set-derp-map",
|
||||
Short: "Set the DERP Map configuration",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var file string
|
||||
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the DERP Map configuration")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
rawJson, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := tc.Client().SetDERPMap(cmd.Context(), connect.NewRequest(&api.SetDERPMapRequest{TailnetId: tc.TailnetID(), Value: rawJson}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
if err := json.Unmarshal(resp.Msg.Value, &derpMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("DERP Map updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func resetDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "reset-derp-map",
|
||||
Short: "Reset the DERP Map to the default configuration",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
if _, err := tc.Client().ResetDERPMap(cmd.Context(), connect.NewRequest(&api.ResetDERPMapRequest{TailnetId: tc.TailnetID()})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("DERP Map updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableFileSharingCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "enable-file-sharing",
|
||||
Aliases: []string{"enable-taildrop"},
|
||||
Short: "Enable Taildrop, the file sharing feature",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.EnableFileSharingRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().EnableFileSharing(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableFileSharingCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "disable-file-sharing",
|
||||
Aliases: []string{"disable-taildrop"},
|
||||
Short: "Disable Taildrop, the file sharing feature",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DisableFileSharingRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().DisableFileSharing(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableServiceCollectionCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "enable-service-collection",
|
||||
Short: "Enable monitoring live services running on your network’s machines.",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.EnableServiceCollectionRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().EnableServiceCollection(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableServiceCollectionCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "disable-service-collection",
|
||||
Short: "Disable monitoring live services running on your network’s machines.",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DisableServiceCollectionRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().DisableServiceCollection(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableSSHCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "enable-ssh",
|
||||
Short: "Enable ssh access using tailnet and ACLs.",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.EnableSSHRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().EnableSSH(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableSSHCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "disable-ssh",
|
||||
Short: "Disable ssh access using tailnet and ACLs.",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DisableSSHRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().DisableSSH(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableMachineAuthorizationCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "enable-machine-authorization",
|
||||
Short: "Enable machine authorization.",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.EnableMachineAuthorizationRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().EnableMachineAuthorization(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableMachineAuthorizationCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "disable-machine-authorization",
|
||||
Short: "Disable machine authorization.",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DisableMachineAuthorizationRequest{
|
||||
TailnetId: tc.TailnetID(),
|
||||
}
|
||||
|
||||
if _, err := tc.Client().DisableMachineAuthorization(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
+92
-17
@@ -1,60 +1,135 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
ionscalev1 "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
ionscaleSystemAdminKey = "IONSCALE_SYSTEM_ADMIN_KEY"
|
||||
ionscaleKeysSystemAdminKey = "IONSCALE_KEYS_SYSTEM_ADMIN_KEY"
|
||||
ionscaleAddr = "IONSCALE_ADDR"
|
||||
ionscaleInsecureSkipVerify = "IONSCALE_SKIP_VERIFY"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
type TargetContext interface {
|
||||
Client() api.IonscaleServiceClient
|
||||
Addr() string
|
||||
TailnetID() uint64
|
||||
}
|
||||
|
||||
type target struct {
|
||||
addr string
|
||||
insecureSkipVerify bool
|
||||
systemAdminKey string
|
||||
|
||||
tailnetID uint64
|
||||
tailnetName string
|
||||
|
||||
client api.IonscaleServiceClient
|
||||
tailnet *ionscalev1.Tailnet
|
||||
}
|
||||
|
||||
func (t *Target) prepareCommand(cmd *coral.Command) {
|
||||
func prepareCommand(enableTailnetSelector bool, cmd *cobra.Command) (*cobra.Command, TargetContext) {
|
||||
t := &target{}
|
||||
|
||||
cmd.Flags().StringVar(&t.addr, "addr", "", "Addr of the ionscale server, as a complete URL")
|
||||
cmd.Flags().BoolVar(&t.insecureSkipVerify, "tls-skip-verify", false, "Disable verification of TLS certificates")
|
||||
cmd.Flags().StringVar(&t.systemAdminKey, "system-admin-key", "", "If specified, the given value will be used as the key to generate a Bearer token for the call. This can also be specified via the IONSCALE_ADMIN_KEY environment variable.")
|
||||
}
|
||||
|
||||
func (t *Target) createGRPCClient() (api.IonscaleServiceClient, error) {
|
||||
addr := t.getAddr()
|
||||
skipVerify := t.getInsecureSkipVerify()
|
||||
systemAdminKey := t.getSystemAdminKey()
|
||||
|
||||
auth, err := ionscale.LoadClientAuth(systemAdminKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if enableTailnetSelector {
|
||||
cmd.Flags().StringVar(&t.tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
cmd.Flags().Uint64Var(&t.tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
}
|
||||
|
||||
return ionscale.NewClient(auth, addr, skipVerify)
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
addr := t.getAddr()
|
||||
skipVerify := t.getInsecureSkipVerify()
|
||||
systemAdminKey := t.getSystemAdminKey()
|
||||
|
||||
auth, err := ionscale.LoadClientAuth(addr, systemAdminKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := ionscale.NewClient(auth, addr, skipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.client = client
|
||||
|
||||
if enableTailnetSelector {
|
||||
savedTailnetID := auth.TailnetID()
|
||||
|
||||
if savedTailnetID == 0 && !cmd.Flags().Changed("tailnet") && !cmd.Flags().Changed("tailnet-id") {
|
||||
return fmt.Errorf("flag --tailnet or --tailnet-id is required")
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("tailnet") && cmd.Flags().Changed("tailnet-id") {
|
||||
return fmt.Errorf("flags --tailnet and --tailnet-id are mutually exclusive")
|
||||
}
|
||||
|
||||
tailnets, err := t.client.ListTailnets(cmd.Context(), connect.NewRequest(&ionscalev1.ListTailnetsRequest{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tailnet := range tailnets.Msg.Tailnet {
|
||||
if tailnet.Id == savedTailnetID || tailnet.Id == t.tailnetID || tailnet.Name == t.tailnetName {
|
||||
t.tailnet = tailnet
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if t.tailnet == nil {
|
||||
return fmt.Errorf("requested tailnet not found or you are not authorized for this tailnet")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd, t
|
||||
}
|
||||
|
||||
func (t *Target) getAddr() string {
|
||||
func (t *target) getAddr() string {
|
||||
if len(t.addr) != 0 {
|
||||
return t.addr
|
||||
}
|
||||
return config.GetString(ionscaleAddr, "https://localhost:8443")
|
||||
}
|
||||
|
||||
func (t *Target) getInsecureSkipVerify() bool {
|
||||
func (t *target) getInsecureSkipVerify() bool {
|
||||
if t.insecureSkipVerify {
|
||||
return true
|
||||
}
|
||||
return config.GetBool(ionscaleInsecureSkipVerify, false)
|
||||
}
|
||||
|
||||
func (t *Target) getSystemAdminKey() string {
|
||||
func (t *target) getSystemAdminKey() string {
|
||||
if len(t.systemAdminKey) != 0 {
|
||||
return t.systemAdminKey
|
||||
}
|
||||
return config.GetString(ionscaleSystemAdminKey, "")
|
||||
return config.GetString(ionscaleSystemAdminKey, config.GetString(ionscaleKeysSystemAdminKey, ""))
|
||||
}
|
||||
|
||||
func (t *target) Addr() string {
|
||||
return t.getAddr()
|
||||
}
|
||||
|
||||
func (t *target) Client() api.IonscaleServiceClient {
|
||||
return t.client
|
||||
}
|
||||
|
||||
func (t *target) TailnetID() uint64 {
|
||||
if t.tailnet == nil {
|
||||
return 0
|
||||
}
|
||||
return t.tailnet.Id
|
||||
}
|
||||
|
||||
+16
-41
@@ -1,17 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func userCommands() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func userCommands() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "users",
|
||||
Aliases: []string{"user"},
|
||||
Short: "Manage ionscale users",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
@@ -22,35 +22,16 @@ func userCommands() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func listUsersCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func listUsersCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(true, &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List users",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.ListUsersRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.ListUsers(context.Background(), connect.NewRequest(&req))
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.ListUsersRequest{TailnetId: tc.TailnetID()}
|
||||
resp, err := tc.Client().ListUsers(cmd.Context(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -68,28 +49,22 @@ func listUsersCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteUserCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
func deleteUserCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Deletes a user",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var userID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&userID, "user-id", 0, "User ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("user-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
req := api.DeleteUserRequest{UserId: userID}
|
||||
if _, err := client.DeleteUser(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
if _, err := tc.Client().DeleteUser(cmd.Context(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+7
-20
@@ -1,25 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/version"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func versionCommand() *coral.Command {
|
||||
var command = &coral.Command{
|
||||
func versionCommand() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display version information",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
})
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Run = func(cmd *coral.Command, args []string) {
|
||||
command.Run = func(cmd *cobra.Command, args []string) {
|
||||
clientVersion, clientRevision := version.GetReleaseInfo()
|
||||
fmt.Printf(`
|
||||
Client:
|
||||
@@ -27,16 +23,7 @@ Client:
|
||||
Git Revision: %s
|
||||
`, clientVersion, clientRevision)
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
fmt.Printf(`
|
||||
Server:
|
||||
Error: %s
|
||||
`, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.GetVersion(context.Background(), connect.NewRequest(&api.GetVersionRequest{}))
|
||||
resp, err := tc.Client().GetVersion(cmd.Context(), connect.NewRequest(&api.GetVersionRequest{}))
|
||||
if err != nil {
|
||||
fmt.Printf(`
|
||||
Server:
|
||||
@@ -50,7 +37,7 @@ Server:
|
||||
Addr: %s
|
||||
Version: %s
|
||||
Git Revision: %s
|
||||
`, target.getAddr(), resp.Msg.Version, resp.Msg.Revision)
|
||||
`, tc.Addr(), resp.Msg.Version, resp.Msg.Revision)
|
||||
|
||||
}
|
||||
|
||||
|
||||
+251
-116
@@ -1,29 +1,49 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/caarlos0/env/v6"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
tkey "tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
KeepAliveInterval = 1 * time.Minute
|
||||
const (
|
||||
defaultKeepAliveInterval = 1 * time.Minute
|
||||
defaultMagicDNSSuffix = "ionscale.net"
|
||||
)
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
config, err := defaultConfig()
|
||||
var (
|
||||
keepAliveInterval = defaultKeepAliveInterval
|
||||
magicDNSSuffix = defaultMagicDNSSuffix
|
||||
dnsProviderConfigured = false
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func KeepAliveInterval() time.Duration {
|
||||
return keepAliveInterval
|
||||
}
|
||||
|
||||
func MagicDNSSuffix() string {
|
||||
return magicDNSSuffix
|
||||
}
|
||||
|
||||
func DNSProviderConfigured() bool {
|
||||
return dnsProviderConfigured
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
cfg := defaultConfig()
|
||||
|
||||
if len(path) != 0 {
|
||||
expandedPath, err := homedir.Expand(path)
|
||||
@@ -41,88 +61,79 @@ func LoadConfig(path string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(b, config); err != nil {
|
||||
if err := yaml.Unmarshal(b, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
KeepAliveInterval = config.PollNet.KeepAliveInterval
|
||||
envCfgB64 := os.Getenv("IONSCALE_CONFIG_BASE64")
|
||||
if len(envCfgB64) != 0 {
|
||||
b, err := base64.StdEncoding.DecodeString(envCfgB64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := yaml.Unmarshal(b, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
httpListenAddrKey = "IONSCALE_HTTP_LISTEN_ADDR"
|
||||
httpsListenAddrKey = "IONSCALE_HTTPS_LISTEN_ADDR"
|
||||
serverUrlKey = "IONSCALE_SERVER_URL"
|
||||
keysSystemAdminKeyKey = "IONSCALE_SYSTEM_ADMIN_KEY"
|
||||
keysControlKeyKey = "IONSCALE_CONTROL_KEY"
|
||||
keysLegacyControlKeyKey = "IONSCALE_LEGACY_CONTROL_KEY"
|
||||
databaseUrlKey = "IONSCALE_DB_URL"
|
||||
tlsDisableKey = "IONSCALE_TLS_DISABLE"
|
||||
tlsForceHttpsKey = "IONSCALE_TLS_FORCE_HTTPS"
|
||||
tlsCertFileKey = "IONSCALE_TLS_CERT_FILE"
|
||||
tlsKeyFileKey = "IONSCALE_TLS_KEY_FILE"
|
||||
tlsAcmeKey = "IONSCALE_TLS_ACME"
|
||||
tlsAcmeCAKey = "IONSCALE_TLS_ACME_CA"
|
||||
tlsAcmeEmailKey = "IONSCALE_TLS_ACME_EMAIL"
|
||||
tlsAcmePath = "IONSCALE_TLS_ACME_PATH"
|
||||
pollNetKeepAliveInterval = "IONSCALE_POLL_NET_KEEP_ALIVE_INTERVAL"
|
||||
metricsListenAddrKey = "IONSCALE_METRICS_LISTEN_ADDR"
|
||||
loggingLevelKey = "IONSCALE_LOGGING_LEVEL"
|
||||
loggingFormatKey = "IONSCALE_LOGGING_FORMAT"
|
||||
loggingFileKey = "IONSCALE_LOGGING_FILE"
|
||||
authProviderIssuerKey = "IONSCALE_AUTH_PROVIDER_ISSUER"
|
||||
authProviderClientIdKey = "IONSCALE_AUTH_PROVIDER_CLIENT_ID"
|
||||
authProviderClientSecretKey = "IONSCALE_AUTH_PROVIDER_CLIENT_SECRET"
|
||||
authProviderScopesKey = "IONSCALE_AUTH_PROVIDER_SCOPES"
|
||||
)
|
||||
|
||||
func defaultConfig() (*Config, error) {
|
||||
defaultKeepAliveInterval, err := GetDuration(pollNetKeepAliveInterval, 1*time.Minute)
|
||||
if err != nil {
|
||||
envCfg := &Config{}
|
||||
if err := env.Parse(envCfg, env.Options{Prefix: "IONSCALE_"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Config{
|
||||
HttpListenAddr: GetString(httpListenAddrKey, ":8080"),
|
||||
HttpsListenAddr: GetString(httpsListenAddrKey, ":8443"),
|
||||
MetricsListenAddr: GetString(metricsListenAddrKey, ":9091"),
|
||||
ServerUrl: GetString(serverUrlKey, "https://localhost:8443"),
|
||||
Keys: Keys{
|
||||
SystemAdminKey: GetString(keysSystemAdminKeyKey, ""),
|
||||
ControlKey: GetString(keysControlKeyKey, ""),
|
||||
LegacyControlKey: GetString(keysLegacyControlKeyKey, ""),
|
||||
},
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(cfg, envCfg, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keepAliveInterval = cfg.PollNet.KeepAliveInterval
|
||||
magicDNSSuffix = cfg.DNS.MagicDNSSuffix
|
||||
|
||||
if cfg.DNS.Provider.Zone != "" {
|
||||
dnsProviderConfigured = true
|
||||
}
|
||||
|
||||
return cfg.Validate()
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
ListenAddr: ":8080",
|
||||
MetricsListenAddr: ":9091",
|
||||
StunListenAddr: ":3478",
|
||||
Database: Database{
|
||||
Url: GetString(databaseUrlKey, "ionscale.db"),
|
||||
Type: "sqlite",
|
||||
Url: "./ionscale.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)",
|
||||
MaxOpenConns: 0,
|
||||
MaxIdleConns: 2,
|
||||
},
|
||||
Tls: Tls{
|
||||
Disable: GetBool(tlsDisableKey, false),
|
||||
ForceHttps: GetBool(tlsForceHttpsKey, true),
|
||||
CertFile: GetString(tlsCertFileKey, ""),
|
||||
KeyFile: GetString(tlsKeyFileKey, ""),
|
||||
AcmeEnabled: GetBool(tlsAcmeKey, false),
|
||||
AcmeCA: GetString(tlsAcmeCAKey, certmagic.LetsEncryptProductionCA),
|
||||
AcmeEmail: GetString(tlsAcmeEmailKey, ""),
|
||||
AcmePath: GetString(tlsAcmePath, ""),
|
||||
Disable: false,
|
||||
ForceHttps: true,
|
||||
AcmeEnabled: false,
|
||||
AcmeCA: certmagic.LetsEncryptProductionCA,
|
||||
},
|
||||
PollNet: PollNet{
|
||||
KeepAliveInterval: defaultKeepAliveInterval,
|
||||
},
|
||||
AuthProvider: AuthProvider{
|
||||
Issuer: GetString(authProviderIssuerKey, ""),
|
||||
ClientID: GetString(authProviderClientIdKey, ""),
|
||||
ClientSecret: GetString(authProviderClientSecretKey, ""),
|
||||
Scopes: GetStrings(authProviderScopesKey, nil),
|
||||
DNS: DNS{
|
||||
MagicDNSSuffix: defaultMagicDNSSuffix,
|
||||
},
|
||||
DERP: DERP{
|
||||
Server: DERPServer{
|
||||
Disabled: false,
|
||||
RegionID: 1000,
|
||||
RegionCode: "ionscale",
|
||||
RegionName: "ionscale Embedded DERP",
|
||||
},
|
||||
},
|
||||
Logging: Logging{
|
||||
Level: GetString(loggingLevelKey, "info"),
|
||||
Format: GetString(loggingFormatKey, ""),
|
||||
File: GetString(loggingFileKey, ""),
|
||||
Level: "info",
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type ServerKeys struct {
|
||||
@@ -132,71 +143,142 @@ type ServerKeys struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
HttpListenAddr string `yaml:"http_listen_addr,omitempty"`
|
||||
HttpsListenAddr string `yaml:"https_listen_addr,omitempty"`
|
||||
MetricsListenAddr string `yaml:"metrics_listen_addr,omitempty"`
|
||||
ServerUrl string `yaml:"server_url,omitempty"`
|
||||
Tls Tls `yaml:"tls,omitempty"`
|
||||
PollNet PollNet `yaml:"poll_net,omitempty"`
|
||||
Keys Keys `yaml:"keys,omitempty"`
|
||||
Database Database `yaml:"database,omitempty"`
|
||||
AuthProvider AuthProvider `yaml:"auth_provider,omitempty"`
|
||||
Logging Logging `yaml:"logging,omitempty"`
|
||||
ListenAddr string `yaml:"listen_addr,omitempty" env:"LISTEN_ADDR"`
|
||||
StunListenAddr string `yaml:"stun_listen_addr,omitempty" env:"STUN_LISTEN_ADDR"`
|
||||
MetricsListenAddr string `yaml:"metrics_listen_addr,omitempty" env:"METRICS_LISTEN_ADDR"`
|
||||
PublicAddr string `yaml:"public_addr,omitempty" env:"PUBLIC_ADDR"`
|
||||
StunPublicAddr string `yaml:"stun_public_addr,omitempty" env:"STUN_PUBLIC_ADDR"`
|
||||
Tls Tls `yaml:"tls,omitempty" envPrefix:"TLS_"`
|
||||
PollNet PollNet `yaml:"poll_net,omitempty" envPrefix:"POLL_NET_"`
|
||||
Keys Keys `yaml:"keys,omitempty" envPrefix:"KEYS_"`
|
||||
Database Database `yaml:"database,omitempty" envPrefix:"DB_"`
|
||||
Auth Auth `yaml:"auth,omitempty" envPrefix:"AUTH_"`
|
||||
DNS DNS `yaml:"dns,omitempty"`
|
||||
DERP DERP `yaml:"derp,omitempty" envPrefix:"DERP_"`
|
||||
Logging Logging `yaml:"logging,omitempty" envPrefix:"LOGGING_"`
|
||||
|
||||
PublicUrl *url.URL `yaml:"-"`
|
||||
|
||||
stunHost string
|
||||
stunPort int
|
||||
derpHost string
|
||||
derpPort int
|
||||
}
|
||||
|
||||
type Tls struct {
|
||||
Disable bool `yaml:"disable"`
|
||||
ForceHttps bool `yaml:"force_https"`
|
||||
CertFile string `yaml:"cert_file,omitempty"`
|
||||
KeyFile string `yaml:"key_file,omitempty"`
|
||||
AcmeEnabled bool `yaml:"acme,omitempty"`
|
||||
AcmeEmail string `yaml:"acme_email,omitempty"`
|
||||
AcmeCA string `yaml:"acme_ca,omitempty"`
|
||||
AcmePath string `yaml:"acme_path,omitempty"`
|
||||
Disable bool `yaml:"disable" env:"DISABLE"`
|
||||
ForceHttps bool `yaml:"force_https" env:"FORCE_HTTPS"`
|
||||
CertFile string `yaml:"cert_file,omitempty" env:"CERT_FILE"`
|
||||
KeyFile string `yaml:"key_file,omitempty" env:"KEY_FILE"`
|
||||
AcmeEnabled bool `yaml:"acme,omitempty" env:"ACME_ENABLED"`
|
||||
AcmeEmail string `yaml:"acme_email,omitempty" env:"ACME_EMAIL"`
|
||||
AcmeCA string `yaml:"acme_ca,omitempty" env:"ACME_CA"`
|
||||
}
|
||||
|
||||
type PollNet struct {
|
||||
KeepAliveInterval time.Duration `yaml:"keep_alive_interval"`
|
||||
KeepAliveInterval time.Duration `yaml:"keep_alive_interval" env:"KEEP_ALIVE_INTERVAL"`
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
Level string `yaml:"level,omitempty"`
|
||||
Format string `yaml:"format,omitempty"`
|
||||
File string `yaml:"file,omitempty"`
|
||||
Level string `yaml:"level,omitempty" env:"LEVEL"`
|
||||
Format string `yaml:"format,omitempty" env:"FORMAT"`
|
||||
File string `yaml:"file,omitempty" env:"FILE"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Type string `yaml:"type,omitempty" env:"TYPE"`
|
||||
Url string `yaml:"url,omitempty" env:"URL"`
|
||||
MaxOpenConns int `yaml:"max_open_conns,omitempty" env:"MAX_OPEN_CONNS"`
|
||||
MaxIdleConns int `yaml:"max_idle_conns,omitempty" env:"MAX_IDLE_CONNS"`
|
||||
ConnMaxLifetime time.Duration `yaml:"conn_max_life_time,omitempty" env:"CONN_MAX_LIFE_TIME"`
|
||||
ConnMaxIdleTime time.Duration `yaml:"conn_max_idle_time,omitempty" env:"CONN_MAX_IDLE_TIME"`
|
||||
}
|
||||
|
||||
type Keys struct {
|
||||
ControlKey string `yaml:"control_key,omitempty"`
|
||||
LegacyControlKey string `yaml:"legacy_control_key,omitempty"`
|
||||
SystemAdminKey string `yaml:"system_admin_key,omitempty"`
|
||||
ControlKey string `yaml:"control_key,omitempty" env:"CONTROL_KEY"`
|
||||
LegacyControlKey string `yaml:"legacy_control_key,omitempty" env:"LEGACY_CONTROL_KEY"`
|
||||
SystemAdminKey string `yaml:"system_admin_key,omitempty" env:"SYSTEM_ADMIN_KEY"`
|
||||
}
|
||||
|
||||
type AuthProvider struct {
|
||||
Issuer string `yaml:"issuer"`
|
||||
ClientID string `yaml:"client_id"`
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
Scopes []string `yaml:"additional_scopes"`
|
||||
type Auth struct {
|
||||
Provider AuthProvider `yaml:"provider,omitempty" envPrefix:"PROVIDER_"`
|
||||
SystemAdminPolicy SystemAdminPolicy `yaml:"system_admins"`
|
||||
}
|
||||
|
||||
type AuthProvider struct {
|
||||
Issuer string `yaml:"issuer" env:"ISSUER"`
|
||||
ClientID string `yaml:"client_id" env:"CLIENT_ID"`
|
||||
ClientSecret string `yaml:"client_secret" env:"CLIENT_SECRET"`
|
||||
Scopes []string `yaml:"additional_scopes" env:"SCOPES"`
|
||||
}
|
||||
|
||||
type DNS struct {
|
||||
MagicDNSSuffix string `yaml:"magic_dns_suffix"`
|
||||
Provider DNSProvider `yaml:"provider,omitempty"`
|
||||
}
|
||||
|
||||
type DNSProvider struct {
|
||||
Name string `yaml:"name"`
|
||||
Zone string `yaml:"zone"`
|
||||
Configuration map[string]string `yaml:"config"`
|
||||
}
|
||||
|
||||
type SystemAdminPolicy struct {
|
||||
Subs []string `json:"subs,omitempty"`
|
||||
Emails []string `json:"emails,omitempty"`
|
||||
Filters []string `json:"filters,omitempty"`
|
||||
Subs []string `yaml:"subs,omitempty"`
|
||||
Emails []string `yaml:"emails,omitempty"`
|
||||
Filters []string `yaml:"filters,omitempty"`
|
||||
}
|
||||
|
||||
type DERP struct {
|
||||
Server DERPServer `yaml:"server,omitempty"`
|
||||
Sources []string `yaml:"sources,omitempty"`
|
||||
}
|
||||
|
||||
type DERPServer struct {
|
||||
Disabled bool `yaml:"disabled,omitempty"`
|
||||
RegionID int `yaml:"region_id,omitempty"`
|
||||
RegionCode string `yaml:"region_code,omitempty"`
|
||||
RegionName string `yaml:"region_name,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) Validate() (*Config, error) {
|
||||
publicWebUrl, webHost, webPort, err := validatePublicAddr(c.PublicAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("web public addr: %w", err)
|
||||
}
|
||||
|
||||
c.PublicUrl = publicWebUrl
|
||||
c.derpHost = webHost
|
||||
c.derpPort = webPort
|
||||
|
||||
if !c.DERP.Server.Disabled {
|
||||
_, stunHost, stunPort, err := validatePublicAddr(c.StunPublicAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stun public addr: %w", err)
|
||||
}
|
||||
|
||||
c.stunHost = stunHost
|
||||
c.stunPort = stunPort
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Config) CreateUrl(format string, a ...interface{}) string {
|
||||
path := fmt.Sprintf(format, a...)
|
||||
return strings.TrimSuffix(c.ServerUrl, "/") + "/" + strings.TrimPrefix(path, "/")
|
||||
u := url.URL{
|
||||
Scheme: c.PublicUrl.Scheme,
|
||||
Host: c.PublicUrl.Host,
|
||||
Path: path,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (c *Config) ReadServerKeys() (*ServerKeys, error) {
|
||||
keys := &ServerKeys{}
|
||||
func (c *Config) ReadServerKeys(defaultKeys *domain.ControlKeys) (*ServerKeys, error) {
|
||||
keys := &ServerKeys{
|
||||
ControlKey: defaultKeys.ControlKey,
|
||||
LegacyControlKey: defaultKeys.LegacyControlKey,
|
||||
}
|
||||
|
||||
if len(c.Keys.SystemAdminKey) != 0 {
|
||||
systemAdminKey, err := key.ParsePrivateKey(c.Keys.SystemAdminKey)
|
||||
@@ -207,17 +289,70 @@ func (c *Config) ReadServerKeys() (*ServerKeys, error) {
|
||||
keys.SystemAdminKey = systemAdminKey
|
||||
}
|
||||
|
||||
controlKey, err := util.ParseMachinePrivateKey(c.Keys.ControlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading control key: %v", err)
|
||||
if len(c.Keys.ControlKey) != 0 {
|
||||
controlKey, err := util.ParseMachinePrivateKey(c.Keys.ControlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading control key: %v", err)
|
||||
}
|
||||
keys.ControlKey = *controlKey
|
||||
}
|
||||
keys.ControlKey = *controlKey
|
||||
|
||||
legacyControlKey, err := util.ParseMachinePrivateKey(c.Keys.LegacyControlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading legacy control key: %v", err)
|
||||
if len(c.Keys.LegacyControlKey) != 0 {
|
||||
legacyControlKey, err := util.ParseMachinePrivateKey(c.Keys.LegacyControlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading legacy control key: %v", err)
|
||||
}
|
||||
keys.LegacyControlKey = *legacyControlKey
|
||||
}
|
||||
keys.LegacyControlKey = *legacyControlKey
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (c *Config) DefaultDERPMap() *tailcfg.DERPMap {
|
||||
if c.derpHost == c.stunHost {
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
c.DERP.Server.RegionID: {
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
RegionCode: c.DERP.Server.RegionCode,
|
||||
RegionName: c.DERP.Server.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
Name: "ionscale",
|
||||
HostName: c.derpHost,
|
||||
DERPPort: c.derpPort,
|
||||
STUNPort: c.stunPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
c.DERP.Server.RegionID: {
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
RegionCode: c.DERP.Server.RegionCode,
|
||||
RegionName: c.DERP.Server.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
Name: "stun",
|
||||
HostName: c.stunHost,
|
||||
STUNOnly: true,
|
||||
STUNPort: c.stunPort,
|
||||
},
|
||||
{
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
Name: "derp",
|
||||
HostName: c.derpHost,
|
||||
DERPPort: c.derpPort,
|
||||
STUNPort: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+37
-12
@@ -1,9 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetBool(key string, defaultValue bool) bool {
|
||||
@@ -22,22 +25,44 @@ func GetString(key, defaultValue string) string {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func GetStrings(key string, defaultValue []string) []string {
|
||||
func GetUint64(key string, defaultValue uint64) uint64 {
|
||||
v := os.Getenv(key)
|
||||
if v != "" {
|
||||
return strings.Split(v, ",")
|
||||
vi, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return vi
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func GetDuration(key string, defaultValue time.Duration) (time.Duration, error) {
|
||||
v := os.Getenv(key)
|
||||
if len(v) > 0 {
|
||||
duration, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return defaultValue, err
|
||||
}
|
||||
return duration, nil
|
||||
func validatePublicAddr(addr string) (*url.URL, string, int, error) {
|
||||
scheme := "https"
|
||||
|
||||
if strings.HasPrefix(addr, "http://") {
|
||||
scheme = "http"
|
||||
addr = strings.TrimPrefix(addr, "http://")
|
||||
}
|
||||
return defaultValue, nil
|
||||
|
||||
if strings.HasPrefix(addr, "https://") {
|
||||
scheme = "https"
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
}
|
||||
|
||||
host, portS, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, "", -1, fmt.Errorf("invalid")
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portS)
|
||||
if err != nil {
|
||||
return nil, "", 0, fmt.Errorf("invalid")
|
||||
}
|
||||
|
||||
if (port == 443 && scheme == "https") || (port == 80 && scheme == "http") {
|
||||
return &url.URL{Scheme: scheme, Host: host}, host, port, nil
|
||||
}
|
||||
|
||||
return &url.URL{Scheme: scheme, Host: fmt.Sprintf("%s:%d", host, port)}, host, port, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPublicAddrToUrl(t *testing.T) {
|
||||
mustParseUrl := func(s string) *url.URL {
|
||||
parse, err := url.Parse(s)
|
||||
require.NoError(t, err)
|
||||
return parse
|
||||
}
|
||||
|
||||
parameters := []struct {
|
||||
input string
|
||||
expected *url.URL
|
||||
err error
|
||||
}{
|
||||
{"localtest.me", nil, fmt.Errorf("invalid")},
|
||||
{"localtest.me:443", mustParseUrl("https://localtest.me"), nil},
|
||||
{"localtest.me:80", mustParseUrl("https://localtest.me:80"), nil},
|
||||
{"localtest.me:8080", mustParseUrl("https://localtest.me:8080"), nil},
|
||||
{"http://localtest.me:8080", mustParseUrl("http://localtest.me:8080"), nil},
|
||||
}
|
||||
|
||||
for _, p := range parameters {
|
||||
t.Run(fmt.Sprintf("Testing [%v]", p.input), func(t *testing.T) {
|
||||
url, _, _, err := validatePublicAddr(p.input)
|
||||
require.Equal(t, p.expected, url)
|
||||
require.Equal(t, p.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Ping struct{}
|
||||
|
||||
type PollMapSessionManager interface {
|
||||
Register(tailnetID uint64, machineID uint64, ch chan *Ping)
|
||||
Deregister(tailnetID uint64, machineID uint64)
|
||||
HasSession(tailnetID uint64, machineID uint64) bool
|
||||
NotifyAll(tailnetID uint64, ignoreMachineIDs ...uint64)
|
||||
}
|
||||
|
||||
func NewPollMapSessionManager() PollMapSessionManager {
|
||||
return &pollMapSessionManager{
|
||||
data: map[uint64]map[uint64]chan *Ping{},
|
||||
timers: map[uint64]*time.Timer{},
|
||||
}
|
||||
}
|
||||
|
||||
type pollMapSessionManager struct {
|
||||
sync.RWMutex
|
||||
data map[uint64]map[uint64]chan *Ping
|
||||
timers map[uint64]*time.Timer
|
||||
}
|
||||
|
||||
func (n *pollMapSessionManager) Register(tailnetID uint64, machineID uint64, ch chan *Ping) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
if ss := n.data[tailnetID]; ss == nil {
|
||||
n.data[tailnetID] = map[uint64]chan *Ping{machineID: ch}
|
||||
} else {
|
||||
ss[machineID] = ch
|
||||
}
|
||||
|
||||
t, ok := n.timers[machineID]
|
||||
if ok {
|
||||
t.Stop()
|
||||
delete(n.timers, machineID)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *pollMapSessionManager) Deregister(tailnetID uint64, machineID uint64) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
if ss := n.data[tailnetID]; ss != nil {
|
||||
delete(ss, machineID)
|
||||
}
|
||||
|
||||
t, ok := n.timers[machineID]
|
||||
if ok {
|
||||
t.Stop()
|
||||
delete(n.timers, machineID)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
go func() {
|
||||
<-timer.C
|
||||
if !n.HasSession(tailnetID, machineID) {
|
||||
n.NotifyAll(tailnetID)
|
||||
}
|
||||
}()
|
||||
|
||||
n.timers[machineID] = timer
|
||||
}
|
||||
|
||||
func (n *pollMapSessionManager) HasSession(tailnetID uint64, machineID uint64) bool {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
if ss := n.data[tailnetID]; ss != nil {
|
||||
if _, ok := ss[machineID]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *pollMapSessionManager) NotifyAll(tailnetID uint64, ignoreMachineIDs ...uint64) {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
if ss := n.data[tailnetID]; ss != nil {
|
||||
for i, p := range ss {
|
||||
if !slices.Contains(ignoreMachineIDs, i) {
|
||||
p <- &Ping{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package handlers
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"time"
|
||||
)
|
||||
@@ -12,26 +11,29 @@ const (
|
||||
inactivityTimeout = 30 * time.Minute
|
||||
)
|
||||
|
||||
func NewReaper(brokers broker.Pubsub, repository domain.Repository) *Reaper {
|
||||
return &Reaper{
|
||||
pubsub: brokers,
|
||||
repository: repository,
|
||||
func StartWorker(repository domain.Repository, sessionManager PollMapSessionManager) {
|
||||
r := &worker{
|
||||
sessionManager: sessionManager,
|
||||
repository: repository,
|
||||
}
|
||||
|
||||
go r.start()
|
||||
}
|
||||
|
||||
type Reaper struct {
|
||||
pubsub broker.Pubsub
|
||||
repository domain.Repository
|
||||
type worker struct {
|
||||
sessionManager PollMapSessionManager
|
||||
repository domain.Repository
|
||||
}
|
||||
|
||||
func (r *Reaper) Start() {
|
||||
func (r *worker) start() {
|
||||
r.deleteInactiveEphemeralNodes()
|
||||
t := time.NewTicker(ticker)
|
||||
for range t.C {
|
||||
r.reapInactiveEphemeralNodes()
|
||||
r.deleteInactiveEphemeralNodes()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reaper) reapInactiveEphemeralNodes() {
|
||||
func (r *worker) deleteInactiveEphemeralNodes() {
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now().UTC()
|
||||
@@ -40,6 +42,7 @@ func (r *Reaper) reapInactiveEphemeralNodes() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var removedNodes = make(map[uint64][]uint64)
|
||||
for _, m := range machines {
|
||||
if now.After(m.LastSeen.Add(inactivityTimeout)) {
|
||||
@@ -54,8 +57,8 @@ func (r *Reaper) reapInactiveEphemeralNodes() {
|
||||
}
|
||||
|
||||
if len(removedNodes) != 0 {
|
||||
for i, p := range removedNodes {
|
||||
r.pubsub.Publish(i, &broker.Signal{PeersRemoved: p})
|
||||
for i, _ := range removedNodes {
|
||||
r.sessionManager.NotifyAll(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-46
@@ -2,69 +2,69 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/database/migration"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/types/key"
|
||||
"time"
|
||||
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/plugin/prometheus"
|
||||
)
|
||||
|
||||
type db interface {
|
||||
DB() *gorm.DB
|
||||
type dbLock interface {
|
||||
Lock() error
|
||||
Unlock() error
|
||||
UnlockErr(error) error
|
||||
}
|
||||
|
||||
func OpenDB(config *config.Database, logger hclog.Logger) (domain.Repository, broker.Pubsub, error) {
|
||||
db, pubsub, err := createDB(config, logger)
|
||||
func OpenDB(config *config.Database, logger *zap.Logger) (*sql.DB, domain.Repository, error) {
|
||||
db, lock, err := createDB(config, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repository := domain.NewRepository(db.DB())
|
||||
_ = db.Use(prometheus.New(prometheus.Config{StartServer: false}))
|
||||
|
||||
if err := db.Lock(); err != nil {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.UnlockErr(migrate(db.DB())); err != nil {
|
||||
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime)
|
||||
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime)
|
||||
|
||||
repository := domain.NewRepository(db)
|
||||
|
||||
if err := lock.Lock(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return repository, pubsub, nil
|
||||
if err := lock.UnlockErr(migrate(db)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return sqlDB, repository, nil
|
||||
}
|
||||
|
||||
func createDB(config *config.Database, logger hclog.Logger) (db, broker.Pubsub, error) {
|
||||
func createDB(config *config.Database, logger *zap.Logger) (*gorm.DB, dbLock, error) {
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: &GormLoggerAdapter{logger: logger.Named("db")},
|
||||
Logger: &GormLoggerAdapter{logger: logger.Sugar()},
|
||||
}
|
||||
|
||||
switch config.Type {
|
||||
case "sqlite", "sqlite3":
|
||||
db, err := newSqliteDB(config, gormConfig)
|
||||
return db, broker.NewPubsubInMemory(), err
|
||||
return newSqliteDB(config, gormConfig)
|
||||
case "postgres", "postgresql":
|
||||
db, err := newPostgresDB(config, gormConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
stdDB, err := db.DB().DB()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pubsub, err := broker.NewPubsub(context.TODO(), stdDB, config.Url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return db, pubsub, err
|
||||
return newPostgresDB(config, gormConfig)
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("invalid database type '%s'", config.Type)
|
||||
@@ -77,11 +77,65 @@ func migrate(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
repository := domain.NewRepository(db)
|
||||
|
||||
if err := createServerKey(ctx, repository); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createJSONWebKeySet(ctx, repository); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createServerKey(ctx context.Context, repository domain.Repository) error {
|
||||
serverKey, err := repository.GetControlKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if serverKey != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := domain.ControlKeys{
|
||||
ControlKey: key.NewMachine(),
|
||||
LegacyControlKey: key.NewMachine(),
|
||||
}
|
||||
if err := repository.SetControlKeys(ctx, &keys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createJSONWebKeySet(ctx context.Context, repository domain.Repository) error {
|
||||
jwks, err := repository.GetJSONWebKeySet(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if jwks != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
privateKey, id, err := util.NewPrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonWebKey := domain.JSONWebKey{Id: id, PrivateKey: *privateKey}
|
||||
|
||||
if err := repository.SetJSONWebKeySet(ctx, &domain.JSONWebKeys{Key: jsonWebKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GormLoggerAdapter struct {
|
||||
logger hclog.Logger
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (g *GormLoggerAdapter) LogMode(level logger.LogLevel) logger.Interface {
|
||||
@@ -89,11 +143,11 @@ func (g *GormLoggerAdapter) LogMode(level logger.LogLevel) logger.Interface {
|
||||
}
|
||||
|
||||
func (g *GormLoggerAdapter) Info(ctx context.Context, s string, i ...interface{}) {
|
||||
g.logger.Info(s, i)
|
||||
g.logger.Infow(s, i)
|
||||
}
|
||||
|
||||
func (g *GormLoggerAdapter) Warn(ctx context.Context, s string, i ...interface{}) {
|
||||
g.logger.Warn(s, i)
|
||||
g.logger.Warnw(s, i)
|
||||
}
|
||||
|
||||
func (g *GormLoggerAdapter) Error(ctx context.Context, s string, i ...interface{}) {
|
||||
@@ -101,21 +155,23 @@ func (g *GormLoggerAdapter) Error(ctx context.Context, s string, i ...interface{
|
||||
}
|
||||
|
||||
func (g *GormLoggerAdapter) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
elapsed := time.Since(begin)
|
||||
switch {
|
||||
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
|
||||
sql, rows := fc()
|
||||
if rows == -1 {
|
||||
g.logger.Error("Error executing query", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed, "err", err)
|
||||
} else {
|
||||
g.logger.Error("Error executing query", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed, "rows", rows, "err", err)
|
||||
}
|
||||
case g.logger.IsTrace():
|
||||
sql, rows := fc()
|
||||
if rows == -1 {
|
||||
g.logger.Trace("Statement executed", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed)
|
||||
} else {
|
||||
g.logger.Trace("Statement executed", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed, "rows", rows)
|
||||
if g.logger.Level().Enabled(zap.DebugLevel) {
|
||||
elapsed := time.Since(begin)
|
||||
switch {
|
||||
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
|
||||
sql, rows := fc()
|
||||
if rows == -1 {
|
||||
g.logger.Debugw("Error executing query", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed, "err", err)
|
||||
} else {
|
||||
g.logger.Debugw("Error executing query", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed, "rows", rows, "err", err)
|
||||
}
|
||||
default:
|
||||
sql, rows := fc()
|
||||
if rows == -1 {
|
||||
g.logger.Debugw("Statement executed", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed)
|
||||
} else {
|
||||
g.logger.Debugw("Statement executed", "sql", sql, "start_time", begin.Format(time.RFC3339), "duration", elapsed, "rows", rows)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,26 +14,26 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
// it's a good practice to copy the struct inside the function,
|
||||
// so side effects are prevented if the original struct changes during the time
|
||||
type ServerConfig struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Key string `gorm:"primaryKey"`
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type Tailnet struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string `gorm:"type:varchar(64);unique_index"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
Name string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
DNSConfig domain.DNSConfig
|
||||
IAMPolicy domain.IAMPolicy
|
||||
ACLPolicy domain.ACLPolicy
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
ExternalID string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
Name string
|
||||
UserType domain.UserType
|
||||
TailnetID uint64
|
||||
@@ -43,8 +43,8 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
}
|
||||
|
||||
type SystemApiKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
Hash string
|
||||
|
||||
CreatedAt time.Time
|
||||
@@ -55,8 +55,8 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
}
|
||||
|
||||
type ApiKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
Hash string
|
||||
|
||||
CreatedAt time.Time
|
||||
@@ -70,8 +70,8 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
}
|
||||
|
||||
type AuthKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
Hash string
|
||||
Ephemeral bool
|
||||
Tags domain.Tags
|
||||
@@ -87,7 +87,7 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
Name string
|
||||
NameIdx uint64
|
||||
MachineKey string
|
||||
@@ -117,8 +117,8 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
}
|
||||
|
||||
type RegistrationRequest struct {
|
||||
MachineKey string `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
MachineKey string `gorm:"primaryKey;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
Data domain.RegistrationRequestData
|
||||
CreatedAt time.Time
|
||||
Authenticated bool
|
||||
@@ -126,7 +126,7 @@ func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
}
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
Key string `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"primaryKey;autoIncrement:false"`
|
||||
Token string
|
||||
TailnetID *uint64
|
||||
Error string
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202209251530_add_autoallowips_column() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202209251530",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Machine struct {
|
||||
AutoAllowIPs domain.AllowIPs
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Machine{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202209251532_add_alias_column() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202209251532a",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
Alias *string `gorm:"type:varchar(64)"`
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Tailnet{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func m202229251530_add_alias_column_constraint() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202209251532b",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Alias *string `gorm:"uniqueIndex"`
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Tailnet{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202210040828_add_derpmap_colum() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202210040828",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Alias *string `gorm:"uniqueIndex"`
|
||||
DERPMap domain.DERPMap
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Tailnet{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202210070814_add_filesharing_and_servicecollection_columns() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202210070814",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Alias *string `gorm:"uniqueIndex"`
|
||||
ServiceCollectionEnabled bool
|
||||
FileSharingEnabled bool
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Tailnet{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func m202210080700_ssh_action_request() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202210080700",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Alias *string `gorm:"uniqueIndex"`
|
||||
SSHEnabled bool
|
||||
}
|
||||
|
||||
type SSHActionRequest struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Action string
|
||||
SrcMachineID uint64
|
||||
DstMachineID uint64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Tailnet{},
|
||||
&SSHActionRequest{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202211031100_add_authorized_column() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202211031100",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
MachineAuthorizationEnabled bool
|
||||
}
|
||||
|
||||
type AuthKey struct {
|
||||
PreAuthorized bool
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
Authorized bool `gorm:"default:true"`
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Tailnet{},
|
||||
&AuthKey{},
|
||||
&Machine{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202212201300_add_user_id_column() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202212201300",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type RegistrationRequest struct {
|
||||
Key string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
UserID uint64
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&RegistrationRequest{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202212270800_machine_indeces() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202212270800",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Machine struct {
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false;index:idx_tailnet_id_id,priority:2"`
|
||||
MachineKey string `gorm:"index:idx_machine_keys"`
|
||||
NodeKey string `gorm:"index:idx_machine_keys"`
|
||||
|
||||
Name string `gorm:"index:idx_tailnet_id_name,priority:2"`
|
||||
NameIdx uint64 `gorm:"index:idx_tailnet_id_name,sort:desc,priority:3"`
|
||||
|
||||
TailnetID uint64 `gorm:"index:idx_tailnet_id_id,priority:1;index:idx_tailnet_id_name,priority:1"`
|
||||
|
||||
IPv4 domain.IP `gorm:"index:idx_ipv4"`
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Machine{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func m202312271200_account_last_authenticated() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202312271200",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Account struct {
|
||||
LastAuthenticated *time.Time
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Account{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202312290900_machine_indeces() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202312290900",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Machine struct {
|
||||
Name string `gorm:"index:idx_tailnet_id_name,unique,priority:2"`
|
||||
NameIdx uint64 `gorm:"index:idx_tailnet_id_name,unique,sort:desc,priority:3"`
|
||||
}
|
||||
|
||||
db.Migrator().DropIndex(&Machine{}, "idx_tailnet_id_name")
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Machine{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202401061400_machine_indeces() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202401061400",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Machine struct {
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement:false;index:idx_tailnet_id_id,priority:2"`
|
||||
Name string `gorm:"index:idx_tailnet_id_name,unique,priority:2"`
|
||||
NameIdx uint64 `gorm:"index:idx_tailnet_id_name,unique,sort:desc,priority:3"`
|
||||
TailnetID uint64 `gorm:"index:idx_tailnet_id_id,priority:1;index:idx_tailnet_id_name,priority:1"`
|
||||
}
|
||||
|
||||
db.Migrator().DropIndex(&Machine{}, "idx_tailnet_id_name")
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Machine{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func m202402120800_user_last_authenticated() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202402120800",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type User struct {
|
||||
LastAuthenticated *time.Time
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&User{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202403130830_json_to_text() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202403130830",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Tailnet struct {
|
||||
IAMPolicy string
|
||||
ACLPolicy string
|
||||
}
|
||||
|
||||
if err := db.Migrator().AlterColumn(&Tailnet{}, "IAMPolicy"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Migrator().AlterColumn(&Tailnet{}, "ACLPolicy"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,20 @@ import (
|
||||
func Migrations() []*gormigrate.Migration {
|
||||
var migrations = []*gormigrate.Migration{
|
||||
m202209070900_initial_schema(),
|
||||
m202209251530_add_autoallowips_column(),
|
||||
m202209251532_add_alias_column(),
|
||||
m202229251530_add_alias_column_constraint(),
|
||||
m202210040828_add_derpmap_colum(),
|
||||
m202210070814_add_filesharing_and_servicecollection_columns(),
|
||||
m202210080700_ssh_action_request(),
|
||||
m202211031100_add_authorized_column(),
|
||||
m202212201300_add_user_id_column(),
|
||||
m202212270800_machine_indeces(),
|
||||
m202312271200_account_last_authenticated(),
|
||||
m202312290900_machine_indeces(),
|
||||
m202401061400_machine_indeces(),
|
||||
m202402120800_user_last_authenticated(),
|
||||
m202403130830_json_to_text(),
|
||||
}
|
||||
return migrations
|
||||
}
|
||||
|
||||
@@ -11,26 +11,20 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newPostgresDB(config *config.Database, g *gorm.Config) (db, error) {
|
||||
func newPostgresDB(config *config.Database, g *gorm.Config) (*gorm.DB, dbLock, error) {
|
||||
db, err := gorm.Open(postgres.Open(config.Url), g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &Postgres{
|
||||
db: db,
|
||||
}, nil
|
||||
return db, &pgLock{db: db}, nil
|
||||
}
|
||||
|
||||
type Postgres struct {
|
||||
type pgLock struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (s *Postgres) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Postgres) Lock() error {
|
||||
func (s *pgLock) Lock() error {
|
||||
d, _ := s.db.DB()
|
||||
|
||||
query := `SELECT pg_advisory_lock($1)`
|
||||
@@ -42,7 +36,14 @@ func (s *Postgres) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Postgres) Unlock() error {
|
||||
func (s *pgLock) UnlockErr(prevErr error) error {
|
||||
if err := s.unlock(); err != nil {
|
||||
return multierror.Append(prevErr, err)
|
||||
}
|
||||
return prevErr
|
||||
}
|
||||
|
||||
func (s *pgLock) unlock() error {
|
||||
d, _ := s.db.DB()
|
||||
|
||||
query := `SELECT pg_advisory_unlock($1)`
|
||||
@@ -53,16 +54,9 @@ func (s *Postgres) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Postgres) UnlockErr(prevErr error) error {
|
||||
if err := s.Unlock(); err != nil {
|
||||
return multierror.Append(prevErr, err)
|
||||
}
|
||||
return prevErr
|
||||
}
|
||||
|
||||
const advisoryLockIDSalt uint = 1486364155
|
||||
|
||||
func (s *Postgres) generateAdvisoryLockId() string {
|
||||
func (s *pgLock) generateAdvisoryLockId() string {
|
||||
sum := crc32.ChecksumIEEE([]byte("ionscale_migration"))
|
||||
sum = sum * uint32(advisoryLockIDSalt)
|
||||
return fmt.Sprint(sum)
|
||||
|
||||
@@ -6,33 +6,21 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newSqliteDB(config *config.Database, g *gorm.Config) (db, error) {
|
||||
func newSqliteDB(config *config.Database, g *gorm.Config) (*gorm.DB, dbLock, error) {
|
||||
db, err := gorm.Open(sqlite.Open(config.Url), g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &Sqlite{
|
||||
db: db,
|
||||
}, nil
|
||||
return db, &sqliteLock{}, nil
|
||||
}
|
||||
|
||||
type Sqlite struct {
|
||||
db *gorm.DB
|
||||
type sqliteLock struct {
|
||||
}
|
||||
|
||||
func (s *Sqlite) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Sqlite) Lock() error {
|
||||
func (s *sqliteLock) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) UnlockErr(prevErr error) error {
|
||||
func (s *sqliteLock) UnlockErr(prevErr error) error {
|
||||
return prevErr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package derp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"os"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func LoadDERPSources(c *config.Config) (*tailcfg.DERPMap, error) {
|
||||
derpMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{},
|
||||
}
|
||||
|
||||
var merr *multierror.Error
|
||||
for _, src := range c.DERP.Sources {
|
||||
dm, err := loadDERPSource(src)
|
||||
if err != nil {
|
||||
merr = multierror.Append(merr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for id, r := range dm.Regions {
|
||||
derpMap.Regions[id] = r
|
||||
}
|
||||
}
|
||||
|
||||
if !c.DERP.Server.Disabled {
|
||||
dm := c.DefaultDERPMap()
|
||||
for id, r := range dm.Regions {
|
||||
derpMap.Regions[id] = r
|
||||
}
|
||||
}
|
||||
|
||||
return derpMap, merr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func loadDERPSource(src string) (*tailcfg.DERPMap, error) {
|
||||
temp, err := os.CreateTemp(os.TempDir(), "derp-*.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
if err := getter.Get(temp.Name(), src, getter.WithMode(getter.ClientModeFile)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(temp.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dm tailcfg.DERPMap
|
||||
|
||||
if err := json.Unmarshal(content, &dm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dm, nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/mapping"
|
||||
"github.com/libdns/azure"
|
||||
"github.com/libdns/cloudflare"
|
||||
"github.com/libdns/digitalocean"
|
||||
"github.com/libdns/googleclouddns"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/libdns/route53"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
SetRecord(ctx context.Context, recordType, recordName, value string) error
|
||||
}
|
||||
|
||||
func NewProvider(config config.DNS) (Provider, error) {
|
||||
p := config.Provider
|
||||
if len(p.Zone) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(config.MagicDNSSuffix, p.Zone) {
|
||||
return nil, fmt.Errorf("invalid MagicDNS suffix [%s], not part of zone [%s]", config.MagicDNSSuffix, p.Zone)
|
||||
}
|
||||
|
||||
switch p.Name {
|
||||
case "azure":
|
||||
return configureAzureProvider(p.Zone, p.Configuration)
|
||||
case "cloudflare":
|
||||
return configureCloudflareProvider(p.Zone, p.Configuration)
|
||||
case "digitalocean":
|
||||
return configureDigitalOceanProvider(p.Zone, p.Configuration)
|
||||
case "googleclouddns":
|
||||
return configureGoogleCloudDNSProvider(p.Zone, p.Configuration)
|
||||
case "route53":
|
||||
return configureRoute53Provider(p.Zone, p.Configuration)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown dns provider: %s", p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func configureAzureProvider(zone string, values map[string]string) (Provider, error) {
|
||||
p := &azure.Provider{}
|
||||
if err := mapping.CopyViaJson(values, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &azure.Provider{
|
||||
TenantId: config.GetString("IONSCALE_DNS_AZURE_TENANT_ID", ""),
|
||||
ClientId: config.GetString("IONSCALE_DNS_AZURE_CLIENT_ID", ""),
|
||||
ClientSecret: config.GetString("IONSCALE_DNS_AZURE_CLIENT_SECRET", ""),
|
||||
SubscriptionId: config.GetString("IONSCALE_DNS_AZURE_SUBSCRIPTION_ID", ""),
|
||||
ResourceGroupName: config.GetString("IONSCALE_DNS_AZURE_RESOURCE_GROUP_NAME", ""),
|
||||
}
|
||||
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(p, e, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &externalProvider{zone: zone, setter: p}, nil
|
||||
}
|
||||
|
||||
func configureCloudflareProvider(zone string, values map[string]string) (Provider, error) {
|
||||
p := &cloudflare.Provider{}
|
||||
if err := mapping.CopyViaJson(values, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &cloudflare.Provider{
|
||||
APIToken: config.GetString("IONSCALE_DNS_CLOUDFLARE_API_TOKEN", ""),
|
||||
}
|
||||
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(p, e, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &externalProvider{zone: zone, setter: p}, nil
|
||||
}
|
||||
|
||||
func configureDigitalOceanProvider(zone string, values map[string]string) (Provider, error) {
|
||||
p := &digitalocean.Provider{}
|
||||
if err := mapping.CopyViaJson(values, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &digitalocean.Provider{
|
||||
APIToken: config.GetString("IONSCALE_DNS_DIGITALOCEAN_API_TOKEN", ""),
|
||||
}
|
||||
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(p, e, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &externalProvider{zone: zone, setter: p}, nil
|
||||
}
|
||||
|
||||
func configureGoogleCloudDNSProvider(zone string, values map[string]string) (Provider, error) {
|
||||
p := &googleclouddns.Provider{}
|
||||
if err := mapping.CopyViaJson(values, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &googleclouddns.Provider{
|
||||
Project: config.GetString("IONSCALE_DNS_GOOGLECLOUDDNS_PROJECT", ""),
|
||||
ServiceAccountJSON: config.GetString("IONSCALE_DNS_GOOGLECLOUDDNS_SERVICE_ACCOUNT_JSON", ""),
|
||||
}
|
||||
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(p, e, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &externalProvider{zone: zone, setter: p}, nil
|
||||
}
|
||||
|
||||
func configureRoute53Provider(zone string, values map[string]string) (Provider, error) {
|
||||
p := &route53.Provider{}
|
||||
if err := mapping.CopyViaJson(values, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &route53.Provider{
|
||||
MaxRetries: 0,
|
||||
MaxWaitDur: 0,
|
||||
WaitForPropagation: false,
|
||||
Region: config.GetString("IONSCALE_DNS_ROUTE53_REGION", ""),
|
||||
AWSProfile: config.GetString("IONSCALE_DNS_ROUTE53_AWS_PROFILE", ""),
|
||||
AccessKeyId: config.GetString("IONSCALE_DNS_ROUTE53_ACCESS_KEY_ID", ""),
|
||||
SecretAccessKey: config.GetString("IONSCALE_DNS_ROUTE53_SECRET_ACCESS_KEY", ""),
|
||||
Token: config.GetString("IONSCALE_DNS_ROUTE53_TOKEN", ""),
|
||||
}
|
||||
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(p, e, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &externalProvider{zone: zone, setter: p}, nil
|
||||
}
|
||||
|
||||
type externalProvider struct {
|
||||
zone string
|
||||
setter libdns.RecordSetter
|
||||
}
|
||||
|
||||
func (p *externalProvider) SetRecord(ctx context.Context, recordType, recordName, value string) error {
|
||||
_, err := p.setter.SetRecords(ctx, fmt.Sprintf("%s.", p.zone), []libdns.Record{{
|
||||
Type: recordType,
|
||||
Name: strings.TrimSuffix(recordName, p.zone),
|
||||
Value: value,
|
||||
TTL: 1 * time.Minute,
|
||||
}})
|
||||
return err
|
||||
}
|
||||
@@ -5,8 +5,15 @@ import (
|
||||
"errors"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AccountRepository interface {
|
||||
GetAccount(ctx context.Context, accountID uint64) (*Account, error)
|
||||
GetOrCreateAccount(ctx context.Context, externalID, loginName string) (*Account, bool, error)
|
||||
SetAccountLastAuthenticated(ctx context.Context, accountID uint64) error
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
ExternalID string
|
||||
@@ -43,3 +50,17 @@ func (r *repository) GetAccount(ctx context.Context, id uint64) (*Account, error
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (r *repository) SetAccountLastAuthenticated(ctx context.Context, accountID uint64) error {
|
||||
now := time.Now().UTC()
|
||||
tx := r.withContext(ctx).
|
||||
Model(Account{}).
|
||||
Where("id = ?", accountID).
|
||||
Updates(map[string]interface{}{"last_authenticated": &now})
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+268
-203
@@ -5,253 +5,190 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
AutoGroupSelf = "autogroup:self"
|
||||
AutoGroupMember = "autogroup:member"
|
||||
AutoGroupMembers = "autogroup:members"
|
||||
AutoGroupTagged = "autogroup:tagged"
|
||||
AutoGroupInternet = "autogroup:internet"
|
||||
)
|
||||
|
||||
type AutoApprovers struct {
|
||||
Routes map[string][]string `json:"routes,omitempty"`
|
||||
ExitNode []string `json:"exitNode,omitempty"`
|
||||
}
|
||||
|
||||
type ACLPolicy struct {
|
||||
Groups map[string][]string `json:"groups,omitempty"`
|
||||
Hosts map[string]string `json:"hosts,omitempty"`
|
||||
ACLs []ACL `json:"acls"`
|
||||
TagOwners map[string][]string `json:"tag_owners"`
|
||||
ionscale.ACLPolicy
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
Action string `json:"action"`
|
||||
Src []string `json:"src"`
|
||||
Dst []string `json:"dst"`
|
||||
}
|
||||
|
||||
func DefaultPolicy() ACLPolicy {
|
||||
return ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"*:*"},
|
||||
},
|
||||
},
|
||||
func (a *ACLPolicy) Equal(x *ACLPolicy) bool {
|
||||
if a == nil && x == nil {
|
||||
return true
|
||||
}
|
||||
if (a == nil) != (x == nil) {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(a, x)
|
||||
}
|
||||
|
||||
func (a ACLPolicy) CheckTags(tags []string) error {
|
||||
var result *multierror.Error
|
||||
for _, t := range tags {
|
||||
if _, ok := a.TagOwners[t]; !ok {
|
||||
result = multierror.Append(result, fmt.Errorf("tag [%s] is invalid or not permitted", t))
|
||||
func (a ACLPolicy) FindAutoApprovedIPs(routableIPs []netip.Prefix, tags []string, u *User) []netip.Prefix {
|
||||
if a.AutoApprovers == nil || len(routableIPs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
matches := func(values []string) bool {
|
||||
for _, alias := range values {
|
||||
if alias == u.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
group, ok := a.Groups[alias]
|
||||
if ok {
|
||||
for _, g := range group {
|
||||
if g == u.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") {
|
||||
for _, tag := range tags {
|
||||
if alias == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
isAutoApproved := func(candidate netip.Prefix, approvedIPs []netip.Prefix) bool {
|
||||
for _, approvedIP := range approvedIPs {
|
||||
if candidate.Bits() >= approvedIP.Bits() && approvedIP.Contains(candidate.Masked().Addr()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var autoApprovedIPs []netip.Prefix
|
||||
for route, autoApprovers := range a.AutoApprovers.Routes {
|
||||
candidate, err := netip.ParsePrefix(route)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if matches(autoApprovers) {
|
||||
autoApprovedIPs = append(autoApprovedIPs, candidate)
|
||||
}
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
|
||||
var result []netip.Prefix
|
||||
for _, c := range routableIPs {
|
||||
if c.Bits() == 0 && matches(a.AutoApprovers.ExitNode) {
|
||||
result = append(result, c)
|
||||
}
|
||||
if isAutoApproved(c, autoApprovedIPs) {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a ACLPolicy) CheckTagOwners(tags []string, p *User) error {
|
||||
var result *multierror.Error
|
||||
for _, t := range tags {
|
||||
if ok := a.IsTagOwner(t, p); !ok {
|
||||
if ok := a.isTagOwner(t, p); !ok {
|
||||
result = multierror.Append(result, fmt.Errorf("tag [%s] is invalid or not permitted", t))
|
||||
}
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (a ACLPolicy) IsTagOwner(tag string, p *User) bool {
|
||||
func (a ACLPolicy) isTagOwner(tag string, p *User) bool {
|
||||
if p.UserType == UserTypeService {
|
||||
return true
|
||||
}
|
||||
if tagOwners, ok := a.TagOwners[tag]; ok {
|
||||
if p.UserType == UserTypeService {
|
||||
return true
|
||||
for _, alias := range tagOwners {
|
||||
if strings.HasPrefix(alias, "group:") {
|
||||
if group, ok := a.Groups[alias]; ok {
|
||||
return slices.Contains(group, p.Name)
|
||||
}
|
||||
} else {
|
||||
if alias == p.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return a.validateTagOwners(tagOwners, p)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a ACLPolicy) validateTagOwners(tagOwners []string, p *User) bool {
|
||||
for _, alias := range tagOwners {
|
||||
if strings.HasPrefix(alias, "group:") {
|
||||
if group, ok := a.Groups[alias]; ok {
|
||||
for _, groupMember := range group {
|
||||
if groupMember == p.Name {
|
||||
func (a ACLPolicy) NodeCapabilities(m *Machine) []tailcfg.NodeCapability {
|
||||
var result = &StringSet{}
|
||||
|
||||
matches := func(targets []string) bool {
|
||||
for _, alias := range targets {
|
||||
if alias == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(alias, "@") && !m.HasTags() && m.HasUser(alias) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") && !m.HasTags() {
|
||||
for _, u := range a.Groups[alias] {
|
||||
if m.HasUser(u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if alias == p.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
for _, nodeAddr := range a.NodeAttrs {
|
||||
if matches(nodeAddr.Target) {
|
||||
result.Add(nodeAddr.Attr...)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
items := result.Items()
|
||||
caps := make([]tailcfg.NodeCapability, len(items))
|
||||
for i, c := range items {
|
||||
caps[i] = tailcfg.NodeCapability(c)
|
||||
}
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
|
||||
if !src.HasTags() && !dest.HasTags() && dest.HasUser(src.User.Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, acl := range a.ACLs {
|
||||
allDestPorts := a.expandMachineToDstPorts(dest, acl.Dst)
|
||||
if len(allDestPorts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, alias := range acl.Src {
|
||||
if len(a.expandMachineAlias(src, alias, true)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a ACLPolicy) BuildFilterRules(srcs []Machine, dst *Machine) []tailcfg.FilterRule {
|
||||
var rules []tailcfg.FilterRule
|
||||
|
||||
for _, acl := range a.ACLs {
|
||||
allDestPorts := a.expandMachineToDstPorts(dst, acl.Dst)
|
||||
if len(allDestPorts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var allSrcIPsSet = &StringSet{}
|
||||
for _, alias := range acl.Src {
|
||||
for _, src := range srcs {
|
||||
srcIPs := a.expandMachineAlias(&src, alias, true)
|
||||
allSrcIPsSet.Add(srcIPs...)
|
||||
}
|
||||
}
|
||||
|
||||
allSrcIPs := allSrcIPsSet.Items()
|
||||
|
||||
if len(allSrcIPs) == 0 {
|
||||
allSrcIPs = nil
|
||||
}
|
||||
|
||||
rule := tailcfg.FilterRule{
|
||||
SrcIPs: allSrcIPs,
|
||||
DstPorts: allDestPorts,
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
if len(rules) == 0 {
|
||||
return []tailcfg.FilterRule{{}}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func (a ACLPolicy) expandMachineToDstPorts(m *Machine, ports []string) []tailcfg.NetPortRange {
|
||||
allDestRanges := []tailcfg.NetPortRange{}
|
||||
for _, d := range ports {
|
||||
ranges := a.expandMachineDestToNetPortRanges(m, d)
|
||||
allDestRanges = append(allDestRanges, ranges...)
|
||||
}
|
||||
return allDestRanges
|
||||
}
|
||||
|
||||
func (a ACLPolicy) expandMachineDestToNetPortRanges(m *Machine, dest string) []tailcfg.NetPortRange {
|
||||
tokens := strings.Split(dest, ":")
|
||||
if len(tokens) < 2 || len(tokens) > 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var alias string
|
||||
if len(tokens) == 2 {
|
||||
alias = tokens[0]
|
||||
} else {
|
||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||
}
|
||||
|
||||
ports, err := a.expandValuePortToPortRange(tokens[len(tokens)-1])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ips := a.expandMachineAlias(m, alias, false)
|
||||
if len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dests := []tailcfg.NetPortRange{}
|
||||
for _, d := range ips {
|
||||
for _, p := range ports {
|
||||
pr := tailcfg.NetPortRange{
|
||||
IP: d,
|
||||
Ports: p,
|
||||
}
|
||||
dests = append(dests, pr)
|
||||
}
|
||||
}
|
||||
|
||||
return dests
|
||||
}
|
||||
|
||||
func (a ACLPolicy) expandMachineAlias(m *Machine, alias string, src bool) []string {
|
||||
if alias == "*" {
|
||||
if alias == "*" {
|
||||
return []string{"*"}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(alias, "@") && !m.HasTags() && m.HasUser(alias) {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") && !m.HasTags() {
|
||||
users, ok := a.Groups[alias]
|
||||
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if m.HasUser(u) {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias) {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
}
|
||||
|
||||
if h, ok := a.Hosts[alias]; ok {
|
||||
alias = h
|
||||
}
|
||||
|
||||
if src {
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil && m.HasIP(ip) {
|
||||
return []string{ip.String()}
|
||||
}
|
||||
} else {
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil && m.IsAllowedIP(ip) {
|
||||
return []string{ip.String()}
|
||||
}
|
||||
|
||||
prefix, err := netip.ParsePrefix(alias)
|
||||
if err == nil && m.IsAllowedIPPrefix(prefix) {
|
||||
return []string{prefix.String()}
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (a ACLPolicy) expandValuePortToPortRange(s string) ([]tailcfg.PortRange, error) {
|
||||
func (a ACLPolicy) parsePortRanges(s string) ([]tailcfg.PortRange, error) {
|
||||
if s == "*" {
|
||||
return []tailcfg.PortRange{{First: 0, Last: 65535}}, nil
|
||||
return []tailcfg.PortRange{tailcfg.PortRangeAny}, nil
|
||||
}
|
||||
|
||||
ports := []tailcfg.PortRange{}
|
||||
var ports []tailcfg.PortRange
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
rang := strings.Split(p, "-")
|
||||
if len(rang) == 1 {
|
||||
@@ -283,6 +220,25 @@ func (a ACLPolicy) expandValuePortToPortRange(s string) ([]tailcfg.PortRange, er
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
func (a ACLPolicy) isGroupMember(group string, m *Machine) bool {
|
||||
if m.HasTags() {
|
||||
return false
|
||||
}
|
||||
|
||||
users, ok := a.Groups[group]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if m.HasUser(u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *ACLPolicy) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
@@ -311,6 +267,57 @@ func (ACLPolicy) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
protocolICMP = 1 // Internet Control Message
|
||||
protocolIGMP = 2 // Internet Group Management
|
||||
protocolIPv4 = 4 // IPv4 encapsulation
|
||||
protocolTCP = 6 // Transmission Control
|
||||
protocolEGP = 8 // Exterior Gateway Protocol
|
||||
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
|
||||
protocolUDP = 17 // User Datagram
|
||||
protocolGRE = 47 // Generic Routing Encapsulation
|
||||
protocolESP = 50 // Encap Security Payload
|
||||
protocolAH = 51 // Authentication Header
|
||||
protocolIPv6ICMP = 58 // ICMP for IPv6
|
||||
protocolSCTP = 132 // Stream Control Transmission Protocol
|
||||
)
|
||||
|
||||
func parseProtocol(protocol string) []int {
|
||||
switch protocol {
|
||||
case "":
|
||||
return nil
|
||||
case "igmp":
|
||||
return []int{protocolIGMP}
|
||||
case "ipv4", "ip-in-ip":
|
||||
return []int{protocolIPv4}
|
||||
case "tcp":
|
||||
return []int{protocolTCP}
|
||||
case "egp":
|
||||
return []int{protocolEGP}
|
||||
case "igp":
|
||||
return []int{protocolIGP}
|
||||
case "udp":
|
||||
return []int{protocolUDP}
|
||||
case "gre":
|
||||
return []int{protocolGRE}
|
||||
case "esp":
|
||||
return []int{protocolESP}
|
||||
case "ah":
|
||||
return []int{protocolAH}
|
||||
case "sctp":
|
||||
return []int{protocolSCTP}
|
||||
case "icmp":
|
||||
return []int{protocolICMP, protocolIPv6ICMP}
|
||||
|
||||
default:
|
||||
n, err := strconv.Atoi(protocol)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return []int{n}
|
||||
}
|
||||
}
|
||||
|
||||
type StringSet struct {
|
||||
items map[string]bool
|
||||
}
|
||||
@@ -332,5 +339,63 @@ func (s *StringSet) Items() []string {
|
||||
for i := range s.items {
|
||||
items = append(items, i)
|
||||
}
|
||||
sort.Strings(items)
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *StringSet) Empty() bool {
|
||||
return len(s.items) == 0
|
||||
}
|
||||
|
||||
func autogroupInternetRanges() []string {
|
||||
return []string{
|
||||
"0.0.0.0/5",
|
||||
"8.0.0.0/7",
|
||||
"11.0.0.0/8",
|
||||
"12.0.0.0/6",
|
||||
"16.0.0.0/4",
|
||||
"32.0.0.0/3",
|
||||
"64.0.0.0/3",
|
||||
"96.0.0.0/6",
|
||||
"100.0.0.0/10",
|
||||
"100.128.0.0/9",
|
||||
"101.0.0.0/8",
|
||||
"102.0.0.0/7",
|
||||
"104.0.0.0/5",
|
||||
"112.0.0.0/4",
|
||||
"128.0.0.0/3",
|
||||
"160.0.0.0/5",
|
||||
"168.0.0.0/8",
|
||||
"169.0.0.0/9",
|
||||
"169.128.0.0/10",
|
||||
"169.192.0.0/11",
|
||||
"169.224.0.0/12",
|
||||
"169.240.0.0/13",
|
||||
"169.248.0.0/14",
|
||||
"169.252.0.0/15",
|
||||
"169.255.0.0/16",
|
||||
"170.0.0.0/7",
|
||||
"172.0.0.0/12",
|
||||
"172.32.0.0/11",
|
||||
"172.64.0.0/10",
|
||||
"172.128.0.0/9",
|
||||
"173.0.0.0/8",
|
||||
"174.0.0.0/7",
|
||||
"176.0.0.0/4",
|
||||
"192.0.0.0/9",
|
||||
"192.128.0.0/11",
|
||||
"192.160.0.0/13",
|
||||
"192.169.0.0/16",
|
||||
"192.170.0.0/15",
|
||||
"192.172.0.0/14",
|
||||
"192.176.0.0/12",
|
||||
"192.192.0.0/10",
|
||||
"193.0.0.0/8",
|
||||
"194.0.0.0/7",
|
||||
"196.0.0.0/6",
|
||||
"200.0.0.0/5",
|
||||
"208.0.0.0/4",
|
||||
"224.0.0.0/3",
|
||||
"2000::/3",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
|
||||
if !src.HasTags() && !dest.HasTags() && dest.HasUser(src.User.Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, acl := range a.ACLs {
|
||||
selfDestPorts, allDestPorts := a.translateDestinationAliasesToMachineNetPortRanges(acl.Destination, dest)
|
||||
if len(selfDestPorts) != 0 {
|
||||
for _, alias := range acl.Source {
|
||||
if len(a.translateSourceAliasToMachineIPs(alias, src, &dest.User)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(allDestPorts) != 0 {
|
||||
for _, alias := range acl.Source {
|
||||
if len(a.translateSourceAliasToMachineIPs(alias, src, nil)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, grant := range a.Grants {
|
||||
selfIps, otherIps := a.translateDestinationAliasesToMachineIPs(grant.Destination, dest)
|
||||
if len(selfIps) != 0 {
|
||||
for _, alias := range grant.Source {
|
||||
if len(a.translateSourceAliasToMachineIPs(alias, src, &dest.User)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(otherIps) != 0 {
|
||||
for _, alias := range grant.Source {
|
||||
if len(a.translateSourceAliasToMachineIPs(alias, src, nil)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a ACLPolicy) BuildFilterRules(peers []Machine, dst *Machine) []tailcfg.FilterRule {
|
||||
var rules = make([]tailcfg.FilterRule, 0)
|
||||
|
||||
matchSourceAndAppendRule := func(rules []tailcfg.FilterRule, aliases []string, preparedRules []tailcfg.FilterRule, u *User) []tailcfg.FilterRule {
|
||||
if len(preparedRules) == 0 {
|
||||
return rules
|
||||
}
|
||||
|
||||
var allSrcIPsSet = &StringSet{}
|
||||
for _, alias := range aliases {
|
||||
for _, peer := range peers {
|
||||
allSrcIPsSet.Add(a.translateSourceAliasToMachineIPs(alias, &peer, u)...)
|
||||
}
|
||||
}
|
||||
|
||||
if allSrcIPsSet.Empty() {
|
||||
return rules
|
||||
}
|
||||
|
||||
allSrcIPs := allSrcIPsSet.Items()
|
||||
|
||||
if len(allSrcIPs) == 0 {
|
||||
return rules
|
||||
}
|
||||
|
||||
for _, pr := range preparedRules {
|
||||
rules = append(rules, tailcfg.FilterRule{
|
||||
SrcIPs: allSrcIPs,
|
||||
DstPorts: pr.DstPorts,
|
||||
IPProto: pr.IPProto,
|
||||
CapGrant: pr.CapGrant,
|
||||
})
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
for _, acl := range a.ACLs {
|
||||
self, other := a.prepareFilterRulesFromACL(dst, acl)
|
||||
rules = matchSourceAndAppendRule(rules, acl.Source, self, &dst.User)
|
||||
rules = matchSourceAndAppendRule(rules, acl.Source, other, nil)
|
||||
}
|
||||
|
||||
for _, acl := range a.Grants {
|
||||
self, other := a.prepareFilterRulesFromGrant(dst, acl)
|
||||
rules = matchSourceAndAppendRule(rules, acl.Source, self, &dst.User)
|
||||
rules = matchSourceAndAppendRule(rules, acl.Source, other, nil)
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func (a ACLPolicy) prepareFilterRulesFromACL(candidate *Machine, acl ionscale.ACLEntry) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
|
||||
proto := parseProtocol(acl.Protocol)
|
||||
|
||||
selfDstPorts, otherDstPorts := a.translateDestinationAliasesToMachineNetPortRanges(acl.Destination, candidate)
|
||||
|
||||
var selfFilterRules []tailcfg.FilterRule
|
||||
var otherFilterRules []tailcfg.FilterRule
|
||||
|
||||
if len(selfDstPorts) != 0 {
|
||||
selfFilterRules = append(selfFilterRules, tailcfg.FilterRule{IPProto: proto, DstPorts: selfDstPorts})
|
||||
}
|
||||
|
||||
if len(otherDstPorts) != 0 {
|
||||
otherFilterRules = append(otherFilterRules, tailcfg.FilterRule{IPProto: proto, DstPorts: otherDstPorts})
|
||||
}
|
||||
|
||||
return selfFilterRules, otherFilterRules
|
||||
}
|
||||
|
||||
func (a ACLPolicy) prepareFilterRulesFromGrant(candidate *Machine, grant ionscale.ACLGrant) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
|
||||
selfIPs, otherIPs := a.translateDestinationAliasesToMachineIPs(grant.Destination, candidate)
|
||||
|
||||
var selfFilterRules []tailcfg.FilterRule
|
||||
var otherFilterRules []tailcfg.FilterRule
|
||||
|
||||
for _, ip := range grant.IP {
|
||||
if len(selfIPs) != 0 {
|
||||
ranges := make([]tailcfg.NetPortRange, len(selfIPs))
|
||||
for i, s := range selfIPs {
|
||||
ranges[i] = tailcfg.NetPortRange{IP: s, Ports: ip.Ports}
|
||||
}
|
||||
|
||||
rule := tailcfg.FilterRule{DstPorts: ranges}
|
||||
if ip.Proto != 0 {
|
||||
rule.IPProto = []int{ip.Proto}
|
||||
}
|
||||
|
||||
selfFilterRules = append(selfFilterRules, rule)
|
||||
}
|
||||
|
||||
if len(otherIPs) != 0 {
|
||||
ranges := make([]tailcfg.NetPortRange, len(otherIPs))
|
||||
for i, s := range otherIPs {
|
||||
ranges[i] = tailcfg.NetPortRange{IP: s, Ports: ip.Ports}
|
||||
}
|
||||
|
||||
rule := tailcfg.FilterRule{DstPorts: ranges}
|
||||
if ip.Proto != 0 {
|
||||
rule.IPProto = []int{ip.Proto}
|
||||
}
|
||||
|
||||
otherFilterRules = append(otherFilterRules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
if len(grant.App) != 0 {
|
||||
selfPrefixes, otherPrefixes := appGrantDstIpsToPrefixes(candidate, selfIPs, otherIPs)
|
||||
if len(selfPrefixes) != 0 {
|
||||
rule := tailcfg.FilterRule{CapGrant: []tailcfg.CapGrant{{Dsts: selfPrefixes, CapMap: grant.App}}}
|
||||
selfFilterRules = append(selfFilterRules, rule)
|
||||
}
|
||||
|
||||
if len(otherPrefixes) != 0 {
|
||||
rule := tailcfg.FilterRule{CapGrant: []tailcfg.CapGrant{{Dsts: otherPrefixes, CapMap: grant.App}}}
|
||||
otherFilterRules = append(otherFilterRules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
return selfFilterRules, otherFilterRules
|
||||
}
|
||||
|
||||
func appGrantDstIpsToPrefixes(m *Machine, self []string, other []string) ([]netip.Prefix, []netip.Prefix) {
|
||||
translate := func(ips []string) []netip.Prefix {
|
||||
var prefixes []netip.Prefix
|
||||
for _, ip := range ips {
|
||||
if ip == "*" {
|
||||
prefixes = append(prefixes, netip.PrefixFrom(*m.IPv4.Addr, 32))
|
||||
prefixes = append(prefixes, netip.PrefixFrom(*m.IPv6.Addr, 128))
|
||||
} else {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err == nil && m.HasIP(addr) {
|
||||
if addr.Is4() {
|
||||
prefixes = append(prefixes, netip.PrefixFrom(addr, 32))
|
||||
} else {
|
||||
prefixes = append(prefixes, netip.PrefixFrom(addr, 128))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
return translate(self), translate(other)
|
||||
}
|
||||
|
||||
func (a ACLPolicy) translateDestinationAliasesToMachineIPs(aliases []string, m *Machine) ([]string, []string) {
|
||||
var self = &StringSet{}
|
||||
var other = &StringSet{}
|
||||
for _, alias := range aliases {
|
||||
ips := a.translateDestinationAliasToMachineIPs(alias, m)
|
||||
if alias == AutoGroupSelf {
|
||||
self.Add(ips...)
|
||||
} else {
|
||||
other.Add(ips...)
|
||||
}
|
||||
}
|
||||
return self.Items(), other.Items()
|
||||
}
|
||||
|
||||
func (a ACLPolicy) translateDestinationAliasesToMachineNetPortRanges(aliases []string, m *Machine) ([]tailcfg.NetPortRange, []tailcfg.NetPortRange) {
|
||||
var self []tailcfg.NetPortRange
|
||||
var other []tailcfg.NetPortRange
|
||||
for _, alias := range aliases {
|
||||
ranges := a.translationDestinationAliasToMachineNetPortRanges(alias, m)
|
||||
if strings.HasPrefix(alias, AutoGroupSelf) {
|
||||
self = append(self, ranges...)
|
||||
} else {
|
||||
other = append(other, ranges...)
|
||||
}
|
||||
}
|
||||
return self, other
|
||||
}
|
||||
|
||||
func (a ACLPolicy) translationDestinationAliasToMachineNetPortRanges(alias string, m *Machine) []tailcfg.NetPortRange {
|
||||
lastInd := strings.LastIndex(alias, ":")
|
||||
if lastInd == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ports := alias[lastInd+1:]
|
||||
alias = alias[:lastInd]
|
||||
|
||||
portRanges, err := a.parsePortRanges(ports)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ips := a.translateDestinationAliasToMachineIPs(alias, m)
|
||||
if len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var netPortRanges []tailcfg.NetPortRange
|
||||
for _, d := range ips {
|
||||
for _, p := range portRanges {
|
||||
pr := tailcfg.NetPortRange{
|
||||
IP: d,
|
||||
Ports: p,
|
||||
}
|
||||
netPortRanges = append(netPortRanges, pr)
|
||||
}
|
||||
}
|
||||
|
||||
return netPortRanges
|
||||
}
|
||||
|
||||
func (a ACLPolicy) translateDestinationAliasToMachineIPs(alias string, m *Machine) []string {
|
||||
f := func(alias string, m *Machine) []string {
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil && m.IsAllowedIP(ip) {
|
||||
return []string{ip.String()}
|
||||
}
|
||||
|
||||
prefix, err := netip.ParsePrefix(alias)
|
||||
if err == nil && m.IsAllowedIPPrefix(prefix) {
|
||||
return []string{prefix.String()}
|
||||
}
|
||||
|
||||
return make([]string, 0)
|
||||
}
|
||||
|
||||
return a.translateAliasToMachineIPs(alias, m, nil, f)
|
||||
}
|
||||
|
||||
func (a ACLPolicy) translateSourceAliasToMachineIPs(alias string, m *Machine, u *User) []string {
|
||||
f := func(alias string, m *Machine) []string {
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil && m.HasIP(ip) {
|
||||
return []string{ip.String()}
|
||||
}
|
||||
|
||||
return make([]string, 0)
|
||||
}
|
||||
|
||||
return a.translateAliasToMachineIPs(alias, m, u, f)
|
||||
}
|
||||
|
||||
func (a ACLPolicy) translateAliasToMachineIPs(alias string, m *Machine, u *User, f func(string, *Machine) []string) []string {
|
||||
if u != nil && m.HasTags() {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if u != nil && !m.HasUser(u.Name) {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if alias == "*" && u != nil {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if alias == "*" {
|
||||
return []string{"*"}
|
||||
}
|
||||
|
||||
if alias == AutoGroupMember || alias == AutoGroupMembers || alias == AutoGroupSelf {
|
||||
if !m.HasTags() {
|
||||
return m.IPs()
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
if alias == AutoGroupTagged {
|
||||
if m.HasTags() {
|
||||
return m.IPs()
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
if alias == AutoGroupInternet && m.IsExitNode() {
|
||||
return autogroupInternetRanges()
|
||||
}
|
||||
|
||||
if strings.Contains(alias, "@") && !m.HasTags() && m.HasUser(alias) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") && !m.HasTags() && a.isGroupMember(alias, m) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if h, ok := a.Hosts[alias]; ok {
|
||||
alias = h
|
||||
}
|
||||
|
||||
return f(alias, m)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPolicy {
|
||||
var rules []*tailcfg.SSHRule
|
||||
|
||||
expandSrcAliases := func(aliases []string, action string, u *User) []*tailcfg.SSHPrincipal {
|
||||
var allSrcIPsSet = &StringSet{}
|
||||
for _, alias := range aliases {
|
||||
if strings.HasPrefix(alias, "tag:") && action == "check" {
|
||||
continue
|
||||
}
|
||||
for _, src := range srcs {
|
||||
srcIPs := a.expandSSHSrcAlias(&src, alias, u)
|
||||
allSrcIPsSet.Add(srcIPs...)
|
||||
}
|
||||
}
|
||||
|
||||
var result = []*tailcfg.SSHPrincipal{}
|
||||
for _, i := range allSrcIPsSet.Items() {
|
||||
result = append(result, &tailcfg.SSHPrincipal{NodeIP: i})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
for _, rule := range a.SSH {
|
||||
if rule.Action != "accept" && rule.Action != "check" {
|
||||
continue
|
||||
}
|
||||
|
||||
var action = &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
}
|
||||
|
||||
if rule.Action == "check" {
|
||||
action = &tailcfg.SSHAction{
|
||||
HoldAndDelegate: "https://unused/machine/ssh/action/$SRC_NODE_ID/to/$DST_NODE_ID/" + safeCheckPeriod(rule.CheckPeriod),
|
||||
}
|
||||
}
|
||||
|
||||
selfUsers, otherUsers := a.expandSSHDstToSSHUsers(dst, rule)
|
||||
|
||||
if len(selfUsers) != 0 {
|
||||
principals := expandSrcAliases(rule.Source, rule.Action, &dst.User)
|
||||
if len(principals) != 0 {
|
||||
rules = append(rules, &tailcfg.SSHRule{
|
||||
Principals: principals,
|
||||
SSHUsers: selfUsers,
|
||||
Action: action,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(otherUsers) != 0 {
|
||||
principals := expandSrcAliases(rule.Source, rule.Action, nil)
|
||||
if len(principals) != 0 {
|
||||
rules = append(rules, &tailcfg.SSHRule{
|
||||
Principals: principals,
|
||||
SSHUsers: otherUsers,
|
||||
Action: action,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &tailcfg.SSHPolicy{Rules: rules}
|
||||
}
|
||||
|
||||
func (a ACLPolicy) expandSSHSrcAlias(m *Machine, alias string, dstUser *User) []string {
|
||||
if dstUser != nil {
|
||||
if !m.HasUser(dstUser.Name) || m.HasTags() {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if alias == AutoGroupMember || alias == AutoGroupMembers {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.Contains(alias, "@") && m.HasUser(alias) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") && a.isGroupMember(alias, m) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if (alias == AutoGroupMember || alias == AutoGroupMembers) && !m.HasTags() {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.Contains(alias, "@") && !m.HasTags() && m.HasUser(alias) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") && !m.HasTags() && a.isGroupMember(alias, m) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (a ACLPolicy) expandSSHDstToSSHUsers(m *Machine, rule ionscale.ACLSSH) (map[string]string, map[string]string) {
|
||||
users := buildSSHUsers(rule.Users)
|
||||
|
||||
var selfUsers map[string]string
|
||||
var otherUsers map[string]string
|
||||
|
||||
for _, d := range rule.Destination {
|
||||
if strings.HasPrefix(d, "tag:") && m.HasTag(d) {
|
||||
otherUsers = users
|
||||
}
|
||||
|
||||
if m.HasUser(d) || d == AutoGroupSelf {
|
||||
selfUsers = users
|
||||
}
|
||||
}
|
||||
|
||||
return selfUsers, otherUsers
|
||||
}
|
||||
|
||||
func buildSSHUsers(users []string) map[string]string {
|
||||
var autogroupNonRoot = false
|
||||
m := make(map[string]string)
|
||||
for _, u := range users {
|
||||
if u == "autogroup:nonroot" {
|
||||
m["*"] = "="
|
||||
autogroupNonRoot = true
|
||||
} else {
|
||||
m[u] = u
|
||||
}
|
||||
}
|
||||
|
||||
// disable root when autogroup:nonroot is used and root is not explicitly enabled
|
||||
if _, exists := m["root"]; !exists && autogroupNonRoot {
|
||||
m["root"] = ""
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func safeCheckPeriod(period string) string {
|
||||
if period == "" {
|
||||
return "always"
|
||||
}
|
||||
return period
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"autogroup:members"},
|
||||
Destination: []string{"autogroup:self"},
|
||||
Users: []string{"autogroup:nonroot"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{
|
||||
{NodeIP: p1.IPv4.String()},
|
||||
{NodeIP: p1.IPv6.String()},
|
||||
},
|
||||
SSHUsers: map[string]string{
|
||||
"*": "=",
|
||||
"root": "",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithGroup(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:sre": {
|
||||
"john@example.com",
|
||||
},
|
||||
},
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"group:sre"},
|
||||
Destination: []string{"tag:web"},
|
||||
Users: []string{"autogroup:nonroot", "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com", "tag:web")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{
|
||||
{NodeIP: p1.IPv4.String()},
|
||||
{NodeIP: p1.IPv6.String()},
|
||||
},
|
||||
SSHUsers: map[string]string{
|
||||
"*": "=",
|
||||
"root": "root",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithMatchingUsers(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"john@example.com"},
|
||||
Destination: []string{"john@example.com"},
|
||||
Users: []string{"autogroup:nonroot", "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: sshPrincipalsFromMachines(*p1),
|
||||
SSHUsers: map[string]string{
|
||||
"*": "=",
|
||||
"root": "root",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithMatchingUsersInGroup(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:sre": {"jane@example.com", "john@example.com"},
|
||||
},
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"group:sre"},
|
||||
Destination: []string{"john@example.com"},
|
||||
Users: []string{"autogroup:nonroot", "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: sshPrincipalsFromMachines(*p1),
|
||||
SSHUsers: map[string]string{
|
||||
"*": "=",
|
||||
"root": "root",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithNoMatchingUsers(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"jane@example.com"},
|
||||
Destination: []string{"john@example.com"},
|
||||
Users: []string{"autogroup:nonroot", "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
|
||||
assert.Nil(t, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithTags(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("nick@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"john@example.com", "tag:web"},
|
||||
Destination: []string{"tag:web"},
|
||||
Users: []string{"ubuntu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com", "tag:web")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2, *p3}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: sshPrincipalsFromMachines(*p1, *p3),
|
||||
SSHUsers: map[string]string{
|
||||
"ubuntu": "ubuntu",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithTagsInDstAndAutogroupMemberInSrc(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("nick@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"autogroup:members"},
|
||||
Destination: []string{"tag:web"},
|
||||
Users: []string{"ubuntu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com", "tag:web")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2, *p3}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: sshPrincipalsFromMachines(*p1, *p2),
|
||||
SSHUsers: map[string]string{
|
||||
"ubuntu": "ubuntu",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithUserInDstAndNonMatchingSrc(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"jane@example.com"},
|
||||
Destination: []string{"john@example.com"},
|
||||
Users: []string{"autogroup:nonroot"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
|
||||
assert.Nil(t, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithUserInDstAndAutogroupMembersSrc(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"autogroup:members"},
|
||||
Destination: []string{"john@example.com"},
|
||||
Users: []string{"autogroup:nonroot"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: sshPrincipalsFromMachines(*p1),
|
||||
SSHUsers: map[string]string{
|
||||
"*": "=",
|
||||
"root": "",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithAutogroupSelfAndTagSrc(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"tag:web"},
|
||||
Destination: []string{"autogroup:self"},
|
||||
Users: []string{"autogroup:nonroot"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
|
||||
assert.Nil(t, actualRules.Rules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildSSHPolicy_WithTagsAndActionCheck(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
SSH: []ionscale.ACLSSH{
|
||||
{
|
||||
Action: "check",
|
||||
Source: []string{"tag:web"},
|
||||
Destination: []string{"tag:web"},
|
||||
Users: []string{"autogroup:nonroot"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com", "tag:web")
|
||||
|
||||
actualRules := policy.BuildSSHPolicy([]Machine{*p1, *p2}, dst)
|
||||
|
||||
assert.Nil(t, actualRules.Rules)
|
||||
}
|
||||
|
||||
func printRules(rules []*tailcfg.SSHRule) {
|
||||
indent, err := json.MarshalIndent(rules, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(indent))
|
||||
}
|
||||
|
||||
func sshPrincipalsFromMachines(machines ...Machine) []*tailcfg.SSHPrincipal {
|
||||
x := StringSet{}
|
||||
for _, m := range machines {
|
||||
x.Add(m.IPv4.String(), m.IPv6.String())
|
||||
}
|
||||
|
||||
var result = []*tailcfg.SSHPrincipal{}
|
||||
|
||||
for _, i := range x.Items() {
|
||||
result = append(result, &tailcfg.SSHPrincipal{NodeIP: i})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+969
-6
@@ -1,18 +1,744 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/jsiebens/ionscale/internal/addr"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACLPolicy_NodeAttributesWithWildcards(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
NodeAttrs: []ionscale.ACLNodeAttrGrant{
|
||||
{
|
||||
Target: []string{"*"},
|
||||
Attr: []string{
|
||||
"attr1",
|
||||
"attr2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Target: []string{"*"},
|
||||
Attr: []string{
|
||||
"attr3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actualAttrs := policy.NodeCapabilities(p1)
|
||||
expectedAttrs := []tailcfg.NodeCapability{
|
||||
tailcfg.NodeCapability("attr1"),
|
||||
tailcfg.NodeCapability("attr2"),
|
||||
tailcfg.NodeCapability("attr3"),
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedAttrs, actualAttrs)
|
||||
}
|
||||
|
||||
func TestACLPolicy_NodeAttributesWithUserAndGroups(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admins": []string{"john@example.com"},
|
||||
},
|
||||
NodeAttrs: []ionscale.ACLNodeAttrGrant{
|
||||
{
|
||||
Target: []string{"john@example.com"},
|
||||
Attr: []string{
|
||||
"attr1",
|
||||
"attr2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Target: []string{"jane@example.com", "group:analytics", "group:admins"},
|
||||
Attr: []string{
|
||||
"attr3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actualAttrs := policy.NodeCapabilities(p1)
|
||||
expectedAttrs := []tailcfg.NodeCapability{
|
||||
tailcfg.NodeCapability("attr1"),
|
||||
tailcfg.NodeCapability("attr2"),
|
||||
tailcfg.NodeCapability("attr3"),
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedAttrs, actualAttrs)
|
||||
}
|
||||
|
||||
func TestACLPolicy_NodeAttributesWithUserAndTags(t *testing.T) {
|
||||
p1 := createMachine("john@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admins": []string{"john@example.com"},
|
||||
},
|
||||
NodeAttrs: []ionscale.ACLNodeAttrGrant{
|
||||
{
|
||||
Target: []string{"john@example.com"},
|
||||
Attr: []string{
|
||||
"attr1",
|
||||
"attr2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Target: []string{"jane@example.com", "tag:web"},
|
||||
Attr: []string{
|
||||
"attr3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actualAttrs := policy.NodeCapabilities(p1)
|
||||
expectedAttrs := []tailcfg.NodeCapability{tailcfg.NodeCapability("attr3")}
|
||||
|
||||
assert.Equal(t, expectedAttrs, actualAttrs)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesEmptyACL(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWildcards(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"*:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesProto(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"*:22"},
|
||||
},
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"*:*"},
|
||||
Protocol: "igmp",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
IPProto: []int{protocolIGMP},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithGroups(t *testing.T) {
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("joe@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admin": []string{"jane@example.com"},
|
||||
"group:audit": []string{"nick@example.com"},
|
||||
},
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"group:admin"},
|
||||
Destination: []string{"*:22"},
|
||||
},
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"group:audit"},
|
||||
Destination: []string{"*:8000-8080"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p2.IPv4.String(),
|
||||
p2.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 8000,
|
||||
Last: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithAutoGroupMembers(t *testing.T) {
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("joe@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"autogroup:members"},
|
||||
Destination: []string{"*:22"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
|
||||
expectedSrcIPs := []string{
|
||||
p1.IPv4.String(), p1.IPv6.String(),
|
||||
p2.IPv4.String(), p2.IPv6.String(),
|
||||
}
|
||||
sort.Strings(expectedSrcIPs)
|
||||
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: expectedSrcIPs,
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithAutoGroupMember(t *testing.T) {
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("joe@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"autogroup:member"},
|
||||
Destination: []string{"*:22"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
|
||||
expectedSrcIPs := []string{
|
||||
p1.IPv4.String(), p1.IPv6.String(),
|
||||
p2.IPv4.String(), p2.IPv6.String(),
|
||||
}
|
||||
sort.Strings(expectedSrcIPs)
|
||||
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: expectedSrcIPs,
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithAutoGroupTagged(t *testing.T) {
|
||||
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("joe@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"autogroup:tagged"},
|
||||
Destination: []string{"*:22"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
|
||||
expectedSrcIPs := []string{
|
||||
p3.IPv4.String(), p3.IPv6.String(),
|
||||
}
|
||||
sort.Strings(expectedSrcIPs)
|
||||
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: expectedSrcIPs,
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupSelf(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"autogroup:self:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupSelfAndTags(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("john@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"autogroup:self:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupSelfAndOtherDestinations(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("john@example.com", "tag:web")
|
||||
p3 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"autogroup:self:22", "john@example.com:80"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: p1.IPs(),
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 80,
|
||||
Last: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 80,
|
||||
Last: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupInternet(t *testing.T) {
|
||||
p1 := createMachine("nick@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"nick@example.com"},
|
||||
Destination: []string{"autogroup:internet:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
dst.AllowIPs = []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
}
|
||||
|
||||
expectedDstPorts := []tailcfg.NetPortRange{}
|
||||
for _, r := range autogroupInternetRanges() {
|
||||
expectedDstPorts = append(expectedDstPorts, tailcfg.NetPortRange{
|
||||
IP: r,
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: expectedDstPorts,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestWithUser(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"john@example.com:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("john@example.com")
|
||||
assert.True(t, policy.IsValidPeer(src, createMachine("john@example.com")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("john@example.com", "tag:web")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("jane@example.com")))
|
||||
}
|
||||
|
||||
func TestWithGroup(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admin": {"john@example.com"},
|
||||
},
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"group:admin:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("john@example.com")
|
||||
assert.True(t, policy.IsValidPeer(src, createMachine("john@example.com")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("jane@example.com")))
|
||||
}
|
||||
|
||||
func TestWithTags(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"tag:web:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("john@example.com")
|
||||
|
||||
assert.True(t, policy.IsValidPeer(src, createMachine("john@example.com", "tag:web")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("john@example.com", "tag:ci")))
|
||||
}
|
||||
|
||||
func TestWithHosts(t *testing.T) {
|
||||
dst1 := createMachine("john@example.com")
|
||||
dst2 := createMachine("john@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Hosts: map[string]string{
|
||||
"dst1": dst1.IPv4.String(),
|
||||
},
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"dst1:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("jane@example.com")
|
||||
|
||||
assert.True(t, policy.IsValidPeer(src, dst1))
|
||||
assert.False(t, policy.IsValidPeer(src, dst2))
|
||||
}
|
||||
|
||||
func createMachine(user string, tags ...string) *Machine {
|
||||
ipv4, ipv6, err := addr.SelectIP(func(addr netip.Addr) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &Machine{
|
||||
IPv4: IP{ipv4},
|
||||
IPv6: IP{ipv6},
|
||||
User: User{
|
||||
Name: user,
|
||||
},
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLPolicy_IsTagOwner(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:engineers": {"jane@example.com"},
|
||||
},
|
||||
TagOwners: map[string][]string{
|
||||
"tag:web": {"john@example.com", "group:engineers"},
|
||||
}}
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:engineers": {"jane@example.com"},
|
||||
},
|
||||
TagOwners: map[string][]string{
|
||||
"tag:web": {"john@example.com", "group:engineers"},
|
||||
}}}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -28,6 +754,13 @@ func TestACLPolicy_IsTagOwner(t *testing.T) {
|
||||
userType: UserTypeService,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "system admin is always a valid owner",
|
||||
tag: "tag:unknown",
|
||||
userName: "system admin",
|
||||
userType: UserTypeService,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "direct tag owner",
|
||||
tag: "tag:web",
|
||||
@@ -69,3 +802,233 @@ func TestACLPolicy_IsTagOwner(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLPolicy_FindAutoApprovedIPsWhenNoAutoapproversAreSet(t *testing.T) {
|
||||
route1 := netip.MustParsePrefix("10.160.0.0/20")
|
||||
route2 := netip.MustParsePrefix("10.161.0.0/20")
|
||||
route3 := netip.MustParsePrefix("10.162.0.0/20")
|
||||
|
||||
policy := ACLPolicy{}
|
||||
assert.Nil(t, policy.FindAutoApprovedIPs([]netip.Prefix{route1, route2, route3}, nil, nil))
|
||||
}
|
||||
|
||||
func TestACLPolicy_FindAutoApprovedIPs(t *testing.T) {
|
||||
route1 := netip.MustParsePrefix("10.160.0.0/20")
|
||||
route2 := netip.MustParsePrefix("10.161.0.0/20")
|
||||
route3 := netip.MustParsePrefix("10.162.0.0/20")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admins": {"jane@example.com"},
|
||||
},
|
||||
AutoApprovers: &ionscale.ACLAutoApprovers{
|
||||
Routes: map[string][]string{
|
||||
route1.String(): {"group:admins"},
|
||||
route2.String(): {"john@example.com", "tag:router"},
|
||||
},
|
||||
ExitNode: []string{"nick@example.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
tag []string
|
||||
userName string
|
||||
routableIPs []netip.Prefix
|
||||
expected []netip.Prefix
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "by user",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{route2},
|
||||
},
|
||||
{
|
||||
name: "partial by user",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{netip.MustParsePrefix("10.161.4.0/22")},
|
||||
expected: []netip.Prefix{netip.MustParsePrefix("10.161.4.0/22")},
|
||||
},
|
||||
{
|
||||
name: "by tag",
|
||||
tag: []string{"tag:router"},
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{route2},
|
||||
},
|
||||
{
|
||||
name: "by group",
|
||||
userName: "jane@example.com",
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{route1},
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
userName: "nick@example.com",
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "exit",
|
||||
userName: "nick@example.com",
|
||||
routableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
expected: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
},
|
||||
{
|
||||
name: "exit no match",
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualAllowedIPs := policy.FindAutoApprovedIPs(tc.routableIPs, tc.tag, &User{Name: tc.userName})
|
||||
assert.Equal(t, tc.expected, actualAllowedIPs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithAdvertisedRoutes(t *testing.T) {
|
||||
route1 := netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:1:a3c:0/120")
|
||||
p1 := createMachine("john@example.com", "tag:trusted")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
ACLs: []ionscale.ACLEntry{
|
||||
{
|
||||
Action: "accept",
|
||||
Source: []string{"tag:trusted"},
|
||||
Destination: []string{"fd7a:115c:a1e0:b1a:0:1:a3c:0/120:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
dst.AllowIPs = []netip.Prefix{route1}
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: p1.IPs(),
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: route1.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWildcardGrants(t *testing.T) {
|
||||
ranges, err := tailcfg.ParseProtoPortRanges([]string{"*"})
|
||||
require.NoError(t, err)
|
||||
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Grants: []ionscale.ACLGrant{
|
||||
{
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"*"},
|
||||
IP: ranges,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithAppGrants(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
mycap := map[string]interface{}{
|
||||
"channel": "alpha",
|
||||
"ids": []string{"1", "2", "3"},
|
||||
}
|
||||
|
||||
marshal, _ := json.Marshal(mycap)
|
||||
|
||||
policy := ACLPolicy{
|
||||
ionscale.ACLPolicy{
|
||||
Grants: []ionscale.ACLGrant{
|
||||
{
|
||||
Source: []string{"*"},
|
||||
Destination: []string{"*"},
|
||||
App: map[tailcfg.PeerCapability][]tailcfg.RawMessage{
|
||||
tailcfg.PeerCapability("localtest.me/cap/test"): {tailcfg.RawMessage(marshal)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
CapGrant: []tailcfg.CapGrant{
|
||||
{
|
||||
Dsts: []netip.Prefix{
|
||||
netip.PrefixFrom(*dst.IPv4.Addr, 32),
|
||||
netip.PrefixFrom(*dst.IPv6.Addr, 128),
|
||||
},
|
||||
CapMap: map[tailcfg.PeerCapability][]tailcfg.RawMessage{
|
||||
tailcfg.PeerCapability("localtest.me/cap/test"): {tailcfg.RawMessage(marshal)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ func CreateApiKey(tailnet *Tailnet, user *User, expiresAt *time.Time) (string, *
|
||||
}
|
||||
}
|
||||
|
||||
type ApiKeyRepository interface {
|
||||
SaveApiKey(ctx context.Context, key *ApiKey) error
|
||||
LoadApiKey(ctx context.Context, key string) (*ApiKey, error)
|
||||
DeleteApiKeysByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteApiKeysByUser(ctx context.Context, userID uint64) error
|
||||
}
|
||||
|
||||
type ApiKey struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
@@ -65,7 +72,7 @@ func (r *repository) LoadApiKey(ctx context.Context, key string) (*ApiKey, error
|
||||
}
|
||||
|
||||
var m ApiKey
|
||||
tx := r.withContext(ctx).Preload("User").Preload("Tailnet").First(&m, "key = ?", split[0])
|
||||
tx := r.withContext(ctx).Preload("User").Preload("Tailnet").Take(&m, "key = ?", split[0])
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -79,7 +86,7 @@ func (r *repository) LoadApiKey(ctx context.Context, key string) (*ApiKey, error
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
if m.ExpiresAt != nil && !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
+28
-15
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateAuthKey(tailnet *Tailnet, user *User, ephemeral bool, tags Tags, expiresAt *time.Time) (string, *AuthKey) {
|
||||
func CreateAuthKey(tailnet *Tailnet, user *User, ephemeral bool, preAuthorized bool, tags Tags, expiresAt *time.Time) (string, *AuthKey) {
|
||||
key := util.RandStringBytes(12)
|
||||
pwd := util.RandStringBytes(22)
|
||||
value := fmt.Sprintf("%s_%s", key, pwd)
|
||||
@@ -22,25 +22,38 @@ func CreateAuthKey(tailnet *Tailnet, user *User, ephemeral bool, tags Tags, expi
|
||||
}
|
||||
|
||||
return value, &AuthKey{
|
||||
ID: util.NextID(),
|
||||
Key: key,
|
||||
Hash: string(hash),
|
||||
Ephemeral: ephemeral,
|
||||
Tags: tags,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: expiresAt,
|
||||
ID: util.NextID(),
|
||||
Key: key,
|
||||
Hash: string(hash),
|
||||
Ephemeral: ephemeral,
|
||||
PreAuthorized: preAuthorized,
|
||||
Tags: tags,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: expiresAt,
|
||||
|
||||
TailnetID: tailnet.ID,
|
||||
UserID: user.ID,
|
||||
}
|
||||
}
|
||||
|
||||
type AuthKeyRepository interface {
|
||||
GetAuthKey(ctx context.Context, id uint64) (*AuthKey, error)
|
||||
SaveAuthKey(ctx context.Context, key *AuthKey) error
|
||||
DeleteAuthKey(ctx context.Context, id uint64) (bool, error)
|
||||
DeleteAuthKeysByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteAuthKeysByUser(ctx context.Context, userID uint64) error
|
||||
ListAuthKeys(ctx context.Context, tailnetID uint64) ([]AuthKey, error)
|
||||
ListAuthKeysByTailnetAndUser(ctx context.Context, tailnetID, userID uint64) ([]AuthKey, error)
|
||||
LoadAuthKey(ctx context.Context, key string) (*AuthKey, error)
|
||||
}
|
||||
|
||||
type AuthKey struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
Hash string
|
||||
Ephemeral bool
|
||||
Tags Tags
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
Hash string
|
||||
Ephemeral bool
|
||||
PreAuthorized bool
|
||||
Tags Tags
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
@@ -134,7 +147,7 @@ func (r *repository) LoadAuthKey(ctx context.Context, key string) (*AuthKey, err
|
||||
}
|
||||
|
||||
var m AuthKey
|
||||
tx := r.withContext(ctx).Preload("User").Preload("Tailnet").First(&m, "key = ?", split[0])
|
||||
tx := r.withContext(ctx).Preload("User").Preload("Tailnet").Take(&m, "key = ?", split[0])
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -148,7 +161,7 @@ func (r *repository) LoadAuthKey(ctx context.Context, key string) (*AuthKey, err
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
if m.ExpiresAt != nil && !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthenticationRequestRepository interface {
|
||||
SaveAuthenticationRequest(ctx context.Context, session *AuthenticationRequest) error
|
||||
GetAuthenticationRequest(ctx context.Context, key string) (*AuthenticationRequest, error)
|
||||
DeleteAuthenticationRequest(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Token string
|
||||
@@ -27,7 +33,7 @@ func (r *repository) SaveAuthenticationRequest(ctx context.Context, session *Aut
|
||||
|
||||
func (r *repository) GetAuthenticationRequest(ctx context.Context, key string) (*AuthenticationRequest, error) {
|
||||
var m AuthenticationRequest
|
||||
tx := r.withContext(ctx).First(&m, "key = ?", key)
|
||||
tx := r.withContext(ctx).Take(&m, "key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
func NewHuJSON[T any](t *T) HuJSON[T] {
|
||||
marshal, _ := json.Marshal(t)
|
||||
return HuJSON[T]{
|
||||
v: string(marshal),
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseHuJson[T any](v string) (*HuJSON[T], error) {
|
||||
ast, err := hujson.Parse([]byte(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ast.Format()
|
||||
formatted := string(ast.Pack())
|
||||
ast.Standardize()
|
||||
|
||||
t := new(T)
|
||||
if err := json.Unmarshal(ast.Pack(), t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HuJSON[T]{v: formatted, t: t}, nil
|
||||
}
|
||||
|
||||
type HuJSON[T any] struct {
|
||||
v string
|
||||
t *T
|
||||
}
|
||||
|
||||
func (h *HuJSON[T]) Get() *T {
|
||||
return h.t
|
||||
}
|
||||
|
||||
func (h *HuJSON[T]) String() string {
|
||||
return h.v
|
||||
}
|
||||
|
||||
func (i *HuJSON[T]) Equal(x *HuJSON[T]) bool {
|
||||
if i == nil && x == nil {
|
||||
return true
|
||||
}
|
||||
if (i == nil) != (x == nil) {
|
||||
return false
|
||||
}
|
||||
return i.v == x.v
|
||||
}
|
||||
|
||||
func (h HuJSON[T]) Value() (driver.Value, error) {
|
||||
if len(h.v) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return h.v, nil
|
||||
}
|
||||
|
||||
func (h *HuJSON[T]) Scan(destination interface{}) error {
|
||||
var v string
|
||||
switch value := destination.(type) {
|
||||
case string:
|
||||
v = value
|
||||
case []byte:
|
||||
v = string(value)
|
||||
default:
|
||||
return fmt.Errorf("unexpected data type %T", destination)
|
||||
}
|
||||
|
||||
next, err := hujson.Standardize([]byte(v))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var n = new(T)
|
||||
if err := json.Unmarshal(next, n); err != nil {
|
||||
return err
|
||||
}
|
||||
h.v = v
|
||||
h.t = n
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
_defaultDERPMapMu sync.RWMutex
|
||||
_defaultDERPMap = WrapDERPMap(tailcfg.DERPMap{})
|
||||
)
|
||||
|
||||
func SetDefaultDERPMap(v *tailcfg.DERPMap) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_defaultDERPMapMu.Lock()
|
||||
defer _defaultDERPMapMu.Unlock()
|
||||
_defaultDERPMap = WrapDERPMap(*v)
|
||||
}
|
||||
|
||||
func GetDefaultDERPMap() DERPMap {
|
||||
_defaultDERPMapMu.RLock()
|
||||
defer _defaultDERPMapMu.RUnlock()
|
||||
return _defaultDERPMap
|
||||
}
|
||||
|
||||
type DERPMap struct {
|
||||
Checksum string
|
||||
DERPMap tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (d DERPMap) GetDERPMap(_ context.Context) (*DERPMap, error) {
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func WrapDERPMap(d tailcfg.DERPMap) DERPMap {
|
||||
return DERPMap{
|
||||
Checksum: util.Checksum(d),
|
||||
DERPMap: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (hi *DERPMap) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, hi)
|
||||
default:
|
||||
return fmt.Errorf("unexpected data type %T", destination)
|
||||
}
|
||||
}
|
||||
|
||||
func (hi DERPMap) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(hi)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
// GormDataType gorm common data type
|
||||
func (DERPMap) GormDataType() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// GormDBDataType gorm db data type
|
||||
func (DERPMap) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
switch db.Dialector.Name() {
|
||||
case "sqlite":
|
||||
return "JSON"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DefaultDERPMap interface {
|
||||
GetDERPMap(ctx context.Context) (*DERPMap, error)
|
||||
}
|
||||
@@ -6,13 +6,32 @@ import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type DNSConfig struct {
|
||||
MagicDNS bool `json:"magic_dns"`
|
||||
OverrideLocalDNS bool `json:"override_local_dns"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
Routes map[string][]string `json:"routes"`
|
||||
HttpsCertsEnabled bool `json:"http_certs"`
|
||||
MagicDNS bool `json:"magic_dns"`
|
||||
OverrideLocalDNS bool `json:"override_local_dns"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
Routes map[string][]string `json:"routes"`
|
||||
SearchDomains []string `json:"search_domains"`
|
||||
}
|
||||
|
||||
func (i *DNSConfig) Equal(x *DNSConfig) bool {
|
||||
if i == nil && x == nil {
|
||||
return true
|
||||
}
|
||||
if (i == nil) != (x == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
return i.MagicDNS == x.MagicDNS &&
|
||||
i.HttpsCertsEnabled == x.HttpsCertsEnabled &&
|
||||
i.OverrideLocalDNS == x.OverrideLocalDNS &&
|
||||
reflect.DeepEqual(i.Nameservers, x.Nameservers) &&
|
||||
reflect.DeepEqual(i.Routes, x.Routes) &&
|
||||
reflect.DeepEqual(i.SearchDomains, x.SearchDomains)
|
||||
}
|
||||
|
||||
func (i *DNSConfig) Scan(destination interface{}) error {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/mitchellh/pointerstructure"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
@@ -68,6 +69,16 @@ func (i *IAMPolicy) EvaluatePolicy(identity *Identity) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (i *IAMPolicy) Equal(x *IAMPolicy) bool {
|
||||
if i == nil && x == nil {
|
||||
return true
|
||||
}
|
||||
if (i == nil) != (x == nil) {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(i, x)
|
||||
}
|
||||
|
||||
func (i *IAMPolicy) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
|
||||
+121
-17
@@ -13,6 +13,23 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type MachineRepository interface {
|
||||
SaveMachine(ctx context.Context, m *Machine) error
|
||||
DeleteMachine(ctx context.Context, id uint64) (bool, error)
|
||||
GetMachine(ctx context.Context, id uint64) (*Machine, error)
|
||||
GetMachineByKeyAndUser(ctx context.Context, key string, userID uint64) (*Machine, error)
|
||||
GetMachineByKeys(ctx context.Context, machineKey string, nodeKey string) (*Machine, error)
|
||||
CountMachinesWithIPv4(ctx context.Context, ip string) (int64, error)
|
||||
GetNextMachineNameIndex(ctx context.Context, tailnetID uint64, name string) (uint64, error)
|
||||
ListMachineByTailnet(ctx context.Context, tailnetID uint64) (Machines, error)
|
||||
CountMachineByTailnet(ctx context.Context, tailnetID uint64) (int64, error)
|
||||
DeleteMachineByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteMachineByUser(ctx context.Context, userID uint64) error
|
||||
ListMachinePeers(ctx context.Context, tailnetID uint64, machineID uint64) (Machines, error)
|
||||
ListInactiveEphemeralMachines(ctx context.Context, checkpoint time.Time) (Machines, error)
|
||||
SetMachineLastSeen(ctx context.Context, machineID uint64) error
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
@@ -24,10 +41,12 @@ type Machine struct {
|
||||
RegisteredTags Tags
|
||||
Tags Tags
|
||||
KeyExpiryDisabled bool
|
||||
Authorized bool
|
||||
|
||||
HostInfo HostInfo
|
||||
Endpoints Endpoints
|
||||
AllowIPs AllowIPs
|
||||
HostInfo HostInfo
|
||||
Endpoints Endpoints
|
||||
AllowIPs AllowIPs
|
||||
AutoAllowIPs AllowIPs
|
||||
|
||||
IPv4 IP
|
||||
IPv6 IP
|
||||
@@ -45,6 +64,17 @@ type Machine struct {
|
||||
|
||||
type Machines []Machine
|
||||
|
||||
func (m *Machine) CompleteName() string {
|
||||
if m.NameIdx != 0 {
|
||||
return fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
||||
}
|
||||
return m.Name
|
||||
}
|
||||
|
||||
func (m *Machine) IPs() []string {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
}
|
||||
|
||||
func (m *Machine) IsExpired() bool {
|
||||
return !m.KeyExpiryDisabled && !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
@@ -70,6 +100,54 @@ func (m *Machine) HasTags() bool {
|
||||
return len(m.Tags) != 0
|
||||
}
|
||||
|
||||
func (m *Machine) IsAdvertisedExitNode() bool {
|
||||
for _, r := range m.HostInfo.RoutableIPs {
|
||||
if r.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) IsAllowedExitNode() bool {
|
||||
for _, r := range m.AllowIPs {
|
||||
if r.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, r := range m.AutoAllowIPs {
|
||||
if r.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) AdvertisedPrefixes() []string {
|
||||
result := []string{}
|
||||
for _, r := range m.HostInfo.RoutableIPs {
|
||||
if r.Bits() != 0 {
|
||||
result = append(result, r.String())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Machine) AllowedPrefixes() []string {
|
||||
result := StringSet{}
|
||||
for _, r := range m.AllowIPs {
|
||||
if r.Bits() != 0 {
|
||||
result.Add(r.String())
|
||||
}
|
||||
}
|
||||
for _, r := range m.AutoAllowIPs {
|
||||
if r.Bits() != 0 {
|
||||
result.Add(r.String())
|
||||
}
|
||||
}
|
||||
return result.Items()
|
||||
}
|
||||
|
||||
func (m *Machine) IsAllowedIP(i netip.Addr) bool {
|
||||
if m.HasIP(i) {
|
||||
return true
|
||||
@@ -79,6 +157,11 @@ func (m *Machine) IsAllowedIP(i netip.Addr) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range m.AutoAllowIPs {
|
||||
if t.Contains(i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -88,6 +171,25 @@ func (m *Machine) IsAllowedIPPrefix(i netip.Prefix) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range m.AutoAllowIPs {
|
||||
if t.Overlaps(i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) IsExitNode() bool {
|
||||
for _, t := range m.AllowIPs {
|
||||
if t.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range m.AutoAllowIPs {
|
||||
if t.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -225,7 +327,7 @@ func (HostInfo) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type Endpoints []string
|
||||
type Endpoints []netip.AddrPort
|
||||
|
||||
func (hi *Endpoints) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
@@ -272,7 +374,7 @@ func (r *repository) DeleteMachine(ctx context.Context, id uint64) (bool, error)
|
||||
|
||||
func (r *repository) GetMachine(ctx context.Context, machineID uint64) (*Machine, error) {
|
||||
var m Machine
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").First(&m, "id = ?", machineID)
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").Preload("User.Account").Take(&m, machineID)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -291,7 +393,7 @@ func (r *repository) GetNextMachineNameIndex(ctx context.Context, tailnetID uint
|
||||
tx := r.withContext(ctx).
|
||||
Where("name = ? AND tailnet_id = ?", name, tailnetID).
|
||||
Order("name_idx desc").
|
||||
First(&m)
|
||||
Take(&m)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
@@ -304,9 +406,9 @@ func (r *repository) GetNextMachineNameIndex(ctx context.Context, tailnetID uint
|
||||
return m.NameIdx + 1, nil
|
||||
}
|
||||
|
||||
func (r *repository) GetMachineByKey(ctx context.Context, tailnetID uint64, machineKey string) (*Machine, error) {
|
||||
func (r *repository) GetMachineByKeyAndUser(ctx context.Context, machineKey string, userID uint64) (*Machine, error) {
|
||||
var m Machine
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").First(&m, "tailnet_id = ? AND machine_key = ?", tailnetID, machineKey)
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").Take(&m, "machine_key = ? AND user_id = ?", machineKey, userID)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -321,7 +423,7 @@ func (r *repository) GetMachineByKey(ctx context.Context, tailnetID uint64, mach
|
||||
|
||||
func (r *repository) GetMachineByKeys(ctx context.Context, machineKey string, nodeKey string) (*Machine, error) {
|
||||
var m Machine
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").First(&m, "machine_key = ? AND node_key = ?", machineKey, nodeKey)
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").Take(&m, "machine_key = ? AND node_key = ?", machineKey, nodeKey)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -373,9 +475,10 @@ func (r *repository) ListMachineByTailnet(ctx context.Context, tailnetID uint64)
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Preload("Tailnet").
|
||||
Preload("User").
|
||||
Where("tailnet_id = ?", tailnetID).
|
||||
Order("name asc, name_idx asc").
|
||||
Joins("User").
|
||||
Joins("User.Account").
|
||||
Where("machines.tailnet_id = ?", tailnetID).
|
||||
Order("machines.name asc, machines.name_idx asc").
|
||||
Find(&machines)
|
||||
|
||||
if tx.Error != nil {
|
||||
@@ -385,14 +488,15 @@ func (r *repository) ListMachineByTailnet(ctx context.Context, tailnetID uint64)
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListMachinePeers(ctx context.Context, tailnetID uint64, key string) (Machines, error) {
|
||||
var machines = []Machine{}
|
||||
func (r *repository) ListMachinePeers(ctx context.Context, tailnetID uint64, machineID uint64) (Machines, error) {
|
||||
var machines []Machine
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Preload("Tailnet").
|
||||
Preload("User").
|
||||
Where("tailnet_id = ? AND machine_key <> ?", tailnetID, key).
|
||||
Order("id asc").
|
||||
Joins("User").
|
||||
Joins("User.Account").
|
||||
Where("machines.tailnet_id = ? AND machines.id <> ?", tailnetID, machineID).
|
||||
Order("machines.id asc").
|
||||
Find(&machines)
|
||||
|
||||
if tx.Error != nil {
|
||||
|
||||
@@ -12,6 +12,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RegistrationRequestRepository interface {
|
||||
SaveRegistrationRequest(ctx context.Context, request *RegistrationRequest) error
|
||||
GetRegistrationRequestByKey(ctx context.Context, key string) (*RegistrationRequest, error)
|
||||
GetRegistrationRequestByMachineKey(ctx context.Context, key string) (*RegistrationRequest, error)
|
||||
}
|
||||
|
||||
type RegistrationRequest struct {
|
||||
MachineKey string `gorm:"primary_key"`
|
||||
Key string
|
||||
@@ -19,10 +25,7 @@ type RegistrationRequest struct {
|
||||
CreatedAt time.Time
|
||||
Authenticated bool
|
||||
Error string
|
||||
}
|
||||
|
||||
func (r *RegistrationRequest) IsFinished() bool {
|
||||
return r.Authenticated || len(r.Error) != 0
|
||||
UserID uint64
|
||||
}
|
||||
|
||||
type RegistrationRequestData tailcfg.RegisterRequest
|
||||
@@ -67,7 +70,7 @@ func (r *repository) SaveRegistrationRequest(ctx context.Context, request *Regis
|
||||
|
||||
func (r *repository) GetRegistrationRequestByKey(ctx context.Context, key string) (*RegistrationRequest, error) {
|
||||
var m RegistrationRequest
|
||||
tx := r.withContext(ctx).First(&m, "key = ?", key)
|
||||
tx := r.withContext(ctx).Take(&m, "key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -82,7 +85,7 @@ func (r *repository) GetRegistrationRequestByKey(ctx context.Context, key string
|
||||
|
||||
func (r *repository) GetRegistrationRequestByMachineKey(ctx context.Context, key string) (*RegistrationRequest, error) {
|
||||
var m RegistrationRequest
|
||||
tx := r.withContext(ctx).First(&m, "machine_key = ?", key)
|
||||
tx := r.withContext(ctx).Take(&m, "machine_key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
|
||||
+18
-104
@@ -2,91 +2,43 @@ package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
"time"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
|
||||
SetDERPMap(ctx context.Context, v *tailcfg.DERPMap) error
|
||||
AccountRepository
|
||||
ApiKeyRepository
|
||||
SystemApiKeyRepository
|
||||
AuthKeyRepository
|
||||
MachineRepository
|
||||
TailnetRepository
|
||||
UserRepository
|
||||
AuthenticationRequestRepository
|
||||
RegistrationRequestRepository
|
||||
SSHActionRequestRepository
|
||||
|
||||
GetAccount(ctx context.Context, accountID uint64) (*Account, error)
|
||||
GetOrCreateAccount(ctx context.Context, externalID, loginName string) (*Account, bool, error)
|
||||
GetControlKeys(ctx context.Context) (*ControlKeys, error)
|
||||
SetControlKeys(ctx context.Context, keys *ControlKeys) error
|
||||
|
||||
SaveTailnet(ctx context.Context, tailnet *Tailnet) error
|
||||
GetOrCreateTailnet(ctx context.Context, name string) (*Tailnet, bool, error)
|
||||
GetTailnet(ctx context.Context, id uint64) (*Tailnet, error)
|
||||
ListTailnets(ctx context.Context) ([]Tailnet, error)
|
||||
DeleteTailnet(ctx context.Context, id uint64) error
|
||||
|
||||
SaveSystemApiKey(ctx context.Context, key *SystemApiKey) error
|
||||
LoadSystemApiKey(ctx context.Context, key string) (*SystemApiKey, error)
|
||||
|
||||
SaveApiKey(ctx context.Context, key *ApiKey) error
|
||||
LoadApiKey(ctx context.Context, key string) (*ApiKey, error)
|
||||
DeleteApiKeysByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteApiKeysByUser(ctx context.Context, userID uint64) error
|
||||
|
||||
GetAuthKey(ctx context.Context, id uint64) (*AuthKey, error)
|
||||
SaveAuthKey(ctx context.Context, key *AuthKey) error
|
||||
DeleteAuthKey(ctx context.Context, id uint64) (bool, error)
|
||||
DeleteAuthKeysByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteAuthKeysByUser(ctx context.Context, userID uint64) error
|
||||
ListAuthKeys(ctx context.Context, tailnetID uint64) ([]AuthKey, error)
|
||||
ListAuthKeysByTailnetAndUser(ctx context.Context, tailnetID, userID uint64) ([]AuthKey, error)
|
||||
LoadAuthKey(ctx context.Context, key string) (*AuthKey, error)
|
||||
|
||||
GetOrCreateServiceUser(ctx context.Context, tailnet *Tailnet) (*User, bool, error)
|
||||
GetOrCreateUserWithAccount(ctx context.Context, tailnet *Tailnet, account *Account) (*User, bool, error)
|
||||
GetUser(ctx context.Context, userID uint64) (*User, error)
|
||||
DeleteUser(ctx context.Context, userID uint64) error
|
||||
ListUsers(ctx context.Context, tailnetID uint64) (Users, error)
|
||||
DeleteUsersByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
|
||||
SaveMachine(ctx context.Context, m *Machine) error
|
||||
DeleteMachine(ctx context.Context, id uint64) (bool, error)
|
||||
GetMachine(ctx context.Context, id uint64) (*Machine, error)
|
||||
GetMachineByKey(ctx context.Context, tailnetID uint64, key string) (*Machine, error)
|
||||
GetMachineByKeys(ctx context.Context, machineKey string, nodeKey string) (*Machine, error)
|
||||
CountMachinesWithIPv4(ctx context.Context, ip string) (int64, error)
|
||||
GetNextMachineNameIndex(ctx context.Context, tailnetID uint64, name string) (uint64, error)
|
||||
ListMachineByTailnet(ctx context.Context, tailnetID uint64) (Machines, error)
|
||||
CountMachineByTailnet(ctx context.Context, tailnetID uint64) (int64, error)
|
||||
DeleteMachineByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteMachineByUser(ctx context.Context, userID uint64) error
|
||||
ListMachinePeers(ctx context.Context, tailnetID uint64, key string) (Machines, error)
|
||||
ListInactiveEphemeralMachines(ctx context.Context, checkpoint time.Time) (Machines, error)
|
||||
SetMachineLastSeen(ctx context.Context, machineID uint64) error
|
||||
|
||||
SaveRegistrationRequest(ctx context.Context, request *RegistrationRequest) error
|
||||
GetRegistrationRequestByKey(ctx context.Context, key string) (*RegistrationRequest, error)
|
||||
GetRegistrationRequestByMachineKey(ctx context.Context, key string) (*RegistrationRequest, error)
|
||||
|
||||
SaveAuthenticationRequest(ctx context.Context, session *AuthenticationRequest) error
|
||||
GetAuthenticationRequest(ctx context.Context, key string) (*AuthenticationRequest, error)
|
||||
DeleteAuthenticationRequest(ctx context.Context, key string) error
|
||||
GetJSONWebKeySet(ctx context.Context) (*JSONWebKeys, error)
|
||||
SetJSONWebKeySet(ctx context.Context, keys *JSONWebKeys) error
|
||||
|
||||
Transaction(func(rp Repository) error) error
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) Repository {
|
||||
return &repository{
|
||||
db: db,
|
||||
defaultDERPMap: &derpMapCache{},
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
defaultDERPMap *derpMapCache
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *repository) withContext(ctx context.Context) *gorm.DB {
|
||||
return r.db.WithContext(ctx)
|
||||
return r.db.WithContext(ctx).Omit(clause.Associations)
|
||||
}
|
||||
|
||||
func (r *repository) Transaction(action func(Repository) error) error {
|
||||
@@ -94,41 +46,3 @@ func (r *repository) Transaction(action func(Repository) error) error {
|
||||
return action(NewRepository(tx))
|
||||
})
|
||||
}
|
||||
|
||||
type derpMapCache struct {
|
||||
sync.RWMutex
|
||||
value *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (d *derpMapCache) Get() (*tailcfg.DERPMap, error) {
|
||||
d.RLock()
|
||||
|
||||
if d.value != nil {
|
||||
d.RUnlock()
|
||||
return d.value, nil
|
||||
}
|
||||
d.RUnlock()
|
||||
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
getJson := func(url string, target interface{}) error {
|
||||
c := http.Client{Timeout: 5 * time.Second}
|
||||
r, err := c.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
m := &tailcfg.DERPMap{}
|
||||
if err := getJson("https://controlplane.tailscale.com/derpmap/default", m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.value = m
|
||||
|
||||
return d.value, nil
|
||||
}
|
||||
|
||||
@@ -2,30 +2,53 @@ package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
tkey "tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
type configKey string
|
||||
|
||||
const (
|
||||
derpMapConfigKey configKey = "derp_map"
|
||||
derpMapConfigKey configKey = "derp_map"
|
||||
controlKeysConfigKey configKey = "control_keys"
|
||||
jwksConfigKey configKey = "jwks"
|
||||
)
|
||||
|
||||
type JSONWebKeys struct {
|
||||
Key JSONWebKey
|
||||
}
|
||||
|
||||
type JSONWebKey struct {
|
||||
Id string
|
||||
PrivateKey rsa.PrivateKey
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (j JSONWebKey) Public() crypto.PublicKey {
|
||||
return j.PrivateKey.Public()
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Key configKey `gorm:"primary_key"`
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (r *repository) GetDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
var m tailcfg.DERPMap
|
||||
type ControlKeys struct {
|
||||
ControlKey tkey.MachinePrivate
|
||||
LegacyControlKey tkey.MachinePrivate
|
||||
}
|
||||
|
||||
err := r.getServerConfig(ctx, derpMapConfigKey, &m)
|
||||
func (r *repository) GetControlKeys(ctx context.Context) (*ControlKeys, error) {
|
||||
var m ControlKeys
|
||||
err := r.getServerConfig(ctx, controlKeysConfigKey, &m)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return r.defaultDERPMap.Get()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -35,8 +58,27 @@ func (r *repository) GetDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) SetDERPMap(ctx context.Context, v *tailcfg.DERPMap) error {
|
||||
return r.setServerConfig(ctx, "derp_map", v)
|
||||
func (r *repository) SetControlKeys(ctx context.Context, v *ControlKeys) error {
|
||||
return r.setServerConfig(ctx, controlKeysConfigKey, v)
|
||||
}
|
||||
|
||||
func (r *repository) GetJSONWebKeySet(ctx context.Context) (*JSONWebKeys, error) {
|
||||
var m JSONWebKeys
|
||||
err := r.getServerConfig(ctx, jwksConfigKey, &m)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) SetJSONWebKeySet(ctx context.Context, v *JSONWebKeys) error {
|
||||
return r.setServerConfig(ctx, jwksConfigKey, v)
|
||||
}
|
||||
|
||||
func (r *repository) getServerConfig(ctx context.Context, s configKey, v interface{}) error {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSHActionRequestRepository interface {
|
||||
SaveSSHActionRequest(ctx context.Context, session *SSHActionRequest) error
|
||||
GetSSHActionRequest(ctx context.Context, key string) (*SSHActionRequest, error)
|
||||
DeleteSSHActionRequest(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
type SSHActionRequest struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Action string
|
||||
SrcMachineID uint64
|
||||
DstMachineID uint64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (r *repository) SaveSSHActionRequest(ctx context.Context, session *SSHActionRequest) error {
|
||||
tx := r.withContext(ctx).Save(session)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) GetSSHActionRequest(ctx context.Context, key string) (*SSHActionRequest, error) {
|
||||
var m SSHActionRequest
|
||||
tx := r.withContext(ctx).Take(&m, "key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteSSHActionRequest(ctx context.Context, key string) error {
|
||||
tx := r.withContext(ctx).Delete(&SSHActionRequest{Key: key})
|
||||
return tx.Error
|
||||
}
|
||||
@@ -32,6 +32,11 @@ func CreateSystemApiKey(account *Account, expiresAt *time.Time) (string, *System
|
||||
}
|
||||
}
|
||||
|
||||
type SystemApiKeyRepository interface {
|
||||
SaveSystemApiKey(ctx context.Context, key *SystemApiKey) error
|
||||
LoadSystemApiKey(ctx context.Context, key string) (*SystemApiKey, error)
|
||||
}
|
||||
|
||||
type SystemApiKey struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
@@ -69,7 +74,7 @@ func (r *repository) LoadSystemApiKey(ctx context.Context, token string) (*Syste
|
||||
}
|
||||
|
||||
var m SystemApiKey
|
||||
tx := r.withContext(ctx).Preload("Account").First(&m, "key = ?", key)
|
||||
tx := r.withContext(ctx).Preload("Account").Take(&m, "key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
|
||||
+61
-22
@@ -3,16 +3,56 @@ package domain
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
type Tailnet struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
DNSConfig DNSConfig
|
||||
IAMPolicy IAMPolicy
|
||||
ACLPolicy ACLPolicy
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
DNSConfig DNSConfig
|
||||
IAMPolicy HuJSON[IAMPolicy]
|
||||
ACLPolicy HuJSON[ACLPolicy]
|
||||
DERPMap DERPMap
|
||||
ServiceCollectionEnabled bool
|
||||
FileSharingEnabled bool
|
||||
SSHEnabled bool
|
||||
MachineAuthorizationEnabled bool
|
||||
}
|
||||
|
||||
type TailnetRepository interface {
|
||||
SaveTailnet(ctx context.Context, tailnet *Tailnet) error
|
||||
GetTailnet(ctx context.Context, id uint64) (*Tailnet, error)
|
||||
GetTailnetByName(ctx context.Context, name string) (*Tailnet, error)
|
||||
ListTailnets(ctx context.Context) ([]Tailnet, error)
|
||||
DeleteTailnet(ctx context.Context, id uint64) error
|
||||
}
|
||||
|
||||
func (t Tailnet) GetDERPMap(ctx context.Context, fallack DefaultDERPMap) (*DERPMap, error) {
|
||||
if t.DERPMap.Checksum == "" {
|
||||
return fallack.GetDERPMap(ctx)
|
||||
} else {
|
||||
return &t.DERPMap, nil
|
||||
}
|
||||
}
|
||||
|
||||
func SanitizeTailnetName(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
|
||||
a, err := mail.ParseAddress(name)
|
||||
if err == nil && a.Address == name {
|
||||
s := strings.Split(name, "@")
|
||||
return strings.Join([]string{dnsname.SanitizeLabel(s[0]), s[1]}, ".")
|
||||
}
|
||||
|
||||
labels := strings.Split(name, ".")
|
||||
for i, s := range labels {
|
||||
labels[i] = dnsname.SanitizeLabel(s)
|
||||
}
|
||||
|
||||
return strings.Join(labels, ".")
|
||||
}
|
||||
|
||||
func (r *repository) SaveTailnet(ctx context.Context, tailnet *Tailnet) error {
|
||||
@@ -25,22 +65,6 @@ func (r *repository) SaveTailnet(ctx context.Context, tailnet *Tailnet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) GetOrCreateTailnet(ctx context.Context, name string) (*Tailnet, bool, error) {
|
||||
tailnet := &Tailnet{}
|
||||
id := util.NextID()
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Where(Tailnet{Name: name}).
|
||||
Attrs(Tailnet{ID: id, ACLPolicy: DefaultPolicy()}).
|
||||
FirstOrCreate(tailnet)
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, false, tx.Error
|
||||
}
|
||||
|
||||
return tailnet, tailnet.ID == id, nil
|
||||
}
|
||||
|
||||
func (r *repository) GetTailnet(ctx context.Context, id uint64) (*Tailnet, error) {
|
||||
var t Tailnet
|
||||
tx := r.withContext(ctx).Take(&t, "id = ?", id)
|
||||
@@ -56,6 +80,21 @@ func (r *repository) GetTailnet(ctx context.Context, id uint64) (*Tailnet, error
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *repository) GetTailnetByName(ctx context.Context, name string) (*Tailnet, error) {
|
||||
var t Tailnet
|
||||
tx := r.withContext(ctx).Take(&t, "name = ?", name)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListTailnets(ctx context.Context) ([]Tailnet, error) {
|
||||
var tailnets = []Tailnet{}
|
||||
tx := r.withContext(ctx).Find(&tailnets)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeTailnetName(t *testing.T) {
|
||||
assert.Equal(t, "john.example.com", SanitizeTailnetName("john@example.com"))
|
||||
assert.Equal(t, "john.example.com", SanitizeTailnetName("john@examPle.Com"))
|
||||
assert.Equal(t, "john-doe.example.com", SanitizeTailnetName("john.doe@example.com"))
|
||||
assert.Equal(t, "johns-network", SanitizeTailnetName("John's Network"))
|
||||
assert.Equal(t, "example.com", SanitizeTailnetName("example.com"))
|
||||
assert.Equal(t, "johns-example.com", SanitizeTailnetName("John's example.com"))
|
||||
}
|
||||
+33
-8
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SystemRole string
|
||||
@@ -37,14 +38,25 @@ func (s UserRole) IsAdmin() bool {
|
||||
return s == UserRoleAdmin
|
||||
}
|
||||
|
||||
type UserRepository interface {
|
||||
GetOrCreateServiceUser(ctx context.Context, tailnet *Tailnet) (*User, bool, error)
|
||||
GetOrCreateUserWithAccount(ctx context.Context, tailnet *Tailnet, account *Account) (*User, bool, error)
|
||||
GetUser(ctx context.Context, userID uint64) (*User, error)
|
||||
DeleteUser(ctx context.Context, userID uint64) error
|
||||
ListUsers(ctx context.Context, tailnetID uint64) (Users, error)
|
||||
DeleteUsersByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
SetUserLastAuthenticated(ctx context.Context, userID uint64, timestamp time.Time) error
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
UserType UserType
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
AccountID *uint64
|
||||
Account *Account
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
UserType UserType
|
||||
LastAuthenticated *time.Time
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
AccountID *uint64
|
||||
Account *Account
|
||||
}
|
||||
|
||||
type Users []User
|
||||
@@ -100,7 +112,7 @@ func (r *repository) GetOrCreateUserWithAccount(ctx context.Context, tailnet *Ta
|
||||
|
||||
func (r *repository) GetUser(ctx context.Context, userID uint64) (*User, error) {
|
||||
var m User
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("Account").First(&m, "id = ? and user_type = ?", userID, UserTypePerson)
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("Account").Take(&m, "id = ?", userID)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -117,3 +129,16 @@ func (r *repository) DeleteUser(ctx context.Context, userID uint64) error {
|
||||
tx := r.withContext(ctx).Delete(&User{ID: userID})
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) SetUserLastAuthenticated(ctx context.Context, userID uint64, timestamp time.Time) error {
|
||||
tx := r.withContext(ctx).
|
||||
Model(User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{"last_authenticated": ×tamp})
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+253
-152
@@ -3,8 +3,10 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/addr"
|
||||
"github.com/jsiebens/ionscale/internal/provider"
|
||||
"github.com/jsiebens/ionscale/internal/auth"
|
||||
tpl "github.com/jsiebens/ionscale/internal/templates"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/mr-tron/base58"
|
||||
"net/http"
|
||||
@@ -20,7 +22,7 @@ import (
|
||||
|
||||
func NewAuthenticationHandlers(
|
||||
config *config.Config,
|
||||
authProvider provider.AuthProvider,
|
||||
authProvider auth.Provider,
|
||||
systemIAMPolicy *domain.IAMPolicy,
|
||||
repository domain.Repository) *AuthenticationHandlers {
|
||||
|
||||
@@ -34,43 +36,89 @@ func NewAuthenticationHandlers(
|
||||
|
||||
type AuthenticationHandlers struct {
|
||||
repository domain.Repository
|
||||
authProvider provider.AuthProvider
|
||||
authProvider auth.Provider
|
||||
config *config.Config
|
||||
systemIAMPolicy *domain.IAMPolicy
|
||||
}
|
||||
|
||||
type AuthFormData struct {
|
||||
ProviderAvailable bool
|
||||
Csrf string
|
||||
type AuthInput struct {
|
||||
Key string `param:"key"`
|
||||
Flow AuthFlow `param:"flow"`
|
||||
AuthKey string `query:"ak" form:"ak"`
|
||||
Oidc bool `query:"oidc" form:"oidc"`
|
||||
}
|
||||
|
||||
type TailnetSelectionData struct {
|
||||
AccountID uint64
|
||||
Tailnets []domain.Tailnet
|
||||
SystemAdmin bool
|
||||
Csrf string
|
||||
type EndAuthForm struct {
|
||||
AccountID uint64 `form:"aid"`
|
||||
TailnetID uint64 `form:"tid"`
|
||||
AsSystemAdmin bool `form:"sad"`
|
||||
AuthKey string `form:"ak"`
|
||||
State string `form:"state"`
|
||||
}
|
||||
|
||||
type oauthState struct {
|
||||
Key string
|
||||
Flow string
|
||||
Flow AuthFlow
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) StartCliAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
key := c.Param("key")
|
||||
type AuthFlow string
|
||||
|
||||
if s, err := h.repository.GetAuthenticationRequest(ctx, key); err != nil || s == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
const (
|
||||
AuthFlowMachineRegistration = "r"
|
||||
AuthFlowClient = "c"
|
||||
AuthFlowSSHCheckFlow = "s"
|
||||
)
|
||||
|
||||
func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var input AuthInput
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
// machine registration auth flow
|
||||
if input.Flow == AuthFlowMachineRegistration {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, input.Key)
|
||||
if err != nil || req == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if input.Oidc && h.authProvider != nil {
|
||||
goto startOidc
|
||||
}
|
||||
|
||||
if input.AuthKey != "" {
|
||||
return h.endMachineRegistrationFlow(c, EndAuthForm{AuthKey: input.AuthKey}, req)
|
||||
}
|
||||
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
return c.Render(http.StatusOK, "", tpl.Auth(h.authProvider != nil, csrf))
|
||||
}
|
||||
|
||||
// cli auth flow
|
||||
if input.Flow == AuthFlowClient {
|
||||
if s, err := h.repository.GetAuthenticationRequest(ctx, input.Key); err != nil || s == nil {
|
||||
return logError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ssh check auth flow
|
||||
if input.Flow == AuthFlowSSHCheckFlow {
|
||||
if s, err := h.repository.GetSSHActionRequest(ctx, input.Key); err != nil || s == nil {
|
||||
return logError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if h.authProvider == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(fmt.Errorf("unable to start auth flow as no auth provider is configured"))
|
||||
}
|
||||
|
||||
state, err := h.createState("c", key)
|
||||
startOidc:
|
||||
|
||||
state, err := h.createState(input.Flow, input.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
redirectUrl := h.authProvider.GetLoginURL(h.config.CreateUrl("/a/callback"), state)
|
||||
@@ -78,38 +126,27 @@ func (h *AuthenticationHandlers) StartCliAuth(c echo.Context) error {
|
||||
return c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
key := c.Param("key")
|
||||
|
||||
if req, err := h.repository.GetRegistrationRequestByKey(ctx, key); err != nil || req == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
return c.Render(http.StatusOK, "auth.html", &AuthFormData{ProviderAvailable: h.authProvider != nil, Csrf: csrf})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
key := c.Param("key")
|
||||
authKey := c.FormValue("ak")
|
||||
interactive := c.FormValue("s")
|
||||
var input AuthInput
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, key)
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, input.Key)
|
||||
if err != nil || req == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if authKey != "" {
|
||||
return h.endMachineRegistrationFlow(c, req, &oauthState{Key: key})
|
||||
if input.AuthKey != "" {
|
||||
return h.endMachineRegistrationFlow(c, EndAuthForm{AuthKey: input.AuthKey}, req)
|
||||
}
|
||||
|
||||
if interactive != "" {
|
||||
state, err := h.createState("r", key)
|
||||
if input.Oidc {
|
||||
state, err := h.createState(input.Flow, input.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
redirectUrl := h.authProvider.GetLoginURL(h.config.CreateUrl("/a/callback"), state)
|
||||
@@ -117,7 +154,7 @@ func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
return c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/"+key)
|
||||
return c.Redirect(http.StatusFound, fmt.Sprintf("/a/%s/%s", input.Flow, input.Key))
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
@@ -126,27 +163,68 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
code := c.QueryParam("code")
|
||||
state, err := h.readState(c.QueryParam("state"))
|
||||
if err != nil {
|
||||
return err
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid state parameter")
|
||||
}
|
||||
|
||||
user, err := h.exchangeUser(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnets, err := h.listAvailableTailnets(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
account, _, err := h.repository.GetOrCreateAccount(ctx, user.ID, user.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if err := h.repository.SetAccountLastAuthenticated(ctx, account.ID); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if state.Flow == AuthFlowSSHCheckFlow {
|
||||
sshActionReq, err := h.repository.GetSSHActionRequest(ctx, state.Key)
|
||||
if err != nil || sshActionReq == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
|
||||
machine, err := h.repository.GetMachine(ctx, sshActionReq.SrcMachineID)
|
||||
if err != nil || sshActionReq == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if !machine.HasTags() && machine.User.AccountID != nil && *machine.User.AccountID == account.ID {
|
||||
sshActionReq.Action = "accept"
|
||||
|
||||
err := h.repository.Transaction(func(rp domain.Repository) error {
|
||||
if err := rp.SetUserLastAuthenticated(ctx, machine.UserID, time.Now().UTC()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rp.SaveSSHActionRequest(ctx, sshActionReq); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
sshActionReq.Action = "reject"
|
||||
if err := h.repository.SaveSSHActionRequest(ctx, sshActionReq); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=nmo")
|
||||
}
|
||||
|
||||
tailnets, err := h.listAvailableTailnets(ctx, user)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
|
||||
if state.Flow == "r" {
|
||||
if state.Flow == AuthFlowMachineRegistration {
|
||||
if len(tailnets) == 0 {
|
||||
registrationRequest, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err == nil && registrationRequest != nil {
|
||||
@@ -155,18 +233,25 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{
|
||||
Csrf: csrf,
|
||||
Tailnets: tailnets,
|
||||
SystemAdmin: false,
|
||||
AccountID: account.ID,
|
||||
})
|
||||
|
||||
if len(tailnets) == 1 {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
if req == nil {
|
||||
return logError(fmt.Errorf("invalid registration key"))
|
||||
}
|
||||
return h.endMachineRegistrationFlow(c, EndAuthForm{AccountID: account.ID, TailnetID: tailnets[0].ID}, req)
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "", tpl.Tailnets(account.ID, false, tailnets, csrf))
|
||||
}
|
||||
|
||||
if state.Flow == "c" {
|
||||
isSystemAdmin, err := h.isSystemAdmin(ctx, user)
|
||||
if state.Flow == AuthFlowClient {
|
||||
isSystemAdmin, err := h.isSystemAdmin(user)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if !isSystemAdmin && len(tailnets) == 0 {
|
||||
@@ -177,99 +262,77 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{
|
||||
Csrf: csrf,
|
||||
Tailnets: tailnets,
|
||||
SystemAdmin: isSystemAdmin,
|
||||
AccountID: account.ID,
|
||||
})
|
||||
|
||||
return c.Render(http.StatusOK, "", tpl.Tailnets(account.ID, isSystemAdmin, tailnets, csrf))
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) isSystemAdmin(ctx context.Context, u *provider.User) (bool, error) {
|
||||
return h.systemIAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *provider.User) ([]domain.Tailnet, error) {
|
||||
var result = []domain.Tailnet{}
|
||||
tailnets, err := h.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
approved, err := t.IAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if approved {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) EndOAuth(c echo.Context) error {
|
||||
func (h *AuthenticationHandlers) EndAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
state, err := h.readState(c.QueryParam("state"))
|
||||
if err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
var form EndAuthForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if state.Flow == "r" {
|
||||
state, err := h.readState(form.State)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid state parameter")
|
||||
}
|
||||
|
||||
if state.Flow == AuthFlowMachineRegistration {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return h.endMachineRegistrationFlow(c, req, state)
|
||||
return h.endMachineRegistrationFlow(c, form, req)
|
||||
}
|
||||
|
||||
req, err := h.repository.GetAuthenticationRequest(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
if state.Flow == AuthFlowClient {
|
||||
req, err := h.repository.GetAuthenticationRequest(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return h.endCliAuthenticationFlow(c, form, req)
|
||||
}
|
||||
|
||||
return h.endCliAuthenticationFlow(c, req, state)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid state parameter")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) Success(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "success.html", nil)
|
||||
s := c.QueryParam("s")
|
||||
switch s {
|
||||
case "nma":
|
||||
return c.Render(http.StatusOK, "", tpl.NewMachine())
|
||||
}
|
||||
return c.Render(http.StatusOK, "", tpl.Success())
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) Error(c echo.Context) error {
|
||||
e := c.QueryParam("e")
|
||||
switch e {
|
||||
case "iak":
|
||||
return c.Render(http.StatusForbidden, "invalidauthkey.html", nil)
|
||||
return c.Render(http.StatusForbidden, "", tpl.InvalidAuthKey())
|
||||
case "ua":
|
||||
return c.Render(http.StatusForbidden, "unauthorized.html", nil)
|
||||
return c.Render(http.StatusForbidden, "", tpl.Unauthorized())
|
||||
case "nto":
|
||||
return c.Render(http.StatusForbidden, "notagowner.html", nil)
|
||||
return c.Render(http.StatusForbidden, "", tpl.NotTagOwner())
|
||||
case "nmo":
|
||||
return c.Render(http.StatusForbidden, "", tpl.NotMachineOwner())
|
||||
}
|
||||
return c.Render(http.StatusOK, "error.html", nil)
|
||||
return c.Render(http.StatusOK, "", tpl.Error())
|
||||
}
|
||||
|
||||
type TailnetSelectionForm struct {
|
||||
AccountID uint64 `form:"aid"`
|
||||
TailnetID uint64 `form:"tid"`
|
||||
AsSystemAdmin bool `form:"sad"`
|
||||
AuthKey string `form:"ak"`
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *domain.AuthenticationRequest, state *oauthState) error {
|
||||
func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, form EndAuthForm, req *domain.AuthenticationRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var form TailnetSelectionForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
account, err := h.repository.GetAccount(ctx, form.AccountID)
|
||||
if err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
// continue as system admin?
|
||||
@@ -280,27 +343,27 @@ func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *d
|
||||
|
||||
err := h.repository.Transaction(func(rp domain.Repository) error {
|
||||
if err := rp.SaveSystemApiKey(ctx, apiKey); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
if err := rp.SaveAuthenticationRequest(ctx, req); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
tailnet, err := h.repository.GetTailnet(ctx, form.TailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
user, _, err := h.repository.GetOrCreateUserWithAccount(ctx, tailnet, account)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
@@ -309,6 +372,9 @@ func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *d
|
||||
req.TailnetID = &tailnet.ID
|
||||
|
||||
err = h.repository.Transaction(func(rp domain.Repository) error {
|
||||
if err := rp.SetUserLastAuthenticated(ctx, user.ID, time.Now().UTC()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rp.SaveApiKey(ctx, apiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -318,20 +384,15 @@ func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *d
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, registrationRequest *domain.RegistrationRequest, state *oauthState) error {
|
||||
func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form EndAuthForm, registrationRequest *domain.RegistrationRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var form TailnetSelectionForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
req := tailcfg.RegisterRequest(registrationRequest.Data)
|
||||
machineKey := registrationRequest.MachineKey
|
||||
nodeKey := req.NodeKey.String()
|
||||
@@ -340,11 +401,12 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
var user *domain.User
|
||||
var ephemeral bool
|
||||
var tags = []string{}
|
||||
var authorized = false
|
||||
|
||||
if form.AuthKey != "" {
|
||||
authKey, err := h.repository.LoadAuthKey(ctx, form.AuthKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if authKey == nil {
|
||||
@@ -353,7 +415,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
registrationRequest.Error = "invalid auth key"
|
||||
|
||||
if err := h.repository.SaveRegistrationRequest(ctx, registrationRequest); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=iak")
|
||||
@@ -363,20 +425,21 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
user = &authKey.User
|
||||
tags = authKey.Tags
|
||||
ephemeral = authKey.Ephemeral
|
||||
authorized = authKey.PreAuthorized
|
||||
} else {
|
||||
selectedTailnet, err := h.repository.GetTailnet(ctx, form.TailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
account, err := h.repository.GetAccount(ctx, form.AccountID)
|
||||
if err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
selectedUser, _, err := h.repository.GetOrCreateUserWithAccount(ctx, selectedTailnet, account)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
user = selectedUser
|
||||
@@ -384,20 +447,22 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
ephemeral = false
|
||||
}
|
||||
|
||||
if err := tailnet.ACLPolicy.CheckTagOwners(registrationRequest.Data.Hostinfo.RequestTags, user); err != nil {
|
||||
if err := tailnet.ACLPolicy.Get().CheckTagOwners(registrationRequest.Data.Hostinfo.RequestTags, user); err != nil {
|
||||
registrationRequest.Authenticated = false
|
||||
registrationRequest.Error = err.Error()
|
||||
if err := h.repository.SaveRegistrationRequest(ctx, registrationRequest); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
return logError(err)
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=nto")
|
||||
}
|
||||
|
||||
autoAllowIPs := tailnet.ACLPolicy.Get().FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, user)
|
||||
|
||||
var m *domain.Machine
|
||||
|
||||
m, err := h.repository.GetMachineByKey(ctx, tailnet.ID, machineKey)
|
||||
m, err := h.repository.GetMachineByKeyAndUser(ctx, machineKey, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
@@ -410,7 +475,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
m = &domain.Machine{
|
||||
@@ -419,20 +484,24 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
NameIdx: nameIdx,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: nodeKey,
|
||||
Ephemeral: ephemeral,
|
||||
Ephemeral: ephemeral || req.Ephemeral,
|
||||
RegisteredTags: registeredTags,
|
||||
Tags: domain.SanitizeTags(tags),
|
||||
AutoAllowIPs: autoAllowIPs,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(180 * 24 * time.Hour).UTC(),
|
||||
KeyExpiryDisabled: len(tags) != 0,
|
||||
Authorized: !tailnet.MachineAuthorizationEnabled || authorized,
|
||||
|
||||
User: *user,
|
||||
Tailnet: *tailnet,
|
||||
User: *user,
|
||||
UserID: user.ID,
|
||||
Tailnet: *tailnet,
|
||||
TailnetID: tailnet.ID,
|
||||
}
|
||||
|
||||
ipv4, ipv6, err := addr.SelectIP(checkIP(ctx, h.repository.CountMachinesWithIPv4))
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
m.IPv4 = domain.IP{Addr: ipv4}
|
||||
m.IPv6 = domain.IP{Addr: ipv6}
|
||||
@@ -445,15 +514,16 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
if m.Name != sanitizeHostname {
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
m.Name = sanitizeHostname
|
||||
m.NameIdx = nameIdx
|
||||
}
|
||||
m.NodeKey = nodeKey
|
||||
m.Ephemeral = ephemeral
|
||||
m.Ephemeral = ephemeral || req.Ephemeral
|
||||
m.RegisteredTags = registeredTags
|
||||
m.Tags = domain.SanitizeTags(tags)
|
||||
m.AutoAllowIPs = autoAllowIPs
|
||||
m.UserID = user.ID
|
||||
m.User = *user
|
||||
m.TailnetID = tailnet.ID
|
||||
@@ -464,6 +534,11 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
err = h.repository.Transaction(func(rp domain.Repository) error {
|
||||
registrationRequest.Authenticated = true
|
||||
registrationRequest.Error = ""
|
||||
registrationRequest.UserID = user.ID
|
||||
|
||||
if err := rp.SetUserLastAuthenticated(ctx, m.UserID, time.Now().UTC()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := rp.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
@@ -477,13 +552,39 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
if m.Authorized {
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
} else {
|
||||
return c.Redirect(http.StatusFound, "/a/success?s=nma")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) exchangeUser(code string) (*provider.User, error) {
|
||||
func (h *AuthenticationHandlers) isSystemAdmin(u *auth.User) (bool, error) {
|
||||
return h.systemIAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *auth.User) ([]domain.Tailnet, error) {
|
||||
var result = []domain.Tailnet{}
|
||||
tailnets, err := h.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
approved, err := t.IAMPolicy.Get().EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if approved {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) exchangeUser(code string) (*auth.User, error) {
|
||||
redirectUrl := h.config.CreateUrl("/a/callback")
|
||||
|
||||
user, err := h.authProvider.Exchange(redirectUrl, code)
|
||||
@@ -494,7 +595,7 @@ func (h *AuthenticationHandlers) exchangeUser(code string) (*provider.User, erro
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) createState(flow string, key string) (string, error) {
|
||||
func (h *AuthenticationHandlers) createState(flow AuthFlow, key string) (string, error) {
|
||||
stateMap := oauthState{Key: key, Flow: flow}
|
||||
marshal, err := json.Marshal(&stateMap)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func NewDERPHandler() *DERPHandlers {
|
||||
logger := zap.L().Named("derp")
|
||||
return &DERPHandlers{
|
||||
s: derp.NewServer(key.NewNode(), func(format string, args ...any) {
|
||||
logger.Debug(fmt.Sprintf(format, args...))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type DERPHandlers struct {
|
||||
s *derp.Server
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) Handler(c echo.Context) error {
|
||||
derphttp.Handler(h.s).ServeHTTP(c.Response(), c.Request())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) LatencyCheck(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) DebugTraffic(c echo.Context) error {
|
||||
h.s.ServeDebugTraffic(c.Response(), c.Request())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) DebugCheck(c echo.Context) error {
|
||||
if err := h.s.ConsistencyCheck(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.String(http.StatusOK, "DERP Server ConsistencyCheck okay")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/internal/dns"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewDNSHandlers(_ key.MachinePublic, provider dns.Provider) *DNSHandlers {
|
||||
return &DNSHandlers{
|
||||
provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
type DNSHandlers struct {
|
||||
provider dns.Provider
|
||||
}
|
||||
|
||||
func (h *DNSHandlers) SetDNS(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
req := &tailcfg.SetDNSRequest{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if h.provider == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if err := h.provider.SetRecord(ctx, req.Type, req.Name, req.Value); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Name, "_acme-challenge") && req.Type == "TXT" {
|
||||
// Listen to connection close
|
||||
notify := ctx.Done()
|
||||
timeout := time.After(5 * time.Minute)
|
||||
tick := time.NewTicker(5 * time.Second)
|
||||
|
||||
defer func() { tick.Stop() }()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
txtrecords, _ := net.LookupTXT(req.Name)
|
||||
for _, txt := range txtrecords {
|
||||
if txt == req.Value {
|
||||
return c.JSON(http.StatusOK, tailcfg.SetDNSResponse{})
|
||||
}
|
||||
}
|
||||
case <-timeout:
|
||||
return c.JSON(http.StatusOK, tailcfg.SetDNSResponse{})
|
||||
case <-notify:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, tailcfg.SetDNSResponse{})
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func httpsRedirectSkipper(c config.Tls) func(ctx echo.Context) bool {
|
||||
@@ -23,38 +20,3 @@ func HttpsRedirect(c config.Tls) echo.MiddlewareFunc {
|
||||
Skipper: httpsRedirectSkipper(c),
|
||||
})
|
||||
}
|
||||
|
||||
func HttpRedirectHandler(tls config.Tls) echo.HandlerFunc {
|
||||
if tls.Disable {
|
||||
return IndexHandler(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if tls.AcmeEnabled {
|
||||
cfg := certmagic.NewDefault()
|
||||
if len(cfg.Issuers) > 0 {
|
||||
if am, ok := cfg.Issuers[0].(*certmagic.ACMEIssuer); ok {
|
||||
handler := am.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler))
|
||||
return echo.WrapHandler(handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return echo.WrapHandler(http.HandlerFunc(httpRedirectHandler))
|
||||
}
|
||||
|
||||
func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
toURL := "https://"
|
||||
requestHost := hostOnly(r.Host)
|
||||
toURL += requestHost
|
||||
toURL += r.URL.RequestURI()
|
||||
w.Header().Set("Connection", "close")
|
||||
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func hostOnly(hostport string) string {
|
||||
host, _, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewIDTokenHandlers(machineKey key.MachinePublic, config *config.Config, repository domain.Repository) *IDTokenHandlers {
|
||||
return &IDTokenHandlers{
|
||||
machineKey: machineKey,
|
||||
issuer: config.PublicUrl.String(),
|
||||
repository: repository,
|
||||
}
|
||||
}
|
||||
|
||||
func NewOIDCConfigHandlers(config *config.Config, repository domain.Repository) *OIDCConfigHandlers {
|
||||
return &OIDCConfigHandlers{
|
||||
issuer: config.PublicUrl.String(),
|
||||
jwksUri: config.CreateUrl("/.well-known/jwks"),
|
||||
repository: repository,
|
||||
}
|
||||
}
|
||||
|
||||
type IDTokenHandlers struct {
|
||||
machineKey key.MachinePublic
|
||||
issuer string
|
||||
repository domain.Repository
|
||||
}
|
||||
|
||||
func (h *IDTokenHandlers) FetchToken(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
keySet, err := h.repository.GetJSONWebKeySet(c.Request().Context())
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
req := &tailcfg.TokenRequest{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
machineKey := h.machineKey.String()
|
||||
nodeKey := req.NodeKey.String()
|
||||
|
||||
var m *domain.Machine
|
||||
m, err = h.repository.GetMachineByKeys(ctx, machineKey, nodeKey)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
_, tailnetDomain, sub := h.names(m)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"jit": fmt.Sprintf("%d", util.NextID()),
|
||||
"iss": h.issuer,
|
||||
"sub": sub,
|
||||
"aud": []string{req.Audience},
|
||||
"exp": jwt.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
"nbf": jwt.NewNumericDate(now),
|
||||
"iat": jwt.NewNumericDate(now),
|
||||
|
||||
"key": m.NodeKey,
|
||||
"addresses": []string{m.IPv4.String(), m.IPv6.String()},
|
||||
"nid": m.ID,
|
||||
"node": sub,
|
||||
"domain": tailnetDomain,
|
||||
}
|
||||
|
||||
if m.HasTags() {
|
||||
tags := []string{}
|
||||
for _, t := range m.Tags {
|
||||
tags = append(tags, fmt.Sprintf("%s:%s", tailnetDomain, t))
|
||||
}
|
||||
claims["tags"] = tags
|
||||
} else {
|
||||
claims["user"] = fmt.Sprintf("%s:%s", tailnetDomain, m.User.Name)
|
||||
claims["uid"] = m.UserID
|
||||
}
|
||||
|
||||
unsignedToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
unsignedToken.Header["kid"] = keySet.Key.Id
|
||||
|
||||
jwtB64, err := unsignedToken.SignedString(&keySet.Key.PrivateKey)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
resp := tailcfg.TokenResponse{IDToken: jwtB64}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type OIDCConfigHandlers struct {
|
||||
issuer string
|
||||
jwksUri string
|
||||
repository domain.Repository
|
||||
}
|
||||
|
||||
func (h *OIDCConfigHandlers) OpenIDConfig(c echo.Context) error {
|
||||
v := map[string]interface{}{}
|
||||
|
||||
v["issuer"] = h.issuer
|
||||
v["jwks_uri"] = h.jwksUri
|
||||
v["subject_types_supported"] = []string{"public"}
|
||||
v["response_types_supported"] = []string{"id_token"}
|
||||
v["scopes_supported"] = []string{"openid"}
|
||||
v["id_token_signing_alg_values_supported"] = []string{"RS256"}
|
||||
v["claims_supported"] = []string{
|
||||
"sub",
|
||||
"aud",
|
||||
"exp",
|
||||
"iat",
|
||||
"iss",
|
||||
"jti",
|
||||
"nbf",
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, v)
|
||||
}
|
||||
|
||||
func (h *OIDCConfigHandlers) Jwks(c echo.Context) error {
|
||||
keySet, err := h.repository.GetJSONWebKeySet(c.Request().Context())
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
pub := jose.JSONWebKey{Key: keySet.Key.Public(), KeyID: keySet.Key.Id, Algorithm: "RS256", Use: "sig"}
|
||||
set := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{pub}}
|
||||
return c.JSON(http.StatusOK, set)
|
||||
}
|
||||
|
||||
func (h *IDTokenHandlers) names(m *domain.Machine) (string, string, string) {
|
||||
var name = m.Name
|
||||
if m.NameIdx != 0 {
|
||||
name = fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
||||
}
|
||||
|
||||
sanitizedTailnetName := domain.SanitizeTailnetName(m.Tailnet.Name)
|
||||
return name, sanitizedTailnetName, fmt.Sprintf("%s.%s", name, sanitizedTailnetName)
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
tpl "github.com/jsiebens/ionscale/internal/templates"
|
||||
"github.com/jsiebens/ionscale/internal/version"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func IndexHandler(code int) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
info, s := version.GetReleaseInfo()
|
||||
data := map[string]interface{}{
|
||||
"Version": info,
|
||||
"Revision": s,
|
||||
}
|
||||
return c.Render(code, "index.html", data)
|
||||
return c.Render(code, "", tpl.Index(version.GetReleaseInfo()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func KeyHandler(keys *config.ServerKeys) echo.HandlerFunc {
|
||||
if v != "" {
|
||||
clientCapabilityVersion, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return c.String(http.StatusBadRequest, "Invalid version")
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid version")
|
||||
}
|
||||
|
||||
if clientCapabilityVersion >= NoiseCapabilityVersion {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"io"
|
||||
"net/http"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -26,14 +27,49 @@ func NewNoiseHandlers(controlKey key.MachinePrivate, createPeerHandler CreatePee
|
||||
}
|
||||
|
||||
func (h *NoiseHandlers) Upgrade(c echo.Context) error {
|
||||
conn, err := controlhttp.AcceptHTTP(context.Background(), c.Response(), c.Request(), h.controlKey)
|
||||
conn, err := controlhttp.AcceptHTTP(c.Request().Context(), c.Response(), c.Request(), h.controlKey, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
handler := h.createPeerHandler(conn.Peer())
|
||||
|
||||
server := http.Server{}
|
||||
server.Handler = h2c.NewHandler(handler, &http2.Server{})
|
||||
return server.Serve(netutil.NewOneConnListener(conn, nil))
|
||||
if err := server.Serve(netutil.NewOneConnListener(conn, nil)); err != nil && !stderrors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type JsonBinder struct {
|
||||
echo.DefaultBinder
|
||||
}
|
||||
|
||||
func (b JsonBinder) Bind(i interface{}, c echo.Context) error {
|
||||
if err := b.BindPathParams(c, i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method := c.Request().Method
|
||||
if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {
|
||||
if err := b.BindQueryParams(c, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Request().ContentLength == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.Echo().JSONSerializer.Deserialize(c, i); err != nil {
|
||||
switch err.(type) {
|
||||
case *echo.HTTPError:
|
||||
return err
|
||||
default:
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,60 +2,57 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/internal/bind"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/core"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/mapping"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"sync"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewPollNetMapHandler(
|
||||
createBinder bind.Factory,
|
||||
brokers broker.Pubsub,
|
||||
repository domain.Repository,
|
||||
offlineTimers *OfflineTimers) *PollNetMapHandler {
|
||||
machineKey key.MachinePublic,
|
||||
sessionManager core.PollMapSessionManager,
|
||||
repository domain.Repository) *PollNetMapHandler {
|
||||
|
||||
handler := &PollNetMapHandler{
|
||||
createBinder: createBinder,
|
||||
brokers: brokers,
|
||||
repository: repository,
|
||||
offlineTimers: offlineTimers,
|
||||
machineKey: machineKey,
|
||||
sessionManager: sessionManager,
|
||||
repository: repository,
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
type PollNetMapHandler struct {
|
||||
createBinder bind.Factory
|
||||
repository domain.Repository
|
||||
brokers broker.Pubsub
|
||||
offlineTimers *OfflineTimers
|
||||
machineKey key.MachinePublic
|
||||
repository domain.Repository
|
||||
sessionManager core.PollMapSessionManager
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) PollNetMap(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
binder, err := h.createBinder(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &tailcfg.MapRequest{}
|
||||
if err := binder.BindRequest(c, req); err != nil {
|
||||
return err
|
||||
if err := c.Bind(req); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
machineKey := binder.Peer().String()
|
||||
machineKey := h.machineKey.String()
|
||||
nodeKey := req.NodeKey.String()
|
||||
|
||||
var m *domain.Machine
|
||||
m, err = h.repository.GetMachineByKeys(ctx, machineKey, nodeKey)
|
||||
m, err := h.repository.GetMachineByKeys(ctx, machineKey, nodeKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
@@ -63,13 +60,13 @@ func (h *PollNetMapHandler) PollNetMap(c echo.Context) error {
|
||||
}
|
||||
|
||||
if req.ReadOnly {
|
||||
return h.handleReadOnly(c, binder, m, req)
|
||||
return h.handleReadOnly(c, m, req)
|
||||
} else {
|
||||
return h.handleUpdate(c, binder, m, req)
|
||||
return h.handleUpdate(c, m, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *domain.Machine, mapRequest *tailcfg.MapRequest) error {
|
||||
func (h *PollNetMapHandler) handleUpdate(c echo.Context, m *domain.Machine, mapRequest *tailcfg.MapRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
now := time.Now().UTC()
|
||||
@@ -80,59 +77,54 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
m.LastSeen = &now
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
tailnetID := m.TailnetID
|
||||
machineID := m.ID
|
||||
|
||||
h.brokers.Publish(tailnetID, &broker.Signal{PeerUpdated: &machineID})
|
||||
h.sessionManager.NotifyAll(tailnetID, m.ID)
|
||||
|
||||
if !mapRequest.Stream {
|
||||
return c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
var syncedPeers = make(map[uint64]bool)
|
||||
mapper := mapping.NewPollNetMapper(mapRequest, m.ID, h.repository, h.sessionManager)
|
||||
|
||||
response, syncedPeers, err := h.createMapResponse(m, binder, mapRequest, false, make(map[uint64]bool))
|
||||
response, err := h.createMapResponse(mapper, false, mapRequest.Compress)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
updateChan := make(chan *broker.Signal, 20)
|
||||
|
||||
unsubscribe, err := h.brokers.Subscribe(tailnetID, updateChan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.cancelOfflineMessage(machineID)
|
||||
updateChan := make(chan *core.Ping, 20)
|
||||
h.sessionManager.Register(m.TailnetID, m.ID, updateChan)
|
||||
|
||||
// Listen to connection close
|
||||
notify := c.Request().Context().Done()
|
||||
|
||||
keepAliveResponse, err := h.createKeepAliveResponse(binder, mapRequest)
|
||||
keepAliveResponse, err := h.createKeepAliveResponse(mapRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
keepAliveTicker := time.NewTicker(config.KeepAliveInterval)
|
||||
syncTicker := time.NewTicker(5 * time.Second)
|
||||
|
||||
c.Response().WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := c.Response().Write(response); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
c.Response().Flush()
|
||||
|
||||
connectedDevices.WithLabelValues(m.Tailnet.Name).Inc()
|
||||
|
||||
keepAliveTicker := time.NewTicker(config.KeepAliveInterval())
|
||||
syncTicker := time.NewTicker(5 * time.Second)
|
||||
|
||||
defer func() {
|
||||
connectedDevices.WithLabelValues(m.Tailnet.Name).Dec()
|
||||
unsubscribe()
|
||||
h.sessionManager.Deregister(m.TailnetID, m.ID)
|
||||
keepAliveTicker.Stop()
|
||||
syncTicker.Stop()
|
||||
_ = h.repository.SetMachineLastSeen(ctx, machineID)
|
||||
h.scheduleOfflineMessage(tailnetID, machineID)
|
||||
}()
|
||||
|
||||
var latestSync = time.Now()
|
||||
@@ -145,7 +137,7 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
case <-keepAliveTicker.C:
|
||||
if mapRequest.KeepAlive {
|
||||
if _, err := c.Response().Write(keepAliveResponse); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
_ = h.repository.SetMachineLastSeen(ctx, machineID)
|
||||
c.Response().Flush()
|
||||
@@ -154,7 +146,7 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
if latestSync.Before(latestUpdate) {
|
||||
machine, err := h.repository.GetMachine(ctx, machineID)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
if machine == nil {
|
||||
return nil
|
||||
@@ -163,14 +155,14 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
var payload []byte
|
||||
var payloadErr error
|
||||
|
||||
payload, syncedPeers, payloadErr = h.createMapResponse(machine, binder, mapRequest, true, syncedPeers)
|
||||
payload, payloadErr = h.createMapResponse(mapper, true, mapRequest.Compress)
|
||||
|
||||
if payloadErr != nil {
|
||||
return payloadErr
|
||||
}
|
||||
|
||||
if _, err := c.Response().Write(payload); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
c.Response().Flush()
|
||||
|
||||
@@ -182,193 +174,77 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) handleReadOnly(c echo.Context, binder bind.Binder, m *domain.Machine, request *tailcfg.MapRequest) error {
|
||||
func (h *PollNetMapHandler) handleReadOnly(c echo.Context, m *domain.Machine, request *tailcfg.MapRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
m.HostInfo = domain.HostInfo(*request.Hostinfo)
|
||||
m.DiscoKey = request.DiscoKey.String()
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
response, _, err := h.createMapResponse(m, binder, request, false, map[uint64]bool{})
|
||||
mapper := mapping.NewPollNetMapper(request, m.ID, h.repository, h.sessionManager)
|
||||
payload, err := h.createMapResponse(mapper, false, request.Compress)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
_, err = c.Response().Write(response)
|
||||
return err
|
||||
_, err = c.Response().Write(payload)
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) scheduleOfflineMessage(tailnetID, machineID uint64) {
|
||||
h.offlineTimers.startCh <- [2]uint64{tailnetID, machineID}
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) cancelOfflineMessage(machineID uint64) {
|
||||
h.offlineTimers.stopCh <- machineID
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) createKeepAliveResponse(binder bind.Binder, request *tailcfg.MapRequest) ([]byte, error) {
|
||||
func (h *PollNetMapHandler) createKeepAliveResponse(request *tailcfg.MapRequest) ([]byte, error) {
|
||||
mapResponse := &tailcfg.MapResponse{
|
||||
KeepAlive: true,
|
||||
}
|
||||
|
||||
return binder.Marshal(request.Compress, mapResponse)
|
||||
return h.marshalResponse(request.Compress, mapResponse)
|
||||
}
|
||||
|
||||
func (h *PollNetMapHandler) createMapResponse(m *domain.Machine, binder bind.Binder, request *tailcfg.MapRequest, delta bool, prevSyncedPeerIDs map[uint64]bool) ([]byte, map[uint64]bool, error) {
|
||||
ctx := context.TODO()
|
||||
|
||||
node, user, err := mapping.ToNode(m)
|
||||
func (h *PollNetMapHandler) createMapResponse(m *mapping.PollNetMapper, delta bool, compress string) ([]byte, error) {
|
||||
response, err := m.CreateMapResponse(context.Background(), delta)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
return h.marshalResponse(compress, response)
|
||||
}
|
||||
|
||||
tailnet, err := h.repository.GetTailnet(ctx, m.TailnetID)
|
||||
func (h *PollNetMapHandler) marshalResponse(compress string, v interface{}) ([]byte, error) {
|
||||
var payload []byte
|
||||
|
||||
marshalled, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies := tailnet.ACLPolicy
|
||||
var users = []tailcfg.UserProfile{*user}
|
||||
var changedPeers []*tailcfg.Node
|
||||
var removedPeers []tailcfg.NodeID
|
||||
|
||||
candidatePeers, err := h.repository.ListMachinePeers(ctx, m.TailnetID, m.MachineKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
syncedPeerIDs := map[uint64]bool{}
|
||||
syncedUserIDs := map[tailcfg.UserID]bool{}
|
||||
|
||||
for _, peer := range candidatePeers {
|
||||
if peer.IsExpired() {
|
||||
continue
|
||||
}
|
||||
if policies.IsValidPeer(m, &peer) || policies.IsValidPeer(&peer, m) {
|
||||
n, u, err := mapping.ToNode(&peer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
changedPeers = append(changedPeers, n)
|
||||
syncedPeerIDs[peer.ID] = true
|
||||
delete(prevSyncedPeerIDs, peer.ID)
|
||||
|
||||
if _, ok := syncedUserIDs[u.ID]; !ok {
|
||||
users = append(users, *u)
|
||||
syncedUserIDs[u.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for p, _ := range prevSyncedPeerIDs {
|
||||
removedPeers = append(removedPeers, tailcfg.NodeID(p))
|
||||
}
|
||||
|
||||
dnsConfig := tailnet.DNSConfig
|
||||
|
||||
derpMap, err := h.repository.GetDERPMap(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rules := policies.BuildFilterRules(candidatePeers, m)
|
||||
|
||||
controlTime := time.Now().UTC()
|
||||
var mapResponse *tailcfg.MapResponse
|
||||
|
||||
if !delta {
|
||||
mapResponse = &tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
DNSConfig: mapping.ToDNSConfig(&m.Tailnet, &dnsConfig),
|
||||
PacketFilter: rules,
|
||||
DERPMap: derpMap,
|
||||
Domain: dnsname.SanitizeHostname(m.Tailnet.Name),
|
||||
Peers: changedPeers,
|
||||
UserProfiles: users,
|
||||
ControlTime: &controlTime,
|
||||
Debug: &tailcfg.Debug{
|
||||
DisableLogTail: true,
|
||||
},
|
||||
}
|
||||
if compress == "zstd" {
|
||||
payload = zstdEncode(marshalled)
|
||||
} else {
|
||||
mapResponse = &tailcfg.MapResponse{
|
||||
Node: node,
|
||||
DNSConfig: mapping.ToDNSConfig(&m.Tailnet, &dnsConfig),
|
||||
PacketFilter: rules,
|
||||
DERPMap: derpMap,
|
||||
Domain: dnsname.SanitizeHostname(m.Tailnet.Name),
|
||||
PeersChanged: changedPeers,
|
||||
PeersRemoved: removedPeers,
|
||||
UserProfiles: users,
|
||||
ControlTime: &controlTime,
|
||||
payload = marshalled
|
||||
}
|
||||
|
||||
data := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(data, uint32(len(payload)))
|
||||
data = append(data, payload...)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func zstdEncode(in []byte) []byte {
|
||||
encoder := zstdEncoderPool.Get().(*zstd.Encoder)
|
||||
out := encoder.EncodeAll(in, nil)
|
||||
_ = encoder.Close()
|
||||
zstdEncoderPool.Put(encoder)
|
||||
return out
|
||||
}
|
||||
|
||||
var zstdEncoderPool = &sync.Pool{
|
||||
New: func() any {
|
||||
encoder, err := smallzstd.NewEncoder(nil, zstd.WithEncoderLevel(zstd.SpeedFastest))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if request.OmitPeers {
|
||||
mapResponse.PeersChanged = nil
|
||||
mapResponse.PeersRemoved = nil
|
||||
mapResponse.Peers = nil
|
||||
}
|
||||
|
||||
payload, err := binder.Marshal(request.Compress, mapResponse)
|
||||
|
||||
return payload, syncedPeerIDs, nil
|
||||
}
|
||||
|
||||
func NewOfflineTimers(repository domain.Repository, pubsub broker.Pubsub) *OfflineTimers {
|
||||
return &OfflineTimers{
|
||||
repository: repository,
|
||||
pubsub: pubsub,
|
||||
data: make(map[uint64]*time.Timer),
|
||||
startCh: make(chan [2]uint64),
|
||||
stopCh: make(chan uint64),
|
||||
}
|
||||
}
|
||||
|
||||
type OfflineTimers struct {
|
||||
repository domain.Repository
|
||||
pubsub broker.Pubsub
|
||||
data map[uint64]*time.Timer
|
||||
stopCh chan uint64
|
||||
startCh chan [2]uint64
|
||||
}
|
||||
|
||||
func (o *OfflineTimers) Start() {
|
||||
for {
|
||||
select {
|
||||
case i := <-o.startCh:
|
||||
o.scheduleOfflineMessage(i[0], i[1])
|
||||
case m := <-o.stopCh:
|
||||
o.cancelOfflineMessage(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OfflineTimers) scheduleOfflineMessage(tailnetID, machineID uint64) {
|
||||
t, ok := o.data[machineID]
|
||||
if ok {
|
||||
t.Stop()
|
||||
delete(o.data, machineID)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(config.KeepAliveInterval)
|
||||
go func() {
|
||||
<-timer.C
|
||||
o.pubsub.Publish(tailnetID, &broker.Signal{PeerUpdated: &machineID})
|
||||
o.stopCh <- machineID
|
||||
}()
|
||||
|
||||
o.data[machineID] = timer
|
||||
}
|
||||
|
||||
func (o *OfflineTimers) cancelOfflineMessage(machineID uint64) {
|
||||
t, ok := o.data[machineID]
|
||||
if ok {
|
||||
t.Stop()
|
||||
delete(o.data, machineID)
|
||||
}
|
||||
return encoder
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/dns"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func NewQueryFeatureHandlers(machineKey key.MachinePublic, dnsProvider dns.Provider, repository domain.Repository) *QueryFeatureHandlers {
|
||||
return &QueryFeatureHandlers{
|
||||
machineKey: machineKey,
|
||||
dnsProvider: dnsProvider,
|
||||
repository: repository,
|
||||
}
|
||||
}
|
||||
|
||||
type QueryFeatureHandlers struct {
|
||||
machineKey key.MachinePublic
|
||||
dnsProvider dns.Provider
|
||||
repository domain.Repository
|
||||
}
|
||||
|
||||
func (h *QueryFeatureHandlers) QueryFeature(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
req := new(tailcfg.QueryFeatureRequest)
|
||||
if err := c.Bind(req); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
machineKey := h.machineKey.String()
|
||||
nodeKey := req.NodeKey.String()
|
||||
|
||||
resp := tailcfg.QueryFeatureResponse{Complete: true}
|
||||
|
||||
switch req.Feature {
|
||||
case "serve":
|
||||
machine, err := h.repository.GetMachineByKeys(ctx, machineKey, nodeKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if machine == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if h.dnsProvider == nil || !machine.Tailnet.DNSConfig.HttpsCertsEnabled {
|
||||
resp.Text = fmt.Sprintf(serverMessage, machine.Tailnet.Name)
|
||||
resp.Complete = false
|
||||
}
|
||||
case "funnel":
|
||||
resp.Text = fmt.Sprintf("Sorry, ionscale has no support for feature '%s'\n", req.Feature)
|
||||
resp.Complete = false
|
||||
default:
|
||||
resp.Text = fmt.Sprintf("Unknown feature request '%s'\n", req.Feature)
|
||||
resp.Complete = false
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
const serverMessage = `Enabling HTTPS is required to use Serve:
|
||||
|
||||
ionscale tailnets set-dns --tailnet %s --https-certs=true --magic-dns
|
||||
`
|
||||
@@ -3,86 +3,88 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/internal/addr"
|
||||
"github.com/jsiebens/ionscale/internal/bind"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/core"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/mapping"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewRegistrationHandlers(
|
||||
createBinder bind.Factory,
|
||||
machineKey key.MachinePublic,
|
||||
config *config.Config,
|
||||
brokers broker.Pubsub,
|
||||
sessionManager core.PollMapSessionManager,
|
||||
repository domain.Repository) *RegistrationHandlers {
|
||||
return &RegistrationHandlers{
|
||||
createBinder: createBinder,
|
||||
pubsub: brokers,
|
||||
repository: repository,
|
||||
config: config,
|
||||
machineKey: machineKey,
|
||||
sessionManager: sessionManager,
|
||||
repository: repository,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type RegistrationHandlers struct {
|
||||
createBinder bind.Factory
|
||||
repository domain.Repository
|
||||
pubsub broker.Pubsub
|
||||
config *config.Config
|
||||
machineKey key.MachinePublic
|
||||
repository domain.Repository
|
||||
sessionManager core.PollMapSessionManager
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func (h *RegistrationHandlers) Register(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
binder, err := h.createBinder(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &tailcfg.RegisterRequest{}
|
||||
if err := binder.BindRequest(c, req); err != nil {
|
||||
return err
|
||||
if err := c.Bind(req); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
machineKey := binder.Peer().String()
|
||||
machineKey := h.machineKey.String()
|
||||
nodeKey := req.NodeKey.String()
|
||||
|
||||
var m *domain.Machine
|
||||
m, err = h.repository.GetMachineByKeys(ctx, machineKey, nodeKey)
|
||||
m, err := h.repository.GetMachineByKeys(ctx, machineKey, nodeKey)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if m != nil {
|
||||
if m.IsExpired() {
|
||||
response := tailcfg.RegisterResponse{NodeKeyExpired: true}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
if !req.Expiry.IsZero() && req.Expiry.Before(time.Now()) {
|
||||
m.ExpiresAt = req.Expiry
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
if m.Ephemeral {
|
||||
if _, err := h.repository.DeleteMachine(ctx, m.ID); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
h.sessionManager.NotifyAll(m.TailnetID)
|
||||
} else {
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
h.sessionManager.NotifyAll(m.TailnetID)
|
||||
}
|
||||
|
||||
h.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
response := tailcfg.RegisterResponse{NodeKeyExpired: true}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
if m.Name != sanitizeHostname {
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
m.Name = sanitizeHostname
|
||||
m.NameIdx = nameIdx
|
||||
@@ -93,26 +95,33 @@ func (h *RegistrationHandlers) Register(c echo.Context) error {
|
||||
m.Tags = append(m.RegisteredTags, advertisedTags...)
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: true}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
tUser, tLogin := mapping.ToUser(m.User)
|
||||
|
||||
response := tailcfg.RegisterResponse{
|
||||
MachineAuthorized: m.Authorized,
|
||||
User: tUser,
|
||||
Login: tLogin,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
return h.authenticateMachine(c, binder, machineKey, req)
|
||||
return h.authenticateMachine(c, machineKey, req)
|
||||
}
|
||||
|
||||
func (h *RegistrationHandlers) authenticateMachine(c echo.Context, binder bind.Binder, machineKey string, req *tailcfg.RegisterRequest) error {
|
||||
func (h *RegistrationHandlers) authenticateMachine(c echo.Context, machineKey string, req *tailcfg.RegisterRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
if req.Followup != "" {
|
||||
return h.followup(c, binder, req)
|
||||
return h.followup(c, req)
|
||||
}
|
||||
|
||||
if req.Auth.AuthKey == "" {
|
||||
key := util.RandStringBytes(8)
|
||||
authUrl := h.config.CreateUrl("/a/%s", key)
|
||||
authUrl := h.config.CreateUrl("/a/r/%s", key)
|
||||
|
||||
request := domain.RegistrationRequest{
|
||||
MachineKey: machineKey,
|
||||
@@ -124,56 +133,58 @@ func (h *RegistrationHandlers) authenticateMachine(c echo.Context, binder bind.B
|
||||
err := h.repository.SaveRegistrationRequest(ctx, &request)
|
||||
if err != nil {
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: false, Error: "something went wrong"}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
response := tailcfg.RegisterResponse{AuthURL: authUrl}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
} else {
|
||||
return h.authenticateMachineWithAuthKey(c, binder, machineKey, req)
|
||||
return h.authenticateMachineWithAuthKey(c, machineKey, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, binder bind.Binder, machineKey string, req *tailcfg.RegisterRequest) error {
|
||||
func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, machineKey string, req *tailcfg.RegisterRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
nodeKey := req.NodeKey.String()
|
||||
|
||||
authKey, err := h.repository.LoadAuthKey(ctx, req.Auth.AuthKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if authKey == nil {
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: false, Error: "invalid auth key"}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
tailnet := authKey.Tailnet
|
||||
user := authKey.User
|
||||
|
||||
if err := tailnet.ACLPolicy.CheckTagOwners(req.Hostinfo.RequestTags, &user); err != nil {
|
||||
if err := tailnet.ACLPolicy.Get().CheckTagOwners(req.Hostinfo.RequestTags, &user); err != nil {
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: false, Error: err.Error()}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
registeredTags := authKey.Tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
autoAllowIPs := tailnet.ACLPolicy.Get().FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, &user)
|
||||
|
||||
var m *domain.Machine
|
||||
|
||||
m, err = h.repository.GetMachineByKey(ctx, tailnet.ID, machineKey)
|
||||
m, err = h.repository.GetMachineByKeyAndUser(ctx, machineKey, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
if m == nil {
|
||||
registeredTags := authKey.Tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
m = &domain.Machine{
|
||||
@@ -182,15 +193,19 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
NameIdx: nameIdx,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: nodeKey,
|
||||
Ephemeral: authKey.Ephemeral,
|
||||
Ephemeral: authKey.Ephemeral || req.Ephemeral,
|
||||
RegisteredTags: registeredTags,
|
||||
Tags: domain.SanitizeTags(tags),
|
||||
AutoAllowIPs: autoAllowIPs,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(180 * 24 * time.Hour).UTC(),
|
||||
KeyExpiryDisabled: len(tags) != 0,
|
||||
Authorized: !tailnet.MachineAuthorizationEnabled || authKey.PreAuthorized,
|
||||
|
||||
User: user,
|
||||
Tailnet: tailnet,
|
||||
User: user,
|
||||
UserID: user.ID,
|
||||
Tailnet: tailnet,
|
||||
TailnetID: tailnet.ID,
|
||||
}
|
||||
|
||||
if !req.Expiry.IsZero() {
|
||||
@@ -199,28 +214,25 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
|
||||
ipv4, ipv6, err := addr.SelectIP(checkIP(ctx, h.repository.CountMachinesWithIPv4))
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
m.IPv4 = domain.IP{Addr: ipv4}
|
||||
m.IPv6 = domain.IP{Addr: ipv6}
|
||||
} else {
|
||||
registeredTags := authKey.Tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
if m.Name != sanitizeHostname {
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
if err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
m.Name = sanitizeHostname
|
||||
m.NameIdx = nameIdx
|
||||
}
|
||||
m.NodeKey = nodeKey
|
||||
m.Ephemeral = authKey.Ephemeral
|
||||
m.Ephemeral = authKey.Ephemeral || req.Ephemeral
|
||||
m.RegisteredTags = registeredTags
|
||||
m.Tags = domain.SanitizeTags(tags)
|
||||
m.AutoAllowIPs = autoAllowIPs
|
||||
m.UserID = user.ID
|
||||
m.User = user
|
||||
m.TailnetID = tailnet.ID
|
||||
@@ -229,14 +241,20 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
}
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: true}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
tUser, tLogin := mapping.ToUser(m.User)
|
||||
response := tailcfg.RegisterResponse{
|
||||
MachineAuthorized: true,
|
||||
User: tUser,
|
||||
Login: tLogin,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *RegistrationHandlers) followup(c echo.Context, binder bind.Binder, req *tailcfg.RegisterRequest) error {
|
||||
func (h *RegistrationHandlers) followup(c echo.Context, req *tailcfg.RegisterRequest) error {
|
||||
// Listen to connection close
|
||||
ctx := c.Request().Context()
|
||||
notify := ctx.Done()
|
||||
@@ -244,7 +262,7 @@ func (h *RegistrationHandlers) followup(c echo.Context, binder bind.Binder, req
|
||||
|
||||
defer func() { tick.Stop() }()
|
||||
|
||||
machineKey := binder.Peer().String()
|
||||
machineKey := h.machineKey.String()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -253,12 +271,32 @@ func (h *RegistrationHandlers) followup(c echo.Context, binder bind.Binder, req
|
||||
|
||||
if err != nil || m == nil {
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: false, Error: "something went wrong"}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
if m != nil && m.IsFinished() {
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: len(m.Error) != 0, Error: m.Error}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
if m != nil && m.Authenticated {
|
||||
user, err := h.repository.GetUser(ctx, m.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, l := mapping.ToUser(*user)
|
||||
|
||||
response := tailcfg.RegisterResponse{
|
||||
MachineAuthorized: len(m.Error) != 0,
|
||||
Error: m.Error,
|
||||
User: u,
|
||||
Login: l,
|
||||
}
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
if m != nil && len(m.Error) != 0 {
|
||||
response := tailcfg.RegisterResponse{
|
||||
MachineAuthorized: len(m.Error) != 0,
|
||||
Error: m.Error,
|
||||
}
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
case <-notify:
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewSSHActionHandlers(machineKey key.MachinePublic, config *config.Config, repository domain.Repository) *SSHActionHandlers {
|
||||
return &SSHActionHandlers{
|
||||
machineKey: machineKey,
|
||||
repository: repository,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type SSHActionHandlers struct {
|
||||
machineKey key.MachinePublic
|
||||
repository domain.Repository
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
type sshActionRequestData struct {
|
||||
SrcMachineID uint64 `param:"src_machine_id"`
|
||||
DstMachineID uint64 `param:"dst_machine_id"`
|
||||
CheckPeriod string `param:"check_period"`
|
||||
}
|
||||
|
||||
func (h *SSHActionHandlers) StartAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
data := new(sshActionRequestData)
|
||||
if err := c.Bind(data); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if data.CheckPeriod != "" && data.CheckPeriod != "always" {
|
||||
checkPeriod, err := time.ParseDuration(data.CheckPeriod)
|
||||
if err != nil {
|
||||
_ = logError(err)
|
||||
goto check
|
||||
}
|
||||
|
||||
machine, err := h.repository.GetMachine(ctx, data.SrcMachineID)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if machine.User.Account != nil && machine.User.LastAuthenticated != nil {
|
||||
sinceLastAuthentication := time.Since(*machine.User.LastAuthenticated)
|
||||
|
||||
if sinceLastAuthentication < checkPeriod {
|
||||
resp := &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check:
|
||||
key := util.RandStringBytes(8)
|
||||
request := &domain.SSHActionRequest{
|
||||
Key: key,
|
||||
SrcMachineID: data.SrcMachineID,
|
||||
DstMachineID: data.DstMachineID,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
authUrl := h.config.CreateUrl("/a/s/%s", key)
|
||||
|
||||
if err := h.repository.SaveSSHActionRequest(ctx, request); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
resp := &tailcfg.SSHAction{
|
||||
Message: fmt.Sprintf("# Tailscale SSH requires an additional check.\n# To authenticate, visit: %s\n", authUrl),
|
||||
HoldAndDelegate: fmt.Sprintf("https://unused/machine/ssh/action/check/%s", key),
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *SSHActionHandlers) CheckAuth(c echo.Context) error {
|
||||
// Listen to connection close
|
||||
ctx := c.Request().Context()
|
||||
notify := ctx.Done()
|
||||
|
||||
tick := time.NewTicker(2 * time.Second)
|
||||
|
||||
defer func() { tick.Stop() }()
|
||||
|
||||
key := c.Param("key")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
m, err := h.repository.GetSSHActionRequest(ctx, key)
|
||||
|
||||
if err != nil || m == nil {
|
||||
return c.JSON(http.StatusOK, &tailcfg.SSHAction{Reject: true})
|
||||
}
|
||||
|
||||
if m.Action == "accept" {
|
||||
action := &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
}
|
||||
_ = h.repository.DeleteSSHActionRequest(ctx, key)
|
||||
return c.JSON(http.StatusOK, action)
|
||||
}
|
||||
|
||||
if m.Action == "reject" {
|
||||
action := &tailcfg.SSHAction{Reject: true}
|
||||
_ = h.repository.DeleteSSHActionRequest(ctx, key)
|
||||
return c.JSON(http.StatusOK, action)
|
||||
}
|
||||
case <-notify:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/internal/version"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -14,3 +15,8 @@ func Version(c echo.Context) error {
|
||||
}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func logError(err error) error {
|
||||
zap.L().WithOptions(zap.AddCallerSkip(1)).Error("error processing request", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
+123
-50
@@ -7,16 +7,14 @@ import (
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
"time"
|
||||
)
|
||||
|
||||
const NetworkMagicDNSSuffix = "ionscale.net"
|
||||
|
||||
func CopyViaJson[F any, T any](f F, t T) error {
|
||||
raw, err := json.Marshal(f)
|
||||
if err != nil {
|
||||
@@ -30,51 +28,65 @@ func CopyViaJson[F any, T any](f F, t T) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ToDNSConfig(tailnet *domain.Tailnet, c *domain.DNSConfig) *tailcfg.DNSConfig {
|
||||
tailnetDomain := dnsname.SanitizeHostname(tailnet.Name)
|
||||
resolvers := []*dnstype.Resolver{}
|
||||
func ToDNSConfig(m *domain.Machine, tailnet *domain.Tailnet, c *domain.DNSConfig) *tailcfg.DNSConfig {
|
||||
certsEnabled := c.HttpsCertsEnabled && config.DNSProviderConfigured()
|
||||
|
||||
sanitizeTailnetName := domain.SanitizeTailnetName(tailnet.Name)
|
||||
tailnetDomain := fmt.Sprintf("%s.%s", sanitizeTailnetName, config.MagicDNSSuffix())
|
||||
|
||||
resolvers := make([]*dnstype.Resolver, 0)
|
||||
|
||||
for _, r := range c.Nameservers {
|
||||
resolver := &dnstype.Resolver{
|
||||
Addr: r,
|
||||
}
|
||||
resolvers = append(resolvers, resolver)
|
||||
resolvers = append(resolvers, &dnstype.Resolver{Addr: r})
|
||||
}
|
||||
|
||||
config := &tailcfg.DNSConfig{}
|
||||
dnsConfig := &tailcfg.DNSConfig{}
|
||||
|
||||
var routes = make(map[string][]*dnstype.Resolver)
|
||||
var domains []string
|
||||
var certDomains []string
|
||||
|
||||
if c.MagicDNS {
|
||||
domains = append(domains, fmt.Sprintf("%s.%s", tailnetDomain, NetworkMagicDNSSuffix))
|
||||
config.Proxied = true
|
||||
routes[tailnetDomain] = nil
|
||||
domains = append(domains, tailnetDomain)
|
||||
dnsConfig.Proxied = true
|
||||
|
||||
if certsEnabled {
|
||||
certDomains = append(certDomains, fmt.Sprintf("%s.%s", m.CompleteName(), tailnetDomain))
|
||||
}
|
||||
}
|
||||
|
||||
if c.OverrideLocalDNS {
|
||||
config.Resolvers = resolvers
|
||||
dnsConfig.Resolvers = resolvers
|
||||
} else {
|
||||
config.FallbackResolvers = resolvers
|
||||
dnsConfig.FallbackResolvers = resolvers
|
||||
}
|
||||
|
||||
if len(c.Routes) != 0 {
|
||||
routes := make(map[string][]*dnstype.Resolver)
|
||||
if len(c.Routes) != 0 || certsEnabled {
|
||||
for r, s := range c.Routes {
|
||||
routeResolver := []*dnstype.Resolver{}
|
||||
routeResolver := make([]*dnstype.Resolver, 0)
|
||||
for _, addr := range s {
|
||||
resolver := &dnstype.Resolver{Addr: addr}
|
||||
routeResolver = append(routeResolver, resolver)
|
||||
routeResolver = append(routeResolver, &dnstype.Resolver{Addr: addr})
|
||||
}
|
||||
routes[r] = routeResolver
|
||||
domains = append(domains, r)
|
||||
}
|
||||
config.Routes = routes
|
||||
|
||||
dnsConfig.Routes = routes
|
||||
}
|
||||
|
||||
config.Domains = domains
|
||||
dnsConfig.Domains = append(domains, c.SearchDomains...)
|
||||
dnsConfig.CertDomains = certDomains
|
||||
|
||||
return config
|
||||
dnsConfig.ExitNodeFilteredSet = []string{
|
||||
fmt.Sprintf(".%s", config.MagicDNSSuffix()),
|
||||
}
|
||||
|
||||
return dnsConfig
|
||||
}
|
||||
|
||||
func ToNode(m *domain.Machine) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
func ToNode(capVer tailcfg.CapabilityVersion, m *domain.Machine, tailnet *domain.Tailnet, taggedDevicesUser *domain.User, peer bool, connected bool, routeFilter func(m *domain.Machine) []netip.Prefix) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
role := tailnet.IAMPolicy.Get().GetRole(m.User)
|
||||
|
||||
nKey, err := util.ParseNodePublicKey(m.NodeKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -118,7 +130,13 @@ func ToNode(m *domain.Machine) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
allowedIPs = append(allowedIPs, ipv6)
|
||||
}
|
||||
|
||||
allowedIPs = append(allowedIPs, m.AllowIPs...)
|
||||
if connected {
|
||||
allowedIPs = append(allowedIPs, routeFilter(m)...)
|
||||
}
|
||||
|
||||
if m.IsAllowedExitNode() {
|
||||
allowedIPs = append(allowedIPs, netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"))
|
||||
}
|
||||
|
||||
var derp string
|
||||
if hostinfo.NetInfo != nil {
|
||||
@@ -127,23 +145,21 @@ func ToNode(m *domain.Machine) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
derp = "127.3.3.40:0"
|
||||
}
|
||||
|
||||
var name = m.Name
|
||||
if m.NameIdx != 0 {
|
||||
name = fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
||||
}
|
||||
var name = m.CompleteName()
|
||||
|
||||
sanitizedTailnetName := dnsname.SanitizeHostname(m.Tailnet.Name)
|
||||
sanitizedTailnetName := domain.SanitizeTailnetName(m.Tailnet.Name)
|
||||
|
||||
hostInfo := tailcfg.Hostinfo{
|
||||
OS: hostinfo.OS,
|
||||
Hostname: hostinfo.Hostname,
|
||||
Services: hostinfo.Services,
|
||||
OS: hostinfo.OS,
|
||||
Hostname: hostinfo.Hostname,
|
||||
Services: filterServices(hostinfo.Services),
|
||||
SSH_HostKeys: hostinfo.SSH_HostKeys,
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
ID: tailcfg.NodeID(m.ID),
|
||||
StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)),
|
||||
Name: fmt.Sprintf("%s.%s.%s.", name, sanitizedTailnetName, NetworkMagicDNSSuffix),
|
||||
Name: fmt.Sprintf("%s.%s.%s.", name, sanitizedTailnetName, config.MagicDNSSuffix()),
|
||||
Key: *nKey,
|
||||
Machine: *mKey,
|
||||
DiscoKey: discoKey,
|
||||
@@ -153,13 +169,54 @@ func ToNode(m *domain.Machine) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
DERP: derp,
|
||||
|
||||
Hostinfo: hostInfo.View(),
|
||||
Created: m.CreatedAt.UTC(),
|
||||
|
||||
Created: m.CreatedAt.UTC(),
|
||||
|
||||
MachineAuthorized: true,
|
||||
MachineAuthorized: m.Authorized,
|
||||
User: tailcfg.UserID(m.UserID),
|
||||
}
|
||||
|
||||
if !peer {
|
||||
var capabilities []tailcfg.NodeCapability
|
||||
capMap := make(tailcfg.NodeCapMap)
|
||||
|
||||
for _, c := range tailnet.ACLPolicy.Get().NodeCapabilities(m) {
|
||||
capabilities = append(capabilities, c)
|
||||
capMap[c] = []tailcfg.RawMessage{}
|
||||
}
|
||||
|
||||
if !m.HasTags() && role == domain.UserRoleAdmin {
|
||||
capabilities = append(capabilities, tailcfg.CapabilityAdmin)
|
||||
capMap[tailcfg.CapabilityAdmin] = []tailcfg.RawMessage{}
|
||||
}
|
||||
|
||||
if tailnet.FileSharingEnabled {
|
||||
capabilities = append(capabilities, tailcfg.CapabilityFileSharing)
|
||||
capMap[tailcfg.CapabilityFileSharing] = []tailcfg.RawMessage{}
|
||||
}
|
||||
|
||||
if tailnet.SSHEnabled {
|
||||
capabilities = append(capabilities, tailcfg.CapabilitySSH)
|
||||
capMap[tailcfg.CapabilitySSH] = []tailcfg.RawMessage{}
|
||||
}
|
||||
|
||||
if tailnet.DNSConfig.HttpsCertsEnabled {
|
||||
capabilities = append(capabilities, tailcfg.CapabilityHTTPS)
|
||||
capMap[tailcfg.CapabilityHTTPS] = []tailcfg.RawMessage{}
|
||||
}
|
||||
|
||||
// ionscale has no support for Funnel yet, so remove Funnel attribute if set via ACL policy
|
||||
{
|
||||
slices.DeleteFunc(capabilities, func(c tailcfg.NodeCapability) bool { return c == tailcfg.NodeAttrFunnel })
|
||||
delete(capMap, tailcfg.NodeAttrFunnel)
|
||||
}
|
||||
|
||||
if capVer >= 74 {
|
||||
n.CapMap = capMap
|
||||
} else {
|
||||
n.Capabilities = capabilities
|
||||
}
|
||||
}
|
||||
|
||||
if !m.ExpiresAt.IsZero() {
|
||||
e := m.ExpiresAt.UTC()
|
||||
n.KeyExpiry = e
|
||||
@@ -169,19 +226,17 @@ func ToNode(m *domain.Machine) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
n.KeyExpiry = time.Time{}
|
||||
}
|
||||
|
||||
if m.LastSeen != nil {
|
||||
l := m.LastSeen.UTC()
|
||||
online := m.LastSeen.After(time.Now().Add(-config.KeepAliveInterval))
|
||||
n.LastSeen = &l
|
||||
n.Online = &online
|
||||
n.Online = &connected
|
||||
if !connected && m.LastSeen != nil {
|
||||
n.LastSeen = m.LastSeen
|
||||
}
|
||||
|
||||
var user = ToUserProfile(m.User)
|
||||
|
||||
if m.HasTags() {
|
||||
n.User = tailcfg.UserID(m.ID)
|
||||
n.User = tailcfg.UserID(taggedDevicesUser.ID)
|
||||
user = tailcfg.UserProfile{
|
||||
ID: tailcfg.UserID(m.ID),
|
||||
ID: tailcfg.UserID(taggedDevicesUser.ID),
|
||||
LoginName: "tagged-devices",
|
||||
DisplayName: "Tagged Devices",
|
||||
}
|
||||
@@ -199,10 +254,28 @@ func ToUserProfile(u domain.User) tailcfg.UserProfile {
|
||||
return profile
|
||||
}
|
||||
|
||||
func ToUserProfiles(users domain.Users) []tailcfg.UserProfile {
|
||||
var profiles []tailcfg.UserProfile
|
||||
for _, u := range users {
|
||||
profiles = append(profiles, ToUserProfile(u))
|
||||
func ToUser(u domain.User) (tailcfg.User, tailcfg.Login) {
|
||||
user := tailcfg.User{
|
||||
ID: tailcfg.UserID(u.ID),
|
||||
LoginName: u.Name,
|
||||
DisplayName: u.Name,
|
||||
Logins: []tailcfg.LoginID{tailcfg.LoginID(u.ID)},
|
||||
}
|
||||
return profiles
|
||||
login := tailcfg.Login{
|
||||
ID: tailcfg.LoginID(u.ID),
|
||||
LoginName: u.Name,
|
||||
DisplayName: u.Name,
|
||||
}
|
||||
return user, login
|
||||
}
|
||||
|
||||
func filterServices(services []tailcfg.Service) []tailcfg.Service {
|
||||
result := []tailcfg.Service{}
|
||||
for _, s := range services {
|
||||
if s.Proto == tailcfg.TCP || s.Proto == tailcfg.UDP {
|
||||
continue
|
||||
}
|
||||
result = append(result, s)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package mapping
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/internal/core"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MapResponse is a custom tailcfg.MapResponse
|
||||
// for marshalling non-nil zero-length slices (meaning explicitly now empty)
|
||||
// see tailcfg.MapResponse documentation
|
||||
type MapResponse struct {
|
||||
tailcfg.MapResponse
|
||||
PacketFilter []tailcfg.FilterRule
|
||||
}
|
||||
|
||||
func NewPollNetMapper(req *tailcfg.MapRequest, machineID uint64, repository domain.Repository, sessionManager core.PollMapSessionManager) *PollNetMapper {
|
||||
return &PollNetMapper{
|
||||
req: req,
|
||||
machineID: machineID,
|
||||
prevSyncedPeerIDs: make(map[uint64]bool),
|
||||
prevDerpMapChecksum: "",
|
||||
repository: repository,
|
||||
sessionManager: sessionManager,
|
||||
}
|
||||
}
|
||||
|
||||
type PollNetMapper struct {
|
||||
sync.Mutex
|
||||
req *tailcfg.MapRequest
|
||||
machineID uint64
|
||||
|
||||
prevSyncedPeerIDs map[uint64]bool
|
||||
prevDerpMapChecksum string
|
||||
|
||||
repository domain.Repository
|
||||
sessionManager core.PollMapSessionManager
|
||||
}
|
||||
|
||||
func (h *PollNetMapper) CreateMapResponse(ctx context.Context, delta bool) (*MapResponse, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
m, err := h.repository.GetMachine(ctx, h.machineID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostinfo := tailcfg.Hostinfo(m.HostInfo)
|
||||
tailnet := m.Tailnet
|
||||
policies := tailnet.ACLPolicy.Get()
|
||||
dnsConfig := tailnet.DNSConfig
|
||||
|
||||
serviceUser, _, err := h.repository.GetOrCreateServiceUser(ctx, &tailnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derpMap, err := m.Tailnet.GetDERPMap(ctx, domain.GetDefaultDERPMap())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prc := &primaryRoutesCollector{flagged: map[netip.Prefix]bool{}}
|
||||
|
||||
node, user, err := ToNode(h.req.Version, m, &tailnet, serviceUser, false, true, prc.filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users = []tailcfg.UserProfile{*user}
|
||||
var changedPeers []*tailcfg.Node
|
||||
var removedPeers []tailcfg.NodeID
|
||||
var filterRules = make([]tailcfg.FilterRule, 0)
|
||||
var sshPolicy *tailcfg.SSHPolicy
|
||||
syncedPeerIDs := map[uint64]bool{}
|
||||
|
||||
if !h.req.OmitPeers {
|
||||
candidatePeers, err := h.repository.ListMachinePeers(ctx, m.TailnetID, m.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncedUserIDs := map[tailcfg.UserID]bool{user.ID: true}
|
||||
|
||||
for _, peer := range candidatePeers {
|
||||
if policies.IsValidPeer(m, &peer) || policies.IsValidPeer(&peer, m) {
|
||||
isConnected := h.sessionManager.HasSession(peer.TailnetID, peer.ID)
|
||||
|
||||
n, u, err := ToNode(h.req.Version, &peer, &tailnet, serviceUser, true, isConnected, prc.filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changedPeers = append(changedPeers, n)
|
||||
syncedPeerIDs[peer.ID] = true
|
||||
delete(h.prevSyncedPeerIDs, peer.ID)
|
||||
|
||||
if _, ok := syncedUserIDs[u.ID]; !ok {
|
||||
users = append(users, *u)
|
||||
syncedUserIDs[u.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for p, _ := range h.prevSyncedPeerIDs {
|
||||
removedPeers = append(removedPeers, tailcfg.NodeID(p))
|
||||
}
|
||||
|
||||
filterRules = policies.BuildFilterRules(candidatePeers, m)
|
||||
|
||||
if tailnet.SSHEnabled && hostinfo.TailscaleSSHEnabled() {
|
||||
sshPolicy = policies.BuildSSHPolicy(candidatePeers, m)
|
||||
}
|
||||
}
|
||||
|
||||
controlTime := time.Now().UTC()
|
||||
var mapResponse tailcfg.MapResponse
|
||||
|
||||
if !delta {
|
||||
mapResponse = tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
DNSConfig: ToDNSConfig(m, &m.Tailnet, &dnsConfig),
|
||||
PacketFilter: filterRules,
|
||||
SSHPolicy: sshPolicy,
|
||||
DERPMap: &derpMap.DERPMap,
|
||||
Domain: domain.SanitizeTailnetName(m.Tailnet.Name),
|
||||
Peers: changedPeers,
|
||||
UserProfiles: users,
|
||||
ControlTime: &controlTime,
|
||||
CollectServices: optBool(tailnet.ServiceCollectionEnabled),
|
||||
Debug: &tailcfg.Debug{
|
||||
DisableLogTail: true,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
mapResponse = tailcfg.MapResponse{
|
||||
Node: node,
|
||||
DNSConfig: ToDNSConfig(m, &m.Tailnet, &dnsConfig),
|
||||
PacketFilter: filterRules,
|
||||
SSHPolicy: sshPolicy,
|
||||
Domain: domain.SanitizeTailnetName(m.Tailnet.Name),
|
||||
PeersChanged: changedPeers,
|
||||
PeersRemoved: removedPeers,
|
||||
UserProfiles: users,
|
||||
ControlTime: &controlTime,
|
||||
CollectServices: optBool(tailnet.ServiceCollectionEnabled),
|
||||
}
|
||||
|
||||
if h.prevDerpMapChecksum != derpMap.Checksum {
|
||||
mapResponse.DERPMap = &derpMap.DERPMap
|
||||
}
|
||||
}
|
||||
|
||||
if h.req.OmitPeers {
|
||||
mapResponse.PeersChanged = nil
|
||||
mapResponse.PeersRemoved = nil
|
||||
mapResponse.Peers = nil
|
||||
}
|
||||
|
||||
h.prevSyncedPeerIDs = syncedPeerIDs
|
||||
h.prevDerpMapChecksum = derpMap.Checksum
|
||||
|
||||
return &MapResponse{MapResponse: mapResponse, PacketFilter: filterRules}, nil
|
||||
}
|
||||
|
||||
type primaryRoutesCollector struct {
|
||||
flagged map[netip.Prefix]bool
|
||||
}
|
||||
|
||||
func (p *primaryRoutesCollector) filter(m *domain.Machine) []netip.Prefix {
|
||||
var result []netip.Prefix
|
||||
for _, r := range m.AllowIPs {
|
||||
if _, ok := p.flagged[r]; r.Bits() != 0 && !ok {
|
||||
result = append(result, r)
|
||||
p.flagged[r] = true
|
||||
}
|
||||
}
|
||||
for _, r := range m.AutoAllowIPs {
|
||||
if _, ok := p.flagged[r]; r.Bits() != 0 && !ok {
|
||||
result = append(result, r)
|
||||
p.flagged[r] = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func optBool(v bool) opt.Bool {
|
||||
b := opt.Bool("")
|
||||
b.Set(v)
|
||||
return b
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user