aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Moon Flower <mforoud86@gmail.com> 2026-02-24 11:00:04 +0330
committerGravatar Moon Flower <mforoud86@gmail.com> 2026-02-24 11:00:04 +0330
commit60929c580b6697d7679baeff8b8fe0dafd265d01 (patch)
tree17252d7c55bf00ffb4173faac961274d03683ba0
parentadd relays txt (diff)
broadlayer http-only server
-rw-r--r--layer/.gitignore34
-rw-r--r--layer/broadlayer.ts238
-rw-r--r--layer/bun.lock26
-rw-r--r--layer/package.json10
-rw-r--r--layer/tsconfig.json29
5 files changed, 337 insertions, 0 deletions
diff --git a/layer/.gitignore b/layer/.gitignore
new file mode 100644
index 0000000..a14702c
--- /dev/null
+++ b/layer/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/layer/broadlayer.ts b/layer/broadlayer.ts
new file mode 100644
index 0000000..1e0d415
--- /dev/null
+++ b/layer/broadlayer.ts
@@ -0,0 +1,238 @@
+import { randomUUIDv7 } from "bun"
+
+if (process.argv.length < 4) {
+ console.log(`more args.\n${process.argv[0]} <ip> <port>`)
+}
+
+const ip = process.argv[2] ?? '0.0.0.0'
+const port = Number(process.argv[3])
+
+if (!Number.isInteger(port) || port < 1 || port > 65535) {
+ console.log("bad port")
+ process.exit(1)
+}
+
+type tcpsocket = {
+ mode: 'tcp'
+ socket?: Bun.Socket
+}
+type udpsocket = {
+ mode: 'udp'
+ handler: (buf: Buffer) => void
+ die: () => void
+ heartbeat?: Timer
+}
+type socket = tcpsocket | udpsocket
+type connection = {
+ ip: string
+ port: number
+ queue: Buffer[]
+ remoteId?: string
+ lastSeen: number
+} & socket
+
+const udp = await Bun.udpSocket({
+ socket: {
+ data(socket, data, port, ip) {
+ const id = Object.values(sessions).findIndex(s =>
+ s.mode === 'udp' &&
+ s.ip === ip &&
+ s.port === port
+ )
+
+ if (id === -1) return
+
+ // send to handler
+ if (sessions[id] && sessions[id].mode === 'udp') {
+ sessions[id].handler(data)
+ }
+ },
+ error(socket, error) {
+ // dead.
+ for (const sess of Object.values(sessions).filter(e => e.mode === 'udp')) {
+ sess.die()
+ }
+ }
+ }
+})
+
+const connectionsPerIp = new Map<string, number>()
+const sessions: Record<string, connection> = {}
+
+async function establishTCP(ip: string, port: number): Promise<string | false> {
+ const id = randomUUIDv7()
+
+ const conn: connection = {
+ port,
+ ip,
+ mode: 'tcp',
+ socket: undefined,
+ queue: [],
+ lastSeen: Date.now()
+ }
+
+ const sockPromise = Bun.connect({
+ port,
+ hostname: ip,
+ reusePort: true,
+ tls: false,
+ socket: {
+ data(socket, data) { conn.queue.push(data) },
+ close(socket, error) { delete sessions[id] },
+ end(socket) { delete sessions[id] },
+ error(socket, error) { delete sessions[id] }
+ }
+ })
+
+ const timeoutPromise: Promise<0> = new Promise((r) => setTimeout(() => r(0), 5000))
+
+ const sock: Bun.Socket | 0 = await Promise.race([sockPromise, timeoutPromise]).catch(err => {
+ console.log(err)
+ return 0
+ })
+
+ if (sock === 0) return false
+
+ conn.socket = sock
+ sessions[id] = conn
+ return id
+}
+
+async function establishUDP(ip: string, port: number): Promise<string> {
+ const id = randomUUIDv7()
+
+ const conn: connection = {
+ port,
+ ip,
+ mode: 'udp',
+ handler: undefined as any,
+ die: undefined as any,
+ heartbeat: undefined,
+ queue: [],
+ lastSeen: Date.now()
+ }
+
+ conn.handler = (data) => {
+ if (!conn.heartbeat) {
+ const str = data.toString()
+ const bnI = str.indexOf('\n')
+ if (str.startsWith('T') && bnI !== -1) {
+ const tm = Number(str.substring(1, bnI))
+ if (!isNaN(tm)) {
+ conn.heartbeat = setInterval(() => {
+ udp.send('N\n', conn.port, conn.ip)
+ }, tm * 1000)
+ }
+ const temp_len = bnI + 1
+ if (str.length > temp_len) {
+ data = Buffer.from(str.substring(temp_len))
+ } else {
+ return
+ }
+ }
+ }
+ conn.queue.push(data)
+ }
+ conn.die = () => {
+ clearInterval(conn.heartbeat)
+ conn.heartbeat = undefined
+ }
+
+ sessions[id] = conn
+ udp.send("N\n", port, ip) // handshake
+
+ return id
+}
+
+const eviction_timeout = 60_000
+
+setInterval(() => {
+ for (const key of Object.keys(sessions)) {
+ const sess = sessions[key]!
+ if (sess.lastSeen > Date.now() - eviction_timeout) continue
+ // evict
+ if (sess.mode === 'tcp') sess.socket?.close()
+ else sess.die()
+ delete sessions[key]
+ }
+}, eviction_timeout)
+
+Bun.serve({
+ port,
+ hostname: ip,
+ routes: {
+ '/': new Response('this is a broadlayer server.'),
+ '/tcp/connect/:ip/:port': async req => {
+ const port = parseInt(req.params.port)
+ if (isNaN(port)) return new Response('err_badport')
+ if (port > 65535 || port < 0) return new Response('err_badport')
+
+ const clientIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
+ let count = connectionsPerIp.get(clientIp) || 0
+ if (count >= 4) {
+ return new Response('err_toomany', { status: 429 })
+ }
+ connectionsPerIp.set(clientIp, count + 1)
+
+ const res = await establishTCP(req.params.ip, port)
+ if (res === false) return new Response('err_timeout')
+ return new Response('ok_' + res)
+ },
+ '/udp/connect/:ip/:port': async req => {
+ const port = parseInt(req.params.port)
+ if (isNaN(port)) return new Response('err_badport')
+ if (port > 65535 || port < 0) return new Response('err_badport')
+
+ const clientIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
+ let count = connectionsPerIp.get(clientIp) || 0
+ if (count >= 4) {
+ return new Response('err_toomany', { status: 429 })
+ }
+ connectionsPerIp.set(clientIp, count + 1)
+
+ return new Response('ok_' + await establishUDP(req.params.ip, port))
+ },
+ '/poll/:id': async req => { // read and consume queue
+ const sess = sessions[req.params.id]
+ if (!sess) return new Response('err_badid')
+ const resp = JSON.stringify(sess.queue)
+ sess.queue = []
+ sess.lastSeen = Date.now()
+ return new Response(resp)
+ },
+ '/peek/:id': async req => { // read queue without consuming
+ const sess = sessions[req.params.id]
+ if (!sess) return new Response('err_badid')
+ const resp = JSON.stringify(sess.queue)
+ sess.lastSeen = Date.now()
+ return new Response(resp)
+ },
+ '/close/:id': async req => { // closes a connection
+ const sess = sessions[req.params.id]
+ if (!sess) return new Response('err_badid')
+ if (sess.mode === 'tcp') sess.socket?.close()
+
+ delete sessions[req.params.id]
+
+ const clientIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
+ let count = connectionsPerIp.get(clientIp) || 0
+ if (count > 0) connectionsPerIp.set(clientIp, count - 1)
+ if (count <= 1) connectionsPerIp.delete(clientIp)
+
+ return new Response('ok')
+ },
+ '/send/:id': {
+ POST: async req => {
+ const sess = sessions[req.params.id]
+ if (!sess) return new Response('err_badid')
+
+ if (!req.body) return new Response('err_empty')
+ const buf = await (await req.body.blob())!.arrayBuffer()
+ if (sess.mode === 'tcp') sess.socket?.write(buf)
+ else udp.send(buf, sess.port, sess.ip)
+
+ return new Response('ok')
+ }
+ }
+ }
+}) \ No newline at end of file
diff --git a/layer/bun.lock b/layer/bun.lock
new file mode 100644
index 0000000..69eee26
--- /dev/null
+++ b/layer/bun.lock
@@ -0,0 +1,26 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "layer",
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ },
+ "packages": {
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
+
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
+
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+ }
+}
diff --git a/layer/package.json b/layer/package.json
new file mode 100644
index 0000000..206fc3d
--- /dev/null
+++ b/layer/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "layer",
+ "private": true,
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ }
+}
diff --git a/layer/tsconfig.json b/layer/tsconfig.json
new file mode 100644
index 0000000..bfa0fea
--- /dev/null
+++ b/layer/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ // Environment setup & latest features
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}