@@ -2855,6 +2855,183 @@ describe('Proxy structured producers', function () {
28552855 expect ( tailscale ) . to . not . have . property ( 'detour' ) ;
28562856 } ) ;
28572857
2858+ it ( 'emits boolean ssh_server for sing-box Tailscale endpoints' , function ( ) {
2859+ const output = loadProducedJson ( 'sing-box' , {
2860+ type : 'tailscale' ,
2861+ name : 'Tailscale SSH Boolean' ,
2862+ 'ssh-server' : true ,
2863+ } ) ;
2864+
2865+ const tailscale = output . endpoints . find (
2866+ ( endpoint ) => endpoint . tag === 'Tailscale SSH Boolean' ,
2867+ ) ;
2868+
2869+ expect ( tailscale . ssh_server ) . to . equal ( true ) ;
2870+ } ) ;
2871+
2872+ it ( 'emits object ssh_server for sing-box Tailscale endpoints' , function ( ) {
2873+ const output = loadProducedJson ( 'sing-box' , {
2874+ type : 'tailscale' ,
2875+ name : 'Tailscale SSH Object' ,
2876+ 'ssh-server' : {
2877+ enabled : false ,
2878+ 'disable-pty' : true ,
2879+ 'disable-sftp' : true ,
2880+ 'disable-forwarding' : true ,
2881+ } ,
2882+ } ) ;
2883+
2884+ const tailscale = output . endpoints . find (
2885+ ( endpoint ) => endpoint . tag === 'Tailscale SSH Object' ,
2886+ ) ;
2887+
2888+ expect ( tailscale . ssh_server ) . to . deep . equal ( {
2889+ enabled : false ,
2890+ disable_pty : true ,
2891+ disable_sftp : true ,
2892+ disable_forwarding : true ,
2893+ } ) ;
2894+ } ) ;
2895+
2896+ it ( 'does not emit gecko packet sizes for sing-box when using default values' , function ( ) {
2897+ const output = loadProducedJson ( 'sing-box' , {
2898+ type : 'hysteria2' ,
2899+ name : 'Gecko Default Sizes' ,
2900+ server : 'hy2.example.com' ,
2901+ port : 443 ,
2902+ password : 'secret' ,
2903+ obfs : 'gecko' ,
2904+ 'obfs-password' : 'mask' ,
2905+ } ) ;
2906+
2907+ const hysteria2 = output . outbounds . find (
2908+ ( outbound ) => outbound . tag === 'Gecko Default Sizes' ,
2909+ ) ;
2910+
2911+ expectSubset ( hysteria2 , {
2912+ type : 'hysteria2' ,
2913+ obfs : {
2914+ type : 'gecko' ,
2915+ password : 'mask' ,
2916+ } ,
2917+ } ) ;
2918+ expect ( hysteria2 . obfs ) . to . not . have . property ( 'min_packet_size' ) ;
2919+ expect ( hysteria2 . obfs ) . to . not . have . property ( 'max_packet_size' ) ;
2920+ } ) ;
2921+
2922+ it ( 'emits a single gecko packet size for sing-box and relies on the other default' , function ( ) {
2923+ const minOnlyOutput = loadProducedJson ( 'sing-box' , {
2924+ type : 'hysteria2' ,
2925+ name : 'Gecko Min Only' ,
2926+ server : 'hy2.example.com' ,
2927+ port : 443 ,
2928+ password : 'secret' ,
2929+ obfs : 'gecko' ,
2930+ 'obfs-password' : 'mask' ,
2931+ 'obfs-min-packet-size' : 600 ,
2932+ } ) ;
2933+ const maxOnlyOutput = loadProducedJson ( 'sing-box' , {
2934+ type : 'hysteria2' ,
2935+ name : 'Gecko Max Only' ,
2936+ server : 'hy2.example.com' ,
2937+ port : 443 ,
2938+ password : 'secret' ,
2939+ obfs : 'gecko' ,
2940+ 'obfs-password' : 'mask' ,
2941+ 'obfs-max-packet-size' : 1300 ,
2942+ } ) ;
2943+
2944+ const minOnly = minOnlyOutput . outbounds . find (
2945+ ( outbound ) => outbound . tag === 'Gecko Min Only' ,
2946+ ) ;
2947+ const maxOnly = maxOnlyOutput . outbounds . find (
2948+ ( outbound ) => outbound . tag === 'Gecko Max Only' ,
2949+ ) ;
2950+
2951+ expectSubset ( minOnly , {
2952+ obfs : {
2953+ type : 'gecko' ,
2954+ password : 'mask' ,
2955+ min_packet_size : 600 ,
2956+ } ,
2957+ } ) ;
2958+ expect ( minOnly . obfs ) . to . not . have . property ( 'max_packet_size' ) ;
2959+
2960+ expectSubset ( maxOnly , {
2961+ obfs : {
2962+ type : 'gecko' ,
2963+ password : 'mask' ,
2964+ max_packet_size : 1300 ,
2965+ } ,
2966+ } ) ;
2967+ expect ( maxOnly . obfs ) . to . not . have . property ( 'min_packet_size' ) ;
2968+ } ) ;
2969+
2970+ it ( 'does not emit invalid gecko packet sizes for sing-box' , function ( ) {
2971+ const { result, errors } = captureErrors ( ( ) =>
2972+ produceInternal ( 'sing-box' , [
2973+ {
2974+ type : 'hysteria2' ,
2975+ name : 'Gecko Invalid Decimal' ,
2976+ server : 'hy2.example.com' ,
2977+ port : 443 ,
2978+ password : 'secret' ,
2979+ obfs : 'gecko' ,
2980+ 'obfs-password' : 'mask' ,
2981+ 'obfs-min-packet-size' : '600.5' ,
2982+ } ,
2983+ {
2984+ type : 'hysteria2' ,
2985+ name : 'Gecko Invalid Range' ,
2986+ server : 'hy2.example.com' ,
2987+ port : 443 ,
2988+ password : 'secret' ,
2989+ obfs : 'gecko' ,
2990+ 'obfs-password' : 'mask' ,
2991+ 'obfs-min-packet-size' : 1300 ,
2992+ } ,
2993+ ] ) ,
2994+ ) ;
2995+
2996+ expect ( errors ) . to . have . length ( 2 ) ;
2997+ expect ( errors [ 0 ] ) . to . include ( 'Gecko Invalid Decimal' ) ;
2998+ expect ( errors [ 1 ] ) . to . include ( 'Gecko Invalid Range' ) ;
2999+ for ( const hysteria2 of result ) {
3000+ expect ( hysteria2 . obfs ) . to . deep . equal ( {
3001+ type : 'gecko' ,
3002+ password : 'mask' ,
3003+ } ) ;
3004+ }
3005+ } ) ;
3006+
3007+ it ( 'clamps oversized gecko max packet size for sing-box and logs a warning' , function ( ) {
3008+ const { result, warnings } = captureWarns ( ( ) =>
3009+ produceInternal ( 'sing-box' , {
3010+ type : 'hysteria2' ,
3011+ name : 'Gecko Oversized Max' ,
3012+ server : 'hy2.example.com' ,
3013+ port : 443 ,
3014+ password : 'secret' ,
3015+ obfs : 'gecko' ,
3016+ 'obfs-password' : 'mask' ,
3017+ 'obfs-min-packet-size' : 1024 ,
3018+ 'obfs-max-packet-size' : 4096 ,
3019+ } ) ,
3020+ ) ;
3021+
3022+ expect ( warnings ) . to . have . length ( 1 ) ;
3023+ expect ( warnings [ 0 ] ) . to . include ( 'Gecko Oversized Max' ) ;
3024+ expect ( warnings [ 0 ] ) . to . include ( 'clamped to 2048' ) ;
3025+ expectSubset ( result [ 0 ] , {
3026+ obfs : {
3027+ type : 'gecko' ,
3028+ password : 'mask' ,
3029+ min_packet_size : 1024 ,
3030+ max_packet_size : 2048 ,
3031+ } ,
3032+ } ) ;
3033+ } ) ;
3034+
28583035 it ( 'emits WireGuard interface CIDR suffixes for Egern exports' , function ( ) {
28593036 const proxies = [
28603037 {
0 commit comments