Compare commits

..

9 Commits

Author SHA1 Message Date
Owen 83df1cc57c sync the exit node connection 2026-07-31 11:30:32 -04:00
Owen d3324f4f28 Update site resources to handle new inference 2026-07-31 11:06:30 -04:00
Owen 97a1bc3697 send if we need to connect to the gerbil or not 2026-07-31 10:54:18 -04:00
Owen 45623ce36b Add inference resource type 2026-07-30 17:34:13 -04:00
Owen d68f123e13 Handle the ping exit node request backward mode for olm and install handler 2026-07-30 13:57:45 -04:00
Owen 9469fea88f Fix postgres schema exit node subnet 2026-07-30 13:57:07 -04:00
Owen 915e8d5b1f Rename subnet for clarity, pick subnet on client 2026-07-30 12:13:09 -04:00
Owen 719baa201e Add exit node selection to the clients 2026-07-30 11:57:48 -04:00
Owen b0e274f5a9 Fix 100% showing on resources with no hc anymore 2026-07-30 11:14:34 -04:00
45 changed files with 760 additions and 538 deletions
+9 -9
View File
@@ -62,7 +62,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Monitor storage space
run: |
@@ -77,7 +77,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Monitor storage space
run: |
@@ -149,7 +149,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -201,10 +201,10 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Log in to Docker Hub
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Extract tag name
id: get-tag
@@ -264,7 +264,7 @@ jobs:
shell: bash
- name: Install Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.25
@@ -407,7 +407,7 @@ jobs:
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+2 -2
View File
@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
days-before-stale: 14
days-before-close: 14
+4 -4
View File
@@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build Docker image sqlite
run: make dev-build-sqlite
@@ -71,7 +71,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build Docker image pg
run: make dev-build-pg
+2 -2
View File
@@ -1,5 +1,5 @@
# FROM node:24-slim AS base
FROM public.ecr.aws/docker/library/node:26-slim AS base
FROM public.ecr.aws/docker/library/node:24-slim AS base
WORKDIR /app
@@ -33,7 +33,7 @@ FROM base AS builder
RUN npm ci --omit=dev
# FROM node:24-slim AS runner
FROM public.ecr.aws/docker/library/node:26-slim AS runner
FROM public.ecr.aws/docker/library/node:24-slim AS runner
WORKDIR /app
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:26-alpine
FROM node:24-alpine
WORKDIR /app
+2 -2
View File
@@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/term v0.45.0
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,6 +33,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
+4 -4
View File
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+60 -304
View File
@@ -51,7 +51,7 @@
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"arctic": "3.7.0",
"axios": "1.18.0",
"axios": "1.16.1",
"better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1",
@@ -70,12 +70,12 @@
"input-otp": "1.4.2",
"ioredis": "5.11.0",
"jmespath": "0.16.0",
"js-yaml": "4.3.0",
"js-yaml": "4.2.0",
"jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0",
"maxmind": "5.0.6",
"moment": "2.30.1",
"next": "16.2.11",
"next": "16.2.6",
"next-intl": "4.13.0",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
@@ -2574,9 +2574,9 @@
}
},
"node_modules/@next/env": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz",
"integrity": "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2590,9 +2590,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz",
"integrity": "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
"cpu": [
"arm64"
],
@@ -2606,9 +2606,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz",
"integrity": "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
"cpu": [
"x64"
],
@@ -2622,15 +2622,12 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz",
"integrity": "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2641,15 +2638,12 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz",
"integrity": "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2660,15 +2654,12 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz",
"integrity": "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2679,15 +2670,12 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz",
"integrity": "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2698,9 +2686,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz",
"integrity": "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
"cpu": [
"arm64"
],
@@ -2714,9 +2702,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.11.tgz",
"integrity": "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
"cpu": [
"x64"
],
@@ -6472,225 +6460,6 @@
"next": "16.2.6"
}
},
"node_modules/@react-email/ui/node_modules/@next/env": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
"dev": true,
"license": "MIT"
},
"node_modules/@react-email/ui/node_modules/@next/swc-darwin-arm64": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-darwin-x64": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@react-email/ui/node_modules/next": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/env": "16.2.6",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.6",
"@next/swc-darwin-x64": "16.2.6",
"@next/swc-linux-arm64-gnu": "16.2.6",
"@next/swc-linux-arm64-musl": "16.2.6",
"@next/swc-linux-x64-gnu": "16.2.6",
"@next/swc-linux-x64-musl": "16.2.6",
"@next/swc-win32-arm64-msvc": "16.2.6",
"@next/swc-win32-x64-msvc": "16.2.6",
"sharp": "^0.34.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.51.1",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/@react-types/shared": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.34.0.tgz",
@@ -9309,9 +9078,9 @@
}
},
"node_modules/axios": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
"integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
@@ -9445,20 +9214,20 @@
}
},
"node_modules/body-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^2.0.0",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"http-errors": "^2.0.1",
"iconv-lite": "^0.7.2",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.15.2",
"raw-body": "^3.0.2",
"type-is": "^2.1.0"
"qs": "^6.14.1",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
@@ -9468,19 +9237,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@@ -12169,9 +11925,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true,
"funding": [
{
@@ -13633,9 +13389,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"funding": [
{
"type": "github",
@@ -14593,12 +14349,12 @@
}
},
"node_modules/next": {
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.11.tgz",
"integrity": "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==",
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
"license": "MIT",
"dependencies": {
"@next/env": "16.2.11",
"@next/env": "16.2.6",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -14612,14 +14368,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.11",
"@next/swc-darwin-x64": "16.2.11",
"@next/swc-linux-arm64-gnu": "16.2.11",
"@next/swc-linux-arm64-musl": "16.2.11",
"@next/swc-linux-x64-gnu": "16.2.11",
"@next/swc-linux-x64-musl": "16.2.11",
"@next/swc-win32-arm64-msvc": "16.2.11",
"@next/swc-win32-x64-msvc": "16.2.11",
"@next/swc-darwin-arm64": "16.2.6",
"@next/swc-darwin-x64": "16.2.6",
"@next/swc-linux-arm64-gnu": "16.2.6",
"@next/swc-linux-arm64-musl": "16.2.6",
"@next/swc-linux-x64-gnu": "16.2.6",
"@next/swc-linux-x64-musl": "16.2.6",
"@next/swc-win32-arm64-msvc": "16.2.6",
"@next/swc-win32-x64-msvc": "16.2.6",
"sharp": "^0.34.5"
},
"peerDependencies": {
+3 -3
View File
@@ -74,7 +74,7 @@
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"arctic": "3.7.0",
"axios": "1.18.0",
"axios": "1.16.1",
"better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1",
@@ -93,12 +93,12 @@
"input-otp": "1.4.2",
"ioredis": "5.11.0",
"jmespath": "0.16.0",
"js-yaml": "4.3.0",
"js-yaml": "4.2.0",
"jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0",
"maxmind": "5.0.6",
"moment": "2.30.1",
"next": "16.2.11",
"next": "16.2.6",
"next-intl": "4.13.0",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
+10 -3
View File
@@ -99,6 +99,7 @@ export const sites = pgTable(
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet"),
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node
megabytesIn: real("bytesIn").default(0),
megabytesOut: real("bytesOut").default(0),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
@@ -194,7 +195,10 @@ export const resources = pgTable(
postAuthPath: text("postAuthPath"),
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: boolean("wildcard").notNull().default(false),
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
mode: text("mode")
.default("http")
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference">()
.notNull(),
pamMode: varchar("pamMode", { length: 32 })
.$type<"passthrough" | "push">()
.default("passthrough"),
@@ -424,11 +428,14 @@ export const siteResources = pgTable(
onDelete: "restrict"
}
),
requiresExitNodeConnection: boolean("requiresExitNodeConnection")
.notNull()
.default(false),
niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(),
ssl: boolean("ssl").notNull().default(false),
mode: varchar("mode")
.$type<"host" | "cidr" | "http" | "ssh">()
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
.notNull(), // "host" | "cidr" | "http"
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
@@ -1181,7 +1188,7 @@ export const clients = pgTable(
olmId: text("olmId"), // to lock it to a specific olm optionally
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(),
exitNodeSubnet: varchar("exitNodeSubnet").notNull(),
megabytesIn: real("bytesIn"),
megabytesOut: real("bytesOut"),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
+14 -3
View File
@@ -107,7 +107,7 @@ export const sites = sqliteTable("sites", {
}),
name: text("name").notNull(),
pubKey: text("pubKey"),
subnet: text("subnet"),
exitNodeSubnet: text("exitNodeSubnet"),
megabytesIn: integer("bytesIn").default(0),
megabytesOut: integer("bytesOut").default(0),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
@@ -203,7 +203,10 @@ export const resources = sqliteTable("resources", {
postAuthPath: text("postAuthPath"),
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
mode: text("mode")
.default("http")
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference">()
.notNull(), // rdp, ssh, http, vnc, inference
pamMode: text("pamMode")
.$type<"passthrough" | "push">()
.default("passthrough"),
@@ -422,10 +425,17 @@ export const siteResources = sqliteTable("siteResources", {
() => networks.networkId,
{ onDelete: "restrict" }
),
requiresExitNodeConnection: integer("requiresExitNodeConnection", {
mode: "boolean"
})
.notNull()
.default(false),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
mode: text("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
mode: text("mode")
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
.notNull(), // "host" | "cidr" | "http"
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
@@ -599,6 +609,7 @@ export const clients = sqliteTable("clients", {
pubKey: text("pubKey"),
olmId: text("olmId"), // to lock it to a specific olm optionally
subnet: text("subnet").notNull(),
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
-14
View File
@@ -339,19 +339,6 @@ export async function calculateUserClientsForOrgs(
continue;
}
// Get exit nodes for this org
const exitNodesList = await getExitNodes(orgId);
if (exitNodesList.length === 0) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
);
continue;
}
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
// Get next available subnet
const { value: newSubnet, release: releaseSubnetLock } =
await getNextAvailableClientSubnet(orgId, trx);
@@ -370,7 +357,6 @@ export async function calculateUserClientsForOrgs(
const newClientData: InferInsertModel<typeof clients> = {
userId,
orgId: userOrg.orgId,
exitNodeId: randomExitNode.exitNodeId,
name: olm.name || "User Client",
subnet: updatedSubnet,
olmId: olm.olmId,
@@ -0,0 +1,40 @@
import { db, sites, clients } from "@server/db";
import { and, eq, count } from "drizzle-orm";
// (MAX_CONNECTIONS - current_connections) / MAX_CONNECTIONS)
// higher = more desirable
// like saying, this node has x% of its capacity left
export async function calculateExitNodeWeight(
exitNodeId: number,
maxConnections: number | null | undefined
): Promise<number | null> {
if (maxConnections === null || maxConnections === undefined) {
return 1;
}
const [[siteConnections], [clientConnections]] = await Promise.all([
db
.select({ count: count() })
.from(sites)
.where(
and(eq(sites.exitNodeId, exitNodeId), eq(sites.online, true))
),
db
.select({ count: count() })
.from(clients)
.where(
and(
eq(clients.exitNodeId, exitNodeId),
eq(clients.online, true)
)
)
]);
const currentConnections = siteConnections.count + clientConnections.count;
if (currentConnections >= maxConnections) {
return null;
}
return (maxConnections - currentConnections) / maxConnections;
}
+10 -1
View File
@@ -1,6 +1,5 @@
import { db, exitNodes, Transaction } from "@server/db";
import logger from "@server/logger";
import { ExitNodePingResult } from "@server/routers/newt";
import { eq } from "drizzle-orm";
export async function verifyExitNodeOrgAccess(
@@ -52,6 +51,16 @@ export async function listExitNodes(
return allExitNodes;
}
export type ExitNodePingResult = {
exitNodeId: number;
latencyMs: number;
weight: number;
error?: string;
exitNodeName: string;
endpoint: string;
wasPreviouslyConnected: boolean;
};
export function selectBestExitNode(
pingResults: ExitNodePingResult[]
): ExitNodePingResult | null {
@@ -0,0 +1,41 @@
import { db, ExitNode, Transaction, sites, clients } from "@server/db";
import { eq } from "drizzle-orm";
import config from "@server/lib/config";
import { findNextAvailableCidr } from "@server/lib/ip";
import { lockManager } from "#dynamic/lib/lock";
export async function getUniqueSubnetForExitNode(
exitNode: ExitNode,
trx: Transaction | typeof db = db
): Promise<string | null> {
const lockKey = `subnet-allocation:${exitNode.exitNodeId}`;
return await lockManager.withLock(
lockKey,
async () => {
const [sitesQuery, clientsQuery] = await Promise.all([
trx
.select({ subnet: sites.exitNodeSubnet })
.from(sites)
.where(eq(sites.exitNodeId, exitNode.exitNodeId)),
trx
.select({ subnet: clients.exitNodeSubnet })
.from(clients)
.where(eq(clients.exitNodeId, exitNode.exitNodeId))
]);
const blockSize = config.getRawConfig().gerbil.site_block_size;
const subnets = [...sitesQuery, ...clientsQuery]
.map((row) => row.subnet)
.filter(
(subnet): subnet is string =>
!!subnet &&
/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
);
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
return findNextAvailableCidr(subnets, blockSize, exitNode.address);
},
5000 // 5 second lock TTL - subnet allocation should be quick
);
}
+2
View File
@@ -2,3 +2,5 @@ export * from "./exitNodes";
export * from "./exitNodeComms";
export * from "./subnet";
export * from "./getCurrentExitNodeId";
export * from "./calculateExitNodeWeight";
export * from "./getUniqueSubnetForExitNode";
+220 -5
View File
@@ -19,7 +19,7 @@ import {
userOrgRoles,
userSiteResources
} from "@server/db";
import { and, count, eq, inArray, ne } from "drizzle-orm";
import { and, count, eq, inArray, isNotNull, ne } from "drizzle-orm";
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
import {
@@ -27,6 +27,9 @@ import {
deletePeersBatch as olmDeletePeersBatch
} from "@server/routers/olm/peers";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { sendToClientsBatch } from "#dynamic/routers/ws";
import { canCompress } from "@server/lib/clientVersionChecks";
import config from "@server/lib/config";
import logger from "@server/logger";
import {
generateAliasConfig,
@@ -187,7 +190,12 @@ export async function getClientSiteResourceAccess(
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}]`
);
if (sitesList.length === 0) {
if (sitesList.length === 0 && siteResource.networkId !== null) {
// A site resource with a networkId is expected to have at least one
// site attached via siteNetworks. Resources with no networkId (e.g.
// inference-mode resources, which connect clients directly to the
// exit node instead of any site) are expected to have no sites, so
// don't warn for those.
logger.warn(
`No sites found for siteResource ${siteResource.siteResourceId} with networkId ${siteResource.networkId}`
);
@@ -687,6 +695,22 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
clientSiteResourcesToRemove,
trx
);
// If this resource requires clients to be connected to the exit node
// (e.g. an inference resource), re-sync the connect/disconnect state for
// every client whose access to it may have changed - both those who
// currently have access and those who just lost it.
if (siteResource.requiresExitNodeConnection) {
await syncClientExitNodeConnections(
Array.from(
new Set([
...mergedAllClientIds,
...existingClientSiteResourceIds
])
),
trx
);
}
}
async function handleMessagesForSiteClients(
@@ -966,7 +990,7 @@ export async function updateClientSiteDestinations(
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
for (const site of sitesData) {
if (!site.sites.subnet) {
if (!site.sites.exitNodeSubnet) {
logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
continue;
}
@@ -1002,7 +1026,7 @@ export async function updateClientSiteDestinations(
sourcePort: parsedEndpoint.port,
destinations: [
{
destinationIP: site.sites.subnet.split("/")[0],
destinationIP: site.sites.exitNodeSubnet.split("/")[0],
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}
]
@@ -1010,7 +1034,7 @@ export async function updateClientSiteDestinations(
} else {
// add to the existing destinations
destinations.destinations.push({
destinationIP: site.sites.subnet.split("/")[0],
destinationIP: site.sites.exitNodeSubnet.split("/")[0],
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
});
}
@@ -1052,6 +1076,179 @@ export async function updateClientSiteDestinations(
}
}
// Determines, for each of the given clients, whether they currently have
// access to any enabled site resource with requiresExitNodeConnection set
// (e.g. an inference-mode resource) and tells the client's olm to connect to
// or disconnect from its assigned exit node accordingly. Site resources with
// requiresExitNodeConnection don't belong to any site/network, so this can't
// be derived from the per-site peer logic above - it has to be recomputed
// from the client's full current resource access every time that access
// changes.
async function syncClientExitNodeConnections(
clientIds: number[],
trx: Transaction | typeof db = db
): Promise<void> {
const uniqueClientIds = Array.from(new Set(clientIds));
if (uniqueClientIds.length === 0) {
return;
}
// Only clients with an exit node assigned can be told to connect/disconnect.
const clientsData = await trx
.select({
clientId: clients.clientId,
exitNodeId: clients.exitNodeId,
exitNodeSubnet: clients.exitNodeSubnet
})
.from(clients)
.where(
and(
inArray(clients.clientId, uniqueClientIds),
isNotNull(clients.exitNodeId)
)
);
if (clientsData.length === 0) {
return;
}
const clientIdsWithExitNode = clientsData.map((c) => c.clientId);
const requiresExitNodeRows = await trx
.select({
clientId: clientSiteResourcesAssociationsCache.clientId
})
.from(clientSiteResourcesAssociationsCache)
.innerJoin(
siteResources,
eq(
clientSiteResourcesAssociationsCache.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
inArray(
clientSiteResourcesAssociationsCache.clientId,
clientIdsWithExitNode
),
eq(siteResources.enabled, true),
eq(siteResources.requiresExitNodeConnection, true)
)
);
const needsConnectSet = new Set(requiresExitNodeRows.map((r) => r.clientId));
const exitNodeIds = Array.from(
new Set(
clientsData
.map((c) => c.exitNodeId)
.filter((id): id is number => id !== null)
)
);
const exitNodeRows =
exitNodeIds.length > 0
? await trx
.select()
.from(exitNodes)
.where(inArray(exitNodes.exitNodeId, exitNodeIds))
: [];
const exitNodeById = new Map(exitNodeRows.map((n) => [n.exitNodeId, n]));
const olmRows = await trx
.select({
clientId: olms.clientId,
olmId: olms.olmId,
version: olms.version
})
.from(olms)
.where(inArray(olms.clientId, clientIdsWithExitNode));
const olmByClientId = new Map(
olmRows
.filter((r) => r.clientId !== null)
.map((r) => [r.clientId as number, r])
);
const relayPort = config.getRawConfig().gerbil.clients_start_port;
const connectPayloads: {
clientId: string;
message: { type: string; data: any };
options: { compress: boolean };
}[] = [];
const disconnectPayloads: {
clientId: string;
message: { type: string; data: any };
options: { compress: boolean };
}[] = [];
for (const client of clientsData) {
const olm = olmByClientId.get(client.clientId);
if (!olm) {
// No olm registered for this client yet/anymore, nothing to send.
continue;
}
const needsConnect = needsConnectSet.has(client.clientId);
if (needsConnect) {
const exitNode = client.exitNodeId
? exitNodeById.get(client.exitNodeId)
: undefined;
if (!exitNode || !client.exitNodeSubnet) {
logger.warn(
`rebuildClientAssociations: [syncClientExitNodeConnections] client ${client.clientId} needs an exit node connection but has no exit node or subnet assigned`
);
continue;
}
connectPayloads.push({
clientId: olm.olmId,
message: {
type: "olm/wg/exitnode/connect",
data: {
connect: true,
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
relayPort,
publicKey: exitNode.publicKey,
serverIP: exitNode.address.split("/")[0],
tunnelIP: client.exitNodeSubnet.split("/")[0]
}
},
options: { compress: canCompress(olm.version, "olm") }
});
} else {
disconnectPayloads.push({
clientId: olm.olmId,
message: {
type: "olm/wg/exitnode/disconnect",
data: {}
},
options: { compress: canCompress(olm.version, "olm") }
});
}
}
if (connectPayloads.length > 0) {
await sendToClientsBatch(connectPayloads).catch((error) => {
logger.error(
`rebuildClientAssociations: Error sending exit node connect messages:`,
error
);
});
}
if (disconnectPayloads.length > 0) {
await sendToClientsBatch(disconnectPayloads).catch((error) => {
logger.error(
`rebuildClientAssociations: Error sending exit node disconnect messages:`,
error
);
});
}
}
async function handleSubnetProxyTargetUpdates(
siteResource: SiteResource,
sitesList: Site[],
@@ -1709,6 +1906,20 @@ export async function handleMessagingForUpdatedSiteResource(
);
}
// If this resource requires (or required) clients to be connected to the
// exit node (e.g. an inference resource), re-sync connect/disconnect
// state for every client currently associated with it - covers toggling
// requiresExitNodeConnection on update as well as enabling/disabling it.
if (
updatedSiteResource.requiresExitNodeConnection ||
existingSiteResource?.requiresExitNodeConnection
) {
await syncClientExitNodeConnections(
mergedAllClients.map((c) => c.clientId),
trx
);
}
logger.debug(
`handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
);
@@ -1990,6 +2201,10 @@ async function rebuildClientAssociationsFromClientImpl(
resourcesToRemove,
trx
);
// Re-sync exit node connect/disconnect state based on this client's
// current full set of resource access (e.g. inference resources).
await syncClientExitNodeConnections([client.clientId], trx);
}
async function handleMessagesForClientSites(
+1 -1
View File
@@ -87,7 +87,7 @@ export async function getTraefikConfig(
siteId: sites.siteId,
siteType: sites.type,
siteOnline: sites.online,
subnet: sites.subnet,
subnet: sites.exitNodeSubnet,
exitNodeId: sites.exitNodeId,
// Domain cert resolver fields
domainCertResolver: domains.certResolver,
+10 -1
View File
@@ -25,7 +25,6 @@ import {
Transaction
} from "@server/db";
import logger from "@server/logger";
import { ExitNodePingResult } from "@server/routers/newt";
import { eq, and, or, ne, isNull, inArray } from "drizzle-orm";
import axios from "axios";
import config from "../config";
@@ -330,6 +329,16 @@ export async function listExitNodes(
return exitNodesList;
}
export type ExitNodePingResult = {
exitNodeId: number;
latencyMs: number;
weight: number;
error?: string;
exitNodeName: string;
endpoint: string;
wasPreviouslyConnected: boolean;
};
/**
* Selects the most suitable exit node from a list of ping results.
*
@@ -134,7 +134,7 @@ export async function getTraefikConfig(
siteId: sites.siteId,
siteType: sites.type,
siteOnline: sites.online,
subnet: sites.subnet,
subnet: sites.exitNodeSubnet,
exitNodeId: sites.exitNodeId,
// Namespace
domainNamespaceId: domainNamespaces.domainNamespaceId,
@@ -178,7 +178,7 @@ export async function reGenerateSiteSecret(
);
}
if (site.exitNodeId && site.subnet) {
if (site.exitNodeId && site.exitNodeSubnet) {
await deletePeer(site.exitNodeId, site.pubKey!); // the old pubkey
await addPeer(site.exitNodeId, {
publicKey: pubKey,
-15
View File
@@ -255,20 +255,6 @@ export async function createClient(
let newClient: Client | null = null;
await db.transaction(async (trx) => {
// TODO: more intelligent way to pick the exit node
const exitNodesList = await listExitNodes(orgId);
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
if (!randomExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`
)
);
}
const [adminRole] = await trx
.select()
.from(roles)
@@ -287,7 +273,6 @@ export async function createClient(
.insert(clients)
.values({
niceId,
exitNodeId: randomExitNode.exitNodeId,
orgId,
name,
subnet: updatedSubnet,
@@ -222,11 +222,6 @@ export async function createUserClient(
let newClient: Client | null = null;
await db.transaction(async (trx) => {
// TODO: more intelligent way to pick the exit node
const exitNodesList = await listExitNodes(orgId);
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
const [adminRole] = await trx
.select()
.from(roles)
@@ -244,7 +239,6 @@ export async function createUserClient(
[newClient] = await trx
.insert(clients)
.values({
exitNodeId: randomExitNode.exitNodeId,
orgId,
niceId,
name,
+4 -4
View File
@@ -100,7 +100,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
// Filter to sites with the required fields up front so the rest of the
// function can safely treat endpoint/subnet/listenPort as defined.
const validSites = sitesRes.filter(
(s) => s.endpoint && s.subnet && s.listenPort
(s) => s.endpoint && s.exitNodeSubnet && s.listenPort
);
if (validSites.length === 0) {
@@ -136,7 +136,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
if (
peer.orgId == null ||
!peer.endpoint ||
!peer.subnet ||
!peer.exitNodeSubnet ||
!peer.listenPort
) {
continue;
@@ -183,7 +183,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
// Process each site using the pre-fetched data.
for (const site of validSites) {
const siteDestination: PeerDestination = {
destinationIP: site.subnet!.split("/")[0],
destinationIP: site.exitNodeSubnet!.split("/")[0],
destinationPort: site.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated
};
@@ -207,7 +207,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
continue;
}
addDestination(site.endpoint!, {
destinationIP: peer.subnet!.split("/")[0],
destinationIP: peer.exitNodeSubnet!.split("/")[0],
destinationPort: peer.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated
});
}
+2 -2
View File
@@ -89,7 +89,7 @@ export async function generateGerbilConfig(exitNode: ExitNode) {
and(
eq(sites.exitNodeId, exitNode.exitNodeId),
isNotNull(sites.pubKey),
isNotNull(sites.subnet)
isNotNull(sites.exitNodeSubnet)
)
);
@@ -103,7 +103,7 @@ export async function generateGerbilConfig(exitNode: ExitNode) {
} else if (site.type === "newt") {
return {
publicKey: site.pubKey,
allowedIps: [site.subnet!]
allowedIps: [site.exitNodeSubnet!]
};
}
return {
+1 -1
View File
@@ -186,7 +186,7 @@ export async function updateAndGenerateEndpointDestinations(
.select({
siteId: sites.siteId,
newtId: newts.newtId,
subnet: sites.subnet,
subnet: sites.exitNodeSubnet,
listenPort: sites.listenPort,
publicKey: sites.publicKey,
endpoint: clientSitesAssociationsCache.endpoint,
@@ -2,14 +2,17 @@ import { db, sites } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt } from "@server/db";
import logger from "@server/logger";
import { ne, eq, or, and, count } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { calculateExitNodeWeight } from "@server/lib/exitNodes";
export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
export const handleNewtExitNodesRequestMessage: MessageHandler = async (
context
) => {
const { message, client, sendToClient } = context;
const newt = client as Newt;
logger.info("Handling ping request newt message!");
logger.info("Handling exit nodes request newt message!");
if (!newt) {
logger.warn("Newt not found");
@@ -54,32 +57,13 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
const exitNodesPayload = await Promise.all(
exitNodesList.map(async (node) => {
// (MAX_CONNECTIONS - current_connections) / MAX_CONNECTIONS)
// higher = more desirable
// like saying, this node has x% of its capacity left
const weight = await calculateExitNodeWeight(
node.exitNodeId,
node.maxConnections
);
let weight = 1;
const maxConnections = node.maxConnections;
if (maxConnections !== null && maxConnections !== undefined) {
const [currentConnections] = await db
.select({
count: count()
})
.from(sites)
.where(
and(
eq(sites.exitNodeId, node.exitNodeId),
eq(sites.online, true)
)
);
if (currentConnections.count >= maxConnections) {
return null;
}
weight =
(maxConnections - currentConnections.count) /
maxConnections;
if (weight === null) {
return null;
}
return {
@@ -95,16 +95,16 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
.limit(1);
if (
exitNode.reachableAt &&
existingSite.subnet &&
existingSite.exitNodeSubnet &&
existingSite.listenPort
) {
const payload = {
oldDestination: {
destinationIP: existingSite.subnet?.split("/")[0],
destinationIP: existingSite.exitNodeSubnet?.split("/")[0],
destinationPort: existingSite.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
},
newDestination: {
destinationIP: site.subnet?.split("/")[0],
destinationIP: site.exitNodeSubnet?.split("/")[0],
destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
}
};
@@ -132,7 +132,10 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
({ targets: dedupedTargets, certs } = dedupeCertsForTargets(targets));
}
const targetsToSend = await convertTargetsIfNecessary(newt.newtId, dedupedTargets); // for backward compatibility with old newt versions that don't support the new target format
const targetsToSend = await convertTargetsIfNecessary(
newt.newtId,
dedupedTargets
); // for backward compatibility with old newt versions that don't support the new target format
return {
message: {
@@ -1,30 +1,20 @@
import { db, ExitNode, newts, remoteExitNodes, Transaction } from "@server/db";
import { db, newts, remoteExitNodes } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers";
import logger from "@server/logger";
import config from "@server/lib/config";
import { findNextAvailableCidr } from "@server/lib/ip";
import {
ExitNodePingResult,
selectBestExitNode,
verifyExitNodeOrgAccess
} from "#dynamic/lib/exitNodes";
import { getUniqueSubnetForExitNode } from "@server/lib/exitNodes";
import { fetchContainers } from "./dockerSocket";
import { lockManager } from "#dynamic/lib/lock";
import { buildTargetConfigurationForNewtClient } from "./buildConfiguration";
import { canCompress } from "@server/lib/clientVersionChecks";
export type ExitNodePingResult = {
exitNodeId: number;
latencyMs: number;
weight: number;
error?: string;
exitNodeName: string;
endpoint: string;
wasPreviouslyConnected: boolean;
};
export const handleNewtRegisterMessage: MessageHandler = async (context) => {
const { message, client, sendToClient } = context;
const newt = client as Newt;
@@ -94,9 +84,12 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
fetchContainers(newt.newtId);
}
let siteSubnet = oldSite.subnet;
let siteSubnet = oldSite.exitNodeSubnet;
let exitNodeIdToQuery = oldSite.exitNodeId;
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {
if (
exitNodeId &&
(oldSite.exitNodeId !== exitNodeId || !oldSite.exitNodeSubnet)
) {
// This effectively moves the exit node to the new one
exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId
@@ -115,7 +108,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
return;
}
const newSubnet = await getUniqueSubnetForSite(exitNode);
const newSubnet = await getUniqueSubnetForExitNode(exitNode);
if (!newSubnet) {
logger.error(
@@ -131,7 +124,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
.set({
pubKey: publicKey,
exitNodeId: exitNodeId,
subnet: newSubnet
exitNodeSubnet: newSubnet
})
.where(eq(sites.siteId, siteId))
.returning();
@@ -250,40 +243,3 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
excludeSender: false // Include sender in broadcast
};
};
async function getUniqueSubnetForSite(
exitNode: ExitNode,
trx: Transaction | typeof db = db
): Promise<string | null> {
const lockKey = `subnet-allocation:${exitNode.exitNodeId}`;
return await lockManager.withLock(
lockKey,
async () => {
const sitesQuery = await trx
.select({
subnet: sites.subnet
})
.from(sites)
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
const blockSize = config.getRawConfig().gerbil.site_block_size;
const subnets = sitesQuery
.map((site) => site.subnet)
.filter(
(subnet) =>
subnet &&
/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
)
.filter((subnet) => subnet !== null);
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
const newSubnet = findNextAvailableCidr(
subnets,
blockSize,
exitNode.address
);
return newSubnet;
},
5000 // 5 second lock TTL - subnet allocation should be quick
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ export * from "./handleNewtRegisterMessage";
export * from "./handleReceiveBandwidthMessage";
export * from "./handleNewtGetConfigMessage";
export * from "./handleSocketMessages";
export * from "./handleNewtPingRequestMessage";
export * from "./handleNewtExitNodesRequestMessage";
export * from "./handleApplyBlueprintMessage";
export * from "./handleNewtPingMessage";
export * from "./handleNewtDisconnectingMessage";
+16 -8
View File
@@ -48,10 +48,6 @@ export async function buildSiteConfigurationForOlmClient(
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
if (sitesData.length === 0) {
return siteConfigurations;
}
// Batch-fetch every site resource this client has access to across ALL sites
// in a single query, then group by siteId in memory. This avoids issuing one
// query per site (which would be N round-trips for N sites).
@@ -68,8 +64,8 @@ export async function buildSiteConfigurationForOlmClient(
clientSiteResourcesAssociationsCache.siteResourceId
)
)
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.leftJoin(networks, eq(siteResources.networkId, networks.networkId))
.leftJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(
and(
eq(
@@ -80,8 +76,20 @@ export async function buildSiteConfigurationForOlmClient(
)
);
const haveInferenceResources = allClientSiteResources.some(
(row) => row.siteResource.requiresExitNodeConnection === true
);
if (sitesData.length === 0) {
return { siteConfigurations, haveInferenceResources };
}
const siteResourcesBySiteId = new Map<number, SiteResource[]>();
for (const row of allClientSiteResources) {
if (!row.siteId) {
// because we are doing a leftJoin above to get the inference resources without a network / sites
continue;
}
const arr = siteResourcesBySiteId.get(row.siteId);
if (arr) {
arr.push(row.siteResource);
@@ -167,7 +175,7 @@ export async function buildSiteConfigurationForOlmClient(
peerOps.push(deletePeer(site.siteId, client.pubKey!));
}
if (!site.subnet) {
if (!site.exitNodeSubnet) {
logger.debug(`Site ${site.siteId} has no subnet, skipping`);
continue;
}
@@ -226,5 +234,5 @@ export async function buildSiteConfigurationForOlmClient(
});
}
return siteConfigurations;
return { siteConfigurations, haveInferenceResources };
}
@@ -0,0 +1,93 @@
import { db, clients } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Olm } from "@server/db";
import logger from "@server/logger";
import { eq } from "drizzle-orm";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { calculateExitNodeWeight } from "@server/lib/exitNodes";
export const handleOlmExitNodesRequestMessage: MessageHandler = async (
context
) => {
const { message, client: olmClient, sendToClient } = context;
const olm = olmClient as Olm;
logger.info("Handling exit nodes request olm message!");
if (!olm) {
logger.warn("olm not found");
return;
}
// Get the olm's orgId through the client relationship
if (!olm.clientId) {
logger.warn("olm clientId not found");
return;
}
const [client] = await db
.select({ orgId: clients.orgId })
.from(clients)
.where(eq(clients.clientId, olm.clientId))
.limit(1);
if (!client || !client.orgId) {
logger.warn("client not found");
return;
}
const { noCloud, chainId } = message.data;
const exitNodesList = await listExitNodes(
client.orgId,
true,
noCloud || false,
olm.clientId
); // filter for only the online ones
let lastExitNodeId = null;
if (olm.clientId) {
const [lastExitNode] = await db
.select()
.from(clients)
.where(eq(clients.clientId, olm.clientId))
.limit(1);
lastExitNodeId = lastExitNode?.exitNodeId || null;
}
const exitNodesPayload = await Promise.all(
exitNodesList.map(async (node) => {
const weight = await calculateExitNodeWeight(
node.exitNodeId,
node.maxConnections
);
if (weight === null) {
return null;
}
return {
exitNodeId: node.exitNodeId,
exitNodeName: node.name,
endpoint: node.endpoint,
weight,
wasPreviouslyConnected: node.exitNodeId === lastExitNodeId
};
})
);
// filter out null values
const filteredExitNodes = exitNodesPayload.filter((node) => node !== null);
return {
message: {
type: "olm/ping/exitNodes",
data: {
exitNodes: filteredExitNodes,
chainId: chainId
}
},
broadcast: false, // Send to all clients
excludeSender: false // Include sender in broadcast
};
};
+98 -9
View File
@@ -1,4 +1,4 @@
import { db, orgs, primaryDb } from "@server/db";
import { db, ExitNode, orgs, primaryDb } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import {
clients,
@@ -22,6 +22,12 @@ import { canCompress } from "@server/lib/clientVersionChecks";
import config from "@server/lib/config";
import cache from "#dynamic/lib/cache"; // not using regional here because we need this in the register message handler before we know where the client is
import { waitForClientRebuildIdle } from "@server/lib/rebuildClientAssociations";
import {
ExitNodePingResult,
selectBestExitNode,
verifyExitNodeOrgAccess
} from "#dynamic/lib/exitNodes";
import { getUniqueSubnetForExitNode } from "@server/lib/exitNodes";
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
@@ -49,11 +55,20 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
olmAgent,
orgId,
userToken,
pingResults,
fingerprint,
postures,
backwardsCompatible,
chainId
} = message.data;
if (backwardsCompatible) {
logger.debug(
"[handleOlmRegisterMessage] Backwards compatible mode detected - not sending connect message and waiting for ping response."
);
return;
}
if (!olm.clientId) {
logger.warn("[handleOlmRegisterMessage] Olm client ID not found");
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
@@ -284,7 +299,65 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
if (client.pubKey !== publicKey || client.archived) {
let exitNodeId: number | undefined;
if (pingResults) {
const bestPingResult = selectBestExitNode(
pingResults as ExitNodePingResult[]
);
if (!bestPingResult) {
logger.warn("No suitable exit node found based on ping results");
}
exitNodeId = bestPingResult?.exitNodeId;
}
let clientSubnet = client.exitNodeSubnet;
let exitNode: ExitNode | null = null;
if (
exitNodeId &&
(client.exitNodeId !== exitNodeId || !client.exitNodeSubnet)
) {
const { exitNode: exitNodeResult, hasAccess } =
await verifyExitNodeOrgAccess(exitNodeId, client.orgId);
exitNode = exitNodeResult;
if (!exitNode) {
logger.warn("[handleOlmRegisterMessage] Exit node not found", {
orgId: client.orgId,
clientId: client.clientId
});
return;
}
if (!hasAccess) {
logger.warn(
"[handleOlmRegisterMessage] Not authorized to use this exit node",
{ orgId: client.orgId, clientId: client.clientId }
);
return;
}
// TODO: IF WE DO NOT HAVE AN INFERENCE RESOURCE DO WE NEED TO BE HOLDING A SUBNET ON THE CLIENT?
const newSubnet = await getUniqueSubnetForExitNode(exitNode);
if (!newSubnet) {
logger.error(
`[handleOlmRegisterMessage] No available subnets found for exit node id ${exitNodeId} and client id ${client.clientId}`,
{ orgId: client.orgId, clientId: client.clientId }
);
return;
}
clientSubnet = newSubnet;
}
if (
client.pubKey !== publicKey ||
client.archived ||
client.exitNodeId !== exitNodeId ||
client.exitNodeSubnet !== clientSubnet
) {
logger.info(
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...",
{ orgId: client.orgId, clientId: client.clientId }
@@ -294,7 +367,9 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
.update(clients)
.set({
pubKey: publicKey,
archived: false
archived: false,
exitNodeId: exitNodeId, // this can be undefined if no exit node was selected, which is fine just means we cant talk to the node or connect to it
exitNodeSubnet: clientSubnet
})
.where(eq(clients.clientId, client.clientId));
@@ -379,12 +454,13 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
// NOTE: its important that the client here is the old client and the public key is the new key
await waitForClientRebuildIdle(olm.clientId);
const siteConfigurations = await buildSiteConfigurationForOlmClient(
client,
publicKey,
relay,
jitMode
);
const { siteConfigurations, haveInferenceResources } =
await buildSiteConfigurationForOlmClient(
client,
publicKey,
relay,
jitMode
);
// Return connect message with all site configurations
return {
@@ -394,6 +470,19 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
sites: siteConfigurations,
tunnelIP: client.subnet,
utilitySubnet: org.utilitySubnet,
exitNode:
exitNode && client.exitNodeSubnet
? {
connect: haveInferenceResources, // we do not need to connect to the exit node if we do not have inference resources
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
relayPort:
config.getRawConfig().gerbil
.clients_start_port,
publicKey: exitNode.publicKey,
serverIP: exitNode.address.split("/")[0],
tunnelIP: client.exitNodeSubnet.split("/")[0]
}
: undefined,
chainId: chainId
}
},
+1
View File
@@ -15,3 +15,4 @@ export * from "./handleOlmServerInitAddPeerHandshake";
export * from "./offlineChecker";
export * from "./handleOlmUnLocalMessage";
export * from "./handleOlmLocalMessage";
export * from "./handleOlmExitNodesRequestMessage";
+4 -4
View File
@@ -311,13 +311,13 @@ export async function createSite(
// lets also make sure there is no overlap with other sites on the exit node
const sitesQuery = await db
.select({
subnet: sites.subnet
subnet: sites.exitNodeSubnet
})
.from(sites)
.where(
and(
eq(sites.exitNodeId, exitNodeId),
eq(sites.subnet, subnet)
eq(sites.exitNodeSubnet, subnet)
)
);
@@ -427,7 +427,7 @@ export async function createSite(
exitNodeId,
name,
niceId: updatedNiceId!,
subnet,
exitNodeSubnet: subnet,
type,
pubKey: pubKey || null,
status: "approved"
@@ -444,7 +444,7 @@ export async function createSite(
type,
dockerSocketEnabled: false,
online: true,
subnet: "0.0.0.0/32",
exitNodeSubnet: "0.0.0.0/32",
status: "approved"
})
.returning();
+1 -1
View File
@@ -125,7 +125,7 @@ function querySitesBase() {
niceId: sites.niceId,
name: sites.name,
pubKey: sites.pubKey,
subnet: sites.subnet,
subnet: sites.exitNodeSubnet,
megabytesIn: sites.megabytesIn,
megabytesOut: sites.megabytesOut,
orgName: orgs.name,
+4 -3
View File
@@ -43,7 +43,6 @@ const PickSiteDefaultsResponseDataSchema = z.object({
clientAddress: z.string().optional()
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/pick-site-defaults",
@@ -60,7 +59,9 @@ registry.registerPath({
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(PickSiteDefaultsResponseDataSchema)
schema: createApiResponseSchema(
PickSiteDefaultsResponseDataSchema
)
}
}
}
@@ -108,7 +109,7 @@ export async function pickSiteDefaults(
// list all of the sites on that exit node
const sitesQuery = await db
.select({
subnet: sites.subnet
subnet: sites.exitNodeSubnet
})
.from(sites)
.where(eq(sites.exitNodeId, randomExitNode.exitNodeId));
@@ -49,7 +49,7 @@ const createSiteResourceSchema = z
name: z.string().min(1).max(255),
niceId: z.string().optional(),
// protocol: z.enum(["tcp", "udp"]).optional(),
mode: z.enum(["host", "cidr", "http", "ssh"]),
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]),
ssl: z.boolean().optional(), // only used for http mode
scheme: z.enum(["http", "https"]).optional(),
siteIds: z.array(z.int()).optional(),
@@ -171,6 +171,9 @@ const createSiteResourceSchema = z
)
.refine(
(data) => {
if (data.mode == "inference") {
return true;
}
return (
(data.siteIds !== undefined && data.siteIds.length > 0) ||
data.siteId !== undefined
@@ -533,21 +536,24 @@ export async function createSiteResource(
let newSiteResource: SiteResource | undefined;
try {
await db.transaction(async (trx) => {
const [network] = await trx
.insert(networks)
.values({
scope: "resource",
orgId: orgId
})
.returning();
let network: typeof networks.$inferSelect | undefined;
if (mode !== "inference") {
[network] = await trx
.insert(networks)
.values({
scope: "resource",
orgId: orgId
})
.returning();
if (!network) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Failed to create network`
)
);
if (!network) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Failed to create network`
)
);
}
}
let tcpPortRangeStringAdjusted = tcpPortRangeString;
@@ -566,7 +572,7 @@ export async function createSiteResource(
name,
mode,
ssl,
networkId: network.networkId,
networkId: network ? network.networkId : null,
destination: destination, // the ssh can be null
scheme,
destinationPort,
@@ -582,7 +588,8 @@ export async function createSiteResource(
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain
fullDomain,
requiresExitNodeConnection: mode === "inference" // in the future we might want to have different modes that do this
};
if (isLicensedSshPam) {
if (authDaemonPort !== undefined)
@@ -600,11 +607,13 @@ export async function createSiteResource(
//////////////////// update the associations ////////////////////
for (const siteId of siteIds) {
await trx.insert(siteNetworks).values({
siteId: siteId,
networkId: network.networkId
});
if (network) {
for (const siteId of siteIds) {
await trx.insert(siteNetworks).values({
siteId: siteId,
networkId: network.networkId
});
}
}
const [adminRole] = await trx
@@ -50,7 +50,7 @@ const updateSiteResourceSchema = z
)
.optional(),
// mode: z.enum(["host", "cidr", "port"]).optional(),
mode: z.enum(["host", "cidr", "http", "ssh"]).optional(),
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]).optional(),
ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).nullish(),
destinationPort: z.int().positive().nullish(),
@@ -173,6 +173,9 @@ const updateSiteResourceSchema = z
)
.refine(
(data) => {
if (data.mode == "inference") {
return true;
}
// if neither is provided, the existing site associations are left unchanged
if (data.siteIds === undefined && data.siteId === undefined) {
return true;
@@ -579,13 +582,15 @@ export async function updateSiteResource(
disableIcmp:
mode !== undefined
? disableIcmp ||
(mode == "http" || mode == "ssh"
? true
: false)
(mode == "http" || mode == "ssh" ? true : false)
: disableIcmp,
domainId,
subdomain: finalSubdomain,
fullDomain,
networkId:
mode === "inference" ? null : undefined,
requiresExitNodeConnection:
mode !== undefined ? mode === "inference" : undefined,
...sshPamSet
})
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
@@ -593,7 +598,20 @@ export async function updateSiteResource(
//////////////////// update the associations ////////////////////
if (siteIds !== undefined) {
if (mode === "inference") {
// inference resources are not attached to any site network
if (existingSiteResource.networkId) {
await trx
.delete(siteNetworks)
.where(
eq(
siteNetworks.networkId,
existingSiteResource.networkId
)
);
}
updatedSiteIds = [];
} else if (siteIds !== undefined) {
// delete the site - site resources associations
await trx
.delete(siteNetworks)
+1 -1
View File
@@ -263,7 +263,7 @@ export async function createTarget(
// make sure the target is within the site subnet
if (
site.type == "wireguard" &&
!isIpInCidr(targetData.ip, site.subnet!)
!isIpInCidr(targetData.ip, site.exitNodeSubnet!)
) {
return next(
createHttpError(
+5 -3
View File
@@ -5,7 +5,7 @@ import {
handleNewtGetConfigMessage,
handleDockerStatusMessage,
handleDockerContainersMessage,
handleNewtPingRequestMessage,
handleNewtExitNodesRequestMessage,
handleApplyBlueprintMessage,
handleNewtPingMessage,
startNewtOfflineChecker,
@@ -22,7 +22,8 @@ import {
handleOlmDisconnectingMessage,
handleOlmServerInitAddPeerHandshake,
handleOlmLocalMessage,
handleOlmUnLocalMessage
handleOlmUnLocalMessage,
handleOlmExitNodesRequestMessage
} from "../olm";
import { handleHealthcheckStatusMessage } from "../target";
import { handleRoundTripMessage } from "./handleRoundTripMessage";
@@ -37,6 +38,7 @@ export const messageHandlers: Record<string, MessageHandler> = {
"olm/wg/local": handleOlmLocalMessage,
"olm/wg/unlocal": handleOlmUnLocalMessage,
"olm/ping": handleOlmPingMessage,
"olm/ping/request": handleOlmExitNodesRequestMessage,
"olm/disconnecting": handleOlmDisconnectingMessage,
"newt/disconnecting": handleNewtDisconnectingMessage,
"newt/ping": handleNewtPingMessage,
@@ -45,7 +47,7 @@ export const messageHandlers: Record<string, MessageHandler> = {
"newt/receive-bandwidth": handleReceiveBandwidthMessage,
"newt/socket/status": handleDockerStatusMessage,
"newt/socket/containers": handleDockerContainersMessage,
"newt/ping/request": handleNewtPingRequestMessage,
"newt/ping/request": handleNewtExitNodesRequestMessage,
"newt/blueprint/apply": handleApplyBlueprintMessage,
"newt/healthcheck/status": handleHealthcheckStatusMessage,
"ws/round-trip/complete": handleRoundTripMessage
@@ -72,8 +72,7 @@ export default function CredentialsPage() {
const { data: latestVersions } = useQuery(
productUpdatesQueries.latestVersion(true)
);
const newtVersion =
latestVersions?.data?.newt?.latestVersion ?? "latest";
const newtVersion = latestVersions?.data?.newt?.latestVersion ?? "latest";
// Fetch site defaults for wireguard sites to show in obfuscated config
useEffect(() => {
@@ -354,7 +353,7 @@ export default function CredentialsPage() {
text={generateObfuscatedWireGuardConfig(
{
subnet:
site?.subnet ||
site?.exitNodeSubnet ||
siteDefaults?.subnet ||
null,
address:
+3 -1
View File
@@ -134,7 +134,9 @@ export default function UptimeBar({
if (!data) return null;
const allNoData = data.days.every((d) => d.status === "no_data");
const allNoData = data.days.every(
(d) => d.status === "no_data" || d.status === "unknown"
);
return (
<div className={cn("space-y-3", className)}>
+3 -1
View File
@@ -124,7 +124,9 @@ export function UptimeMiniBar({
if (!data) return null;
const allNoData = data.days.every((d) => d.status === "no_data");
const allNoData = data.days.every(
(d) => d.status === "no_data" || d.status === "unknown"
);
return (
<div className="flex items-center gap-2">