@@ -27,15 +27,18 @@ export interface ConfigApplyRawPayload {
2727
2828export interface ProxyAddPayload {
2929 proxy : ProxyConfig
30+ nodeId ?: string // Target node ID (for server mode RPC forwarding)
3031}
3132
3233export interface ProxyUpdatePayload {
3334 name : string
3435 proxy : Partial < ProxyConfig >
36+ nodeId ?: string // Target node ID (for server mode RPC forwarding)
3537}
3638
3739export interface ProxyRemovePayload {
3840 name : string
41+ nodeId ?: string // Target node ID (for server mode RPC forwarding)
3942}
4043
4144export interface ProxyGetPayload {
@@ -452,29 +455,81 @@ export class FrpBridge {
452455 }
453456 }
454457
455- // Proxy/tunnel management commands (client mode only)
458+ // Helper function to check if proxy type uses remotePort
459+ const typeUsesRemotePort = ( type : string ) : boolean => {
460+ return [ 'tcp' , 'udp' , 'stcp' , 'xtcp' , 'sudp' , 'tcpmux' ] . includes ( type )
461+ }
462+
463+ // Proxy/tunnel management commands
456464 const proxyAdd : CommandHandler < ProxyAddPayload > = async ( command , _ctx ) => {
457- if ( this . mode !== 'client' ) {
465+ const payload = command . payload
466+ if ( ! payload || ! payload . proxy ) {
458467 return {
459468 status : 'failed' ,
460469 error : {
461470 code : 'VALIDATION_ERROR' ,
462- message : 'proxy.add is only available in client mode '
471+ message : 'proxy.add requires payload.proxy '
463472 }
464473 }
465474 }
466475
467- const payload = command . payload
468- if ( ! payload || ! payload . proxy ) {
469- return {
470- status : 'failed' ,
471- error : {
472- code : 'VALIDATION_ERROR' ,
473- message : 'proxy.add requires payload.proxy'
476+ // Server mode: forward to node via RPC or validate globally
477+ if ( this . mode === 'server' ) {
478+ if ( ! payload . nodeId ) {
479+ return {
480+ status : 'failed' ,
481+ error : {
482+ code : 'VALIDATION_ERROR' ,
483+ message : 'proxy.add requires payload.nodeId in server mode'
484+ }
485+ }
486+ }
487+
488+ // Check global port conflict before forwarding
489+ const proxyRemotePort = ( payload . proxy as any ) . remotePort
490+ if ( proxyRemotePort && typeUsesRemotePort ( payload . proxy . type ) ) {
491+ const portCheck = this . nodeManager ?. isRemotePortInUse ( proxyRemotePort , payload . nodeId )
492+ if ( portCheck ?. inUse ) {
493+ return {
494+ status : 'failed' ,
495+ error : {
496+ code : 'PORT_CONFLICT' ,
497+ message : `Remote port ${ proxyRemotePort } is already in use by tunnel "${ portCheck . tunnelName } " on node ${ portCheck . nodeId } `
498+ }
499+ }
500+ }
501+ }
502+
503+ // Forward to node via RPC
504+ if ( ! this . rpcServer ) {
505+ return {
506+ status : 'failed' ,
507+ error : {
508+ code : 'RPC_NOT_AVAILABLE' ,
509+ message : 'RPC server not available'
510+ }
511+ }
512+ }
513+
514+ try {
515+ const result = await this . rpcServer . rpcCall ( payload . nodeId , 'proxy.add' , { proxy : payload . proxy } )
516+ return {
517+ status : 'success' ,
518+ result
519+ }
520+ }
521+ catch ( error ) {
522+ return {
523+ status : 'failed' ,
524+ error : {
525+ code : 'RPC_ERROR' ,
526+ message : error instanceof Error ? error . message : 'Failed to add tunnel on node'
527+ }
474528 }
475529 }
476530 }
477531
532+ // Client mode: add locally
478533 try {
479534 this . process . addTunnel ( payload . proxy )
480535 return {
@@ -494,27 +549,73 @@ export class FrpBridge {
494549 }
495550
496551 const proxyUpdate : CommandHandler < ProxyUpdatePayload > = async ( command , _ctx ) => {
497- if ( this . mode !== 'client' ) {
552+ const payload = command . payload
553+ if ( ! payload || ! payload . name ) {
498554 return {
499555 status : 'failed' ,
500556 error : {
501557 code : 'VALIDATION_ERROR' ,
502- message : 'proxy.update is only available in client mode '
558+ message : 'proxy.update requires payload.name '
503559 }
504560 }
505561 }
506562
507- const payload = command . payload
508- if ( ! payload || ! payload . name ) {
509- return {
510- status : 'failed' ,
511- error : {
512- code : 'VALIDATION_ERROR' ,
513- message : 'proxy.update requires payload.name'
563+ // Server mode: forward to node via RPC
564+ if ( this . mode === 'server' ) {
565+ if ( ! payload . nodeId ) {
566+ return {
567+ status : 'failed' ,
568+ error : {
569+ code : 'VALIDATION_ERROR' ,
570+ message : 'proxy.update requires payload.nodeId in server mode'
571+ }
572+ }
573+ }
574+
575+ // Check global port conflict if remotePort is being changed
576+ const newRemotePort = ( payload . proxy as any ) ?. remotePort
577+ if ( newRemotePort && typeUsesRemotePort ( ( payload . proxy as any ) ?. type ) ) {
578+ const portCheck = this . nodeManager ?. isRemotePortInUse ( newRemotePort , payload . nodeId )
579+ if ( portCheck ?. inUse ) {
580+ return {
581+ status : 'failed' ,
582+ error : {
583+ code : 'PORT_CONFLICT' ,
584+ message : `Remote port ${ newRemotePort } is already in use by tunnel "${ portCheck . tunnelName } " on node ${ portCheck . nodeId } `
585+ }
586+ }
587+ }
588+ }
589+
590+ if ( ! this . rpcServer ) {
591+ return {
592+ status : 'failed' ,
593+ error : {
594+ code : 'RPC_NOT_AVAILABLE' ,
595+ message : 'RPC server not available'
596+ }
597+ }
598+ }
599+
600+ try {
601+ const result = await this . rpcServer . rpcCall ( payload . nodeId , 'proxy.update' , { name : payload . name , proxy : payload . proxy } )
602+ return {
603+ status : 'success' ,
604+ result
605+ }
606+ }
607+ catch ( error ) {
608+ return {
609+ status : 'failed' ,
610+ error : {
611+ code : 'RPC_ERROR' ,
612+ message : error instanceof Error ? error . message : 'Failed to update tunnel on node'
613+ }
514614 }
515615 }
516616 }
517617
618+ // Client mode: update locally
518619 try {
519620 this . process . updateTunnel ( payload . name , payload . proxy )
520621 return {
@@ -534,27 +635,58 @@ export class FrpBridge {
534635 }
535636
536637 const proxyRemove : CommandHandler < ProxyRemovePayload > = async ( command , _ctx ) => {
537- if ( this . mode !== 'client' ) {
638+ const payload = command . payload
639+ if ( ! payload || ! payload . name ) {
538640 return {
539641 status : 'failed' ,
540642 error : {
541643 code : 'VALIDATION_ERROR' ,
542- message : 'proxy.remove is only available in client mode '
644+ message : 'proxy.remove requires payload.name '
543645 }
544646 }
545647 }
546648
547- const payload = command . payload
548- if ( ! payload || ! payload . name ) {
549- return {
550- status : 'failed' ,
551- error : {
552- code : 'VALIDATION_ERROR' ,
553- message : 'proxy.remove requires payload.name'
649+ // Server mode: forward to node via RPC
650+ if ( this . mode === 'server' ) {
651+ if ( ! payload . nodeId ) {
652+ return {
653+ status : 'failed' ,
654+ error : {
655+ code : 'VALIDATION_ERROR' ,
656+ message : 'proxy.remove requires payload.nodeId in server mode'
657+ }
658+ }
659+ }
660+
661+ if ( ! this . rpcServer ) {
662+ return {
663+ status : 'failed' ,
664+ error : {
665+ code : 'RPC_NOT_AVAILABLE' ,
666+ message : 'RPC server not available'
667+ }
668+ }
669+ }
670+
671+ try {
672+ const result = await this . rpcServer . rpcCall ( payload . nodeId , 'proxy.remove' , { name : payload . name } )
673+ return {
674+ status : 'success' ,
675+ result
676+ }
677+ }
678+ catch ( error ) {
679+ return {
680+ status : 'failed' ,
681+ error : {
682+ code : 'RPC_ERROR' ,
683+ message : error instanceof Error ? error . message : 'Failed to remove tunnel on node'
684+ }
554685 }
555686 }
556687 }
557688
689+ // Client mode: remove locally
558690 try {
559691 this . process . removeTunnel ( payload . name )
560692 return {
0 commit comments