fix(network): скрыть GRE PSK и сохранить признак IPsec
This commit is contained in:
1 parent
a404da768d
commit
3f3f06f147
24 files changed
+1570
-23
No files matched your search
@@ -29,7 +29,7 @@
|
||||
"test:scheduler": "tsx src/services/scheduler-running.test.ts && tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts",
|
||||
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
|
||||
"test:ipsec": "tsx src/services/ipsec-config.test.ts && tsx src/services/ipsec-state.test.ts && tsx src/routes/ipsec-history.test.ts && tsx src/services/ipsec-restore.test.ts && tsx src/services/ipsec-operation.test.ts && tsx src/routes/ipsec-mutation.test.ts && tsx src/routes/ipsec-nat.test.ts && tsx src/routes/ipsec-group.test.ts",
|
||||
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test:live-maps": "tsx src/services/gre-read.test.ts && tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test": "node scripts/test.mjs",
|
||||
"test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
|
||||
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
import {randomBytes} from 'node:crypto'
|
||||
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
|
||||
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
|
||||
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own209.invalid';process.env.AUTH_PORTAL_URL='https://own209.invalid'
|
||||
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
|
||||
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
|
||||
const psk='own209-'+randomBytes(24).toString('base64url'),baseline=process.env.MMAPP_GRE_SECRET_EXPECT==='leak',cases=[];let app,serverId,nativeWrites=0
|
||||
const native=createServer((req,res)=>{if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');res.end(JSON.stringify(req.url.endsWith('/interface/gre')?[{'.id':'*1',name:'own209-encrypted','local-address':'192.0.2.1','remote-address':'192.0.2.2','ipsec-secret':psk,mtu:'1476',keepalive:'10s,3',disabled:'false',running:'true','allow-fast-path':'false'},{'.id':'*2',name:'own209-plain','remote-address':'192.0.2.3','ipsec-secret':'',disabled:'false',running:'true'}]:[]))});await new Promise(r=>native.listen(0,'127.0.0.1',r))
|
||||
const report={date:'2026-10-13',block:'12.5.209',database,baseline,cases,scope:'Production compiled GRE/Filters GET and auth with own restricted PostgreSQL, local native HTTP fixture, synthetic nonempty PSK and isolated signed JWT',passed:false}
|
||||
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own209-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready()
|
||||
for(const role of ['reader','filter-writer','network-writer','network-admin','is-admin-only']){const permissions=['mm:network:read','mm:filters:read',...(role==='filter-writer'?['mm:filters:write']:role==='network-writer'?['mm:network:write']:role==='network-admin'?['mm:network:admin']:[])],token=app.jwt.sign({sub:'own209-'+role,apps:['mm'],permissions,is_admin:role==='is-admin-only',iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});for(const path of ['/api/gre/tunnels','/api/filters/gre-tunnels']){const r=await app.inject({method:'GET',url:path+'?serverId='+serverId,headers:{authorization:'Bearer '+token}});assert.equal(r.statusCode,200);const rows=r.json().tunnels;assert.equal(rows.length,2);const encrypted=rows.find(t=>t.name==='own209-encrypted'),plain=rows.find(t=>t.name==='own209-plain');assert.equal(plain.ipsec,null);const direct=path==='/api/gre/tunnels',privileged=role==='network-writer'||role==='network-admin';if(direct){assert(encrypted.ipsec);assert.equal(encrypted.ipsec.secret,baseline||privileged?psk:'')}else if(baseline)assert.equal(encrypted.ipsec,null);else{assert(encrypted.ipsec);assert.equal(encrypted.ipsec.secret,'')}const exposed=r.body.includes(psk);assert.equal(exposed,direct&&(baseline||privileged));cases.push({role,path,status:200,pskExposed:exposed,configuredStatePresent:!!encrypted.ipsec,plainStateNull:true})}}
|
||||
for(const path of ['/api/gre/tunnels','/api/filters/gre-tunnels']){assert.equal((await app.inject({method:'GET',url:path})).statusCode,401);cases.push({role:'unauthenticated',path,status:401})}
|
||||
const revisions=(await pool.query("SELECT payload FROM config_revisions WHERE server_id=$1 AND section='gre'",[serverId])).rows;assert(revisions.length>0);assert(revisions.some(r=>JSON.stringify(r.payload).includes(psk)));assert.equal(nativeWrites,0);report.internalSnapshotRetainsPsk=true;report.nativeWrites=0;report.passed=true
|
||||
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre209-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/gre209-'+(baseline?'baseline':'fixed')+'-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}
|
||||
if(!report.passed)process.exitCode=1
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
|
||||
interface RosGre {
|
||||
"ipsec-secret"?: string
|
||||
".id"?: string
|
||||
name?: string
|
||||
"local-address"?: string
|
||||
@@ -51,7 +52,7 @@ interface LiveGreTunnel {
|
||||
localInnerIp: string
|
||||
remoteInnerIp: string
|
||||
poolId: string
|
||||
ipsec: null
|
||||
ipsec: { secret: string } | null
|
||||
mtu: number
|
||||
keepaliveInterval: number
|
||||
keepaliveRetries: number
|
||||
@@ -238,7 +239,8 @@ async function fetchServerFilters(server: ServerRow) {
|
||||
localInnerIp: inner.localInnerIp,
|
||||
remoteInnerIp: inner.remoteInnerIp,
|
||||
poolId: "live",
|
||||
ipsec: null,
|
||||
// This routing catalog carries security state, never key material.
|
||||
ipsec: g["ipsec-secret"]?.trim() ? { secret: "" } : null,
|
||||
mtu: Number.parseInt(g.mtu ?? "1476", 10) || 1476,
|
||||
keepaliveInterval: keepalive.interval,
|
||||
keepaliveRetries: keepalive.retries,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import { z } from "zod"
|
||||
import { greReadForUser } from "../services/gre-read.js"
|
||||
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
||||
import { getEnabledServerById } from "../services/wireguard-live.js"
|
||||
import {
|
||||
@@ -111,7 +112,10 @@ const greRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
const sid = parseDbServerId(q.serverId)
|
||||
if (q.serverId !== undefined && sid === null) return reply.status(400).send({ error: "Некорректный serverId GRE" })
|
||||
const result = await listGreTunnels({ serverId: sid !== null ? String(sid) : undefined })
|
||||
return reply.send(result)
|
||||
return reply.send({
|
||||
...result,
|
||||
tunnels: result.tunnels.map(tunnel => greReadForUser(tunnel, req.authUser)),
|
||||
})
|
||||
})
|
||||
|
||||
app.post("/gre/tunnels", async (req, reply) => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {greReadForUser} from './gre-read.js'
|
||||
import type {AuthUser} from '../lib/permissions.js'
|
||||
const tunnel={id:'own209',ipsec:{secret:'own209-PSK',encAlg:'aes-256'},enabled:true}
|
||||
for(const permission of ['mm:network:read','mm:filters:write','mm:network:write','mm:network:admin']){const user={id:'own209',email:'',name:'',apps:['mm'],permissions:[permission],isAdmin:false} satisfies AuthUser;const read=greReadForUser(tunnel,user);assert.equal(read.ipsec?.secret,permission==='mm:network:read'||permission==='mm:filters:write'?'':'own209-PSK');assert.equal(read.ipsec?.encAlg,'aes-256');assert.equal(tunnel.ipsec.secret,'own209-PSK')}
|
||||
assert.equal(greReadForUser(tunnel),tunnel)
|
||||
assert.equal(greReadForUser({ipsec:null},{id:'own209',email:'',name:'',apps:['mm'],permissions:['mm:network:read'],isAdmin:true}).ipsec,null)
|
||||
assert.equal(greReadForUser(tunnel,{id:'own209',email:'',name:'',apps:['mm'],permissions:['mm:network:read'],isAdmin:true}).ipsec.secret,'')
|
||||
console.log('GRE read projection: PSK hidden, configured state and algorithms preserved, source unchanged, writer/legacy retained')
|
||||
@@ -0,0 +1,7 @@
|
||||
import {hasPermission,type AuthUser} from "../lib/permissions.js"
|
||||
|
||||
/** Project HTTP data without changing the secret-bearing native snapshot. */
|
||||
export function greReadForUser<T extends {ipsec:{secret:string}|null}>(tunnel:T,user?:AuthUser):T {
|
||||
if (!user || hasPermission(user.permissions,"mm:network:write") || !tunnel.ipsec) return tunnel
|
||||
return {...tunnel,ipsec:{...tunnel.ipsec,secret:""}}
|
||||
}
|
||||
Reference in new issue
Block a user