mirror of https://github.com/cbeuw/Cloak
Compare commits
19 Commits
5a3a05593b
...
018994bfd1
Author | SHA1 | Date |
---|---|---|
Andy Wang | 018994bfd1 | 1 month ago |
Andy Wang | 0a6846fbfc | 1 month ago |
Andy Wang | f30141b388 | 1 month ago |
Andy Wang | 2aa49ce543 | 1 month ago |
Andy Wang | fe78c7b713 | 1 month ago |
Andy Wang | 4029763123 | 1 month ago |
Andy Wang | 896fd16938 | 1 month ago |
Andy Wang | 4fbf387bbf | 1 month ago |
Andy Wang | de1c7600c1 | 1 month ago |
Andy Wang | 767716b9be | 1 month ago |
renovate[bot] | 1cc4a1f928 | 1 month ago |
Andy Wang | 82687d4419 | 1 month ago |
Nikita | 6b08af0c18 | 2 months ago |
Andy Wang | c48a8800d6 | 3 months ago |
renovate[bot] | c5b31de753 | 3 months ago |
Andy Wang | b9907c2e18 | 3 months ago |
Andy Wang | 6417e3393d | 3 months ago |
Andy Wang | b3ec1ab3bc | 3 months ago |
Andy Wang | eca5f13936 | 6 months ago |
@ -1,5 +1,4 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 1%
|
||||
project: off
|
||||
patch: off
|
@ -1,17 +1,30 @@
|
||||
module github.com/cbeuw/Cloak
|
||||
|
||||
go 1.14
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/juju/ratelimit v1.0.1
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.6.1
|
||||
gitlab.com/yawning/utls.git v0.0.12-1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.1.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/refraction-networking/utls v1.6.4
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
golang.org/x/crypto v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
@ -1,76 +1,52 @@
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs=
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
||||
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/refraction-networking/utls v1.6.4 h1:aeynTroaYn7y+mFtqv8D0bQ4bw0y9nJHneGxJ7lvRDM=
|
||||
github.com/refraction-networking/utls v1.6.4/go.mod h1:2VL2xfiqgFAZtJKeUTlf+PSYFs3Eu7km0gCtXJ3m8zs=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
||||
gitlab.com/yawning/utls.git v0.0.12-1 h1:RL6O0MP2YI0KghuEU/uGN6+8b4183eqNWoYgx7CXD0U=
|
||||
gitlab.com/yawning/utls.git v0.0.12-1/go.mod h1:3ONKiSFR9Im/c3t5RKmMJTVdmZN496FNyk3mjrY1dyo=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -0,0 +1,154 @@
|
||||
package cli_client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/libcloak/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CLIConfig struct {
|
||||
client.Config
|
||||
|
||||
// LocalHost is the hostname or IP address to listen for incoming proxy client connections
|
||||
LocalHost string // jsonOptional
|
||||
// LocalPort is the port to listen for incomig proxy client connections
|
||||
LocalPort string // jsonOptional
|
||||
// AlternativeNames is a list of ServerName Cloak may randomly pick from for different sessions
|
||||
// Optional
|
||||
AlternativeNames []string
|
||||
// StreamTimeout is the duration, in seconds, for an incoming connection to be automatically closed after the last
|
||||
// piece of incoming data .
|
||||
// Optional, Defaults to 300
|
||||
StreamTimeout int
|
||||
}
|
||||
|
||||
// semi-colon separated value. This is for Android plugin options
|
||||
func ssvToJson(ssv string) (ret []byte) {
|
||||
elem := func(val string, lst []string) bool {
|
||||
for _, v := range lst {
|
||||
if val == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
unescape := func(s string) string {
|
||||
r := strings.Replace(s, `\\`, `\`, -1)
|
||||
r = strings.Replace(r, `\=`, `=`, -1)
|
||||
r = strings.Replace(r, `\;`, `;`, -1)
|
||||
return r
|
||||
}
|
||||
unquoted := []string{"NumConn", "StreamTimeout", "KeepAlive", "UDP"}
|
||||
lines := strings.Split(unescape(ssv), ";")
|
||||
ret = []byte("{")
|
||||
for _, ln := range lines {
|
||||
if ln == "" {
|
||||
break
|
||||
}
|
||||
sp := strings.SplitN(ln, "=", 2)
|
||||
if len(sp) < 2 {
|
||||
log.Errorf("Malformed config option: %v", ln)
|
||||
continue
|
||||
}
|
||||
key := sp[0]
|
||||
value := sp[1]
|
||||
if strings.HasPrefix(key, "AlternativeNames") {
|
||||
switch strings.Contains(value, ",") {
|
||||
case true:
|
||||
domains := strings.Split(value, ",")
|
||||
for index, domain := range domains {
|
||||
domains[index] = `"` + domain + `"`
|
||||
}
|
||||
value = strings.Join(domains, ",")
|
||||
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
|
||||
case false:
|
||||
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// JSON doesn't like quotation marks around int and bool
|
||||
// This is extremely ugly but it's still better than writing a tokeniser
|
||||
if elem(key, unquoted) {
|
||||
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
|
||||
} else {
|
||||
ret = append(ret, []byte(`"`+key+`":"`+value+`",`)...)
|
||||
}
|
||||
}
|
||||
ret = ret[:len(ret)-1] // remove the last comma
|
||||
ret = append(ret, '}')
|
||||
return ret
|
||||
}
|
||||
|
||||
func ParseConfig(conf string) (raw *CLIConfig, err error) {
|
||||
var content []byte
|
||||
// Checking if it's a path to json or a ssv string
|
||||
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
|
||||
content = ssvToJson(conf)
|
||||
} else {
|
||||
content, err = ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
raw = new(CLIConfig)
|
||||
err = json.Unmarshal(content, &raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type LocalConnConfig struct {
|
||||
LocalAddr string
|
||||
Timeout time.Duration
|
||||
MockDomainList []string
|
||||
Singleplex bool
|
||||
}
|
||||
|
||||
func (raw *CLIConfig) ProcessCLIConfig(worldState common.WorldState) (local LocalConnConfig, remote client.RemoteConnConfig, auth client.AuthInfo, err error) {
|
||||
remote, auth, err = raw.Config.Process(worldState)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if raw.AlternativeNames != nil && len(raw.AlternativeNames) > 0 {
|
||||
var filteredAlternativeNames []string
|
||||
for _, alternativeName := range raw.AlternativeNames {
|
||||
if len(alternativeName) > 0 {
|
||||
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
|
||||
}
|
||||
}
|
||||
local.MockDomainList = raw.AlternativeNames
|
||||
} else {
|
||||
local.MockDomainList = []string{}
|
||||
}
|
||||
|
||||
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
|
||||
|
||||
if raw.LocalHost == "" {
|
||||
err = fmt.Errorf("LocalHost cannot be empty")
|
||||
return
|
||||
}
|
||||
if raw.LocalPort == "" {
|
||||
err = fmt.Errorf("LocalPort cannot be empty")
|
||||
return
|
||||
}
|
||||
local.LocalAddr = net.JoinHostPort(raw.LocalHost, raw.LocalPort)
|
||||
// stream no write timeout
|
||||
if raw.StreamTimeout == 0 {
|
||||
local.Timeout = 300 * time.Second
|
||||
} else {
|
||||
local.Timeout = time.Duration(raw.StreamTimeout) * time.Second
|
||||
}
|
||||
|
||||
local.Singleplex = raw.NumConn != nil && *raw.NumConn == 0
|
||||
|
||||
return
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package cli_client
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/libcloak/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
ssv := "UID=iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=;PublicKey=IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=;" +
|
||||
"ServerName=www.bing.com;NumConn=4;MaskBrowser=chrome;ProxyMethod=shadowsocks;EncryptionMethod=plain"
|
||||
json := ssvToJson(ssv)
|
||||
expected := []byte(`{"UID":"iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=","PublicKey":"IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=","ServerName":"www.bing.com","NumConn":4,"MaskBrowser":"chrome","ProxyMethod":"shadowsocks","EncryptionMethod":"plain"}`)
|
||||
|
||||
t.Run("byte equality", func(t *testing.T) {
|
||||
assert.Equal(t, expected, json)
|
||||
})
|
||||
|
||||
t.Run("struct equality", func(t *testing.T) {
|
||||
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
|
||||
_, _ = tmpConfig.Write(expected)
|
||||
parsedFromSSV, err := ParseConfig(ssv)
|
||||
assert.NoError(t, err)
|
||||
parsedFromJson, err := ParseConfig(tmpConfig.Name())
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, parsedFromJson, parsedFromSSV)
|
||||
})
|
||||
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
|
||||
_, err := ParseConfig(tmpConfig.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessCLIConfig(t *testing.T) {
|
||||
config := CLIConfig{
|
||||
Config: client.Config{
|
||||
ServerName: "bbc.co.uk",
|
||||
// ProxyMethod is the name of the underlying proxy you wish
|
||||
// to connect to, as determined by your server. The value can
|
||||
// be any string whose UTF-8 ENCODED byte length is no greater than
|
||||
// 12 bytes
|
||||
ProxyMethod: "ssh",
|
||||
// UID is a 16-byte secret string unique to an authorised user
|
||||
// The same UID can be used by the same user for multiple Cloak connections
|
||||
UID: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
|
||||
// PublicKey is the 32-byte public Curve25519 ECDH key of your server
|
||||
PublicKey: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
|
||||
// RemoteHost is the Cloak server's hostname or IP address
|
||||
RemoteHost: "1.2.3.4",
|
||||
},
|
||||
LocalHost: "0.0.0.0",
|
||||
LocalPort: "1234",
|
||||
}
|
||||
|
||||
t.Run("Zero means singleplex", func(t *testing.T) {
|
||||
zero := 0
|
||||
config := config
|
||||
config.NumConn = &zero
|
||||
local, _, _, err := config.ProcessCLIConfig(common.RealWorldState)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, local.Singleplex)
|
||||
})
|
||||
|
||||
t.Run("Empty means no singleplex", func(t *testing.T) {
|
||||
config := config
|
||||
local, _, _, err := config.ProcessCLIConfig(common.RealWorldState)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, local.Singleplex)
|
||||
})
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const appDataMaxLength = 16401
|
||||
|
||||
type clientHelloFields struct {
|
||||
random []byte
|
||||
sessionId []byte
|
||||
x25519KeyShare []byte
|
||||
serverName string
|
||||
}
|
||||
|
||||
func decodeHex(s string) []byte {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type browser interface {
|
||||
composeClientHello(clientHelloFields) []byte
|
||||
}
|
||||
|
||||
func generateSNI(serverName string) []byte {
|
||||
serverNameListLength := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(serverNameListLength, uint16(len(serverName)+3))
|
||||
serverNameType := []byte{0x00} // host_name
|
||||
serverNameLength := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(serverNameLength, uint16(len(serverName)))
|
||||
ret := make([]byte, 2+1+2+len(serverName))
|
||||
copy(ret[0:2], serverNameListLength)
|
||||
copy(ret[2:3], serverNameType)
|
||||
copy(ret[3:5], serverNameLength)
|
||||
copy(ret[5:], serverName)
|
||||
return ret
|
||||
}
|
||||
|
||||
// addExtensionRecord, add type, length to extension data
|
||||
func addExtRec(typ []byte, data []byte) []byte {
|
||||
length := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(length, uint16(len(data)))
|
||||
ret := make([]byte, 2+2+len(data))
|
||||
copy(ret[0:2], typ)
|
||||
copy(ret[2:4], length)
|
||||
copy(ret[4:], data)
|
||||
return ret
|
||||
}
|
||||
|
||||
type DirectTLS struct {
|
||||
*common.TLSConn
|
||||
browser browser
|
||||
}
|
||||
|
||||
// NewClientTransport handles the TLS handshake for a given conn and returns the sessionKey
|
||||
// if the server proceed with Cloak authentication
|
||||
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
|
||||
payload, sharedSecret := makeAuthenticationPayload(authInfo)
|
||||
|
||||
// random is marshalled ephemeral pub key 32 bytes
|
||||
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
|
||||
fields := clientHelloFields{
|
||||
random: payload.randPubKey[:],
|
||||
sessionId: payload.ciphertextWithTag[0:32],
|
||||
x25519KeyShare: payload.ciphertextWithTag[32:64],
|
||||
serverName: authInfo.MockDomain,
|
||||
}
|
||||
chOnly := tls.browser.composeClientHello(fields)
|
||||
chWithRecordLayer := common.AddRecordLayer(chOnly, common.Handshake, common.VersionTLS11)
|
||||
_, err = rawConn.Write(chWithRecordLayer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Trace("client hello sent successfully")
|
||||
tls.TLSConn = common.NewTLSConn(rawConn)
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
log.Trace("waiting for ServerHello")
|
||||
_, err = tls.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encrypted := append(buf[6:38], buf[84:116]...)
|
||||
nonce := encrypted[0:12]
|
||||
ciphertextWithTag := encrypted[12:60]
|
||||
sessionKeySlice, err := common.AESGCMDecrypt(nonce, sharedSecret[:], ciphertextWithTag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
copy(sessionKey[:], sessionKeySlice)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
// ChangeCipherSpec and EncryptedCert (in the format of application data)
|
||||
_, err = tls.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return sessionKey, nil
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func htob(s string) []byte {
|
||||
b, _ := hex.DecodeString(s)
|
||||
return b
|
||||
}
|
||||
|
||||
func TestMakeServerName(t *testing.T) {
|
||||
type testingPair struct {
|
||||
serverName string
|
||||
target []byte
|
||||
}
|
||||
|
||||
pairs := []testingPair{
|
||||
{
|
||||
"www.google.com",
|
||||
htob("001100000e7777772e676f6f676c652e636f6d"),
|
||||
},
|
||||
{
|
||||
"www.gstatic.com",
|
||||
htob("001200000f7777772e677374617469632e636f6d"),
|
||||
},
|
||||
{
|
||||
"googleads.g.doubleclick.net",
|
||||
htob("001e00001b676f6f676c656164732e672e646f75626c65636c69636b2e6e6574"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range pairs {
|
||||
assert.Equal(t, p.target, generateSNI(p.serverName))
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
// Fingerprint of Chrome 112
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
type Chrome struct{}
|
||||
|
||||
func makeGREASE() []byte {
|
||||
// see https://tools.ietf.org/html/draft-davidben-tls-grease-01
|
||||
// This is exclusive to Chrome.
|
||||
var one [1]byte
|
||||
common.CryptoRandRead(one[:])
|
||||
sixteenth := one[0] % 16
|
||||
monoGREASE := sixteenth*16 + 0xA
|
||||
doubleGREASE := []byte{monoGREASE, monoGREASE}
|
||||
return doubleGREASE
|
||||
}
|
||||
|
||||
func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte {
|
||||
|
||||
makeSupportedGroups := func() []byte {
|
||||
suppGroupListLen := []byte{0x00, 0x08}
|
||||
ret := make([]byte, 2+8)
|
||||
copy(ret[0:2], suppGroupListLen)
|
||||
copy(ret[2:4], makeGREASE())
|
||||
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18})
|
||||
return ret
|
||||
}
|
||||
|
||||
makeKeyShare := func(hidden []byte) []byte {
|
||||
ret := make([]byte, 43)
|
||||
ret[0], ret[1] = 0x00, 0x29 // length 41
|
||||
copy(ret[2:4], makeGREASE())
|
||||
ret[4], ret[5] = 0x00, 0x01 // length 1
|
||||
ret[6] = 0x00
|
||||
ret[7], ret[8] = 0x00, 0x1d // group x25519
|
||||
ret[9], ret[10] = 0x00, 0x20 // length 32
|
||||
copy(ret[11:43], hidden)
|
||||
return ret
|
||||
}
|
||||
|
||||
shuffle := func(exts [][]byte) {
|
||||
var qword [8]byte
|
||||
common.CryptoRandRead(qword[:])
|
||||
seed := int64(binary.BigEndian.Uint64(qword[:]))
|
||||
source := rand.NewSource(seed)
|
||||
r := rand.New(source)
|
||||
r.Shuffle(len(exts), func(i, j int) { exts[i], exts[j] = exts[j], exts[i] })
|
||||
}
|
||||
|
||||
// extension length is always 403, and server name length is variable
|
||||
var ext [18][]byte
|
||||
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
|
||||
|
||||
// Start shufflable extensions: https://chromestatus.com/feature/5124606246518784
|
||||
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
||||
sniLen := len(ext[1])
|
||||
ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
|
||||
ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
|
||||
ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups
|
||||
ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
|
||||
ext[6] = addExtRec([]byte{0x00, 0x23}, nil) // Session tickets
|
||||
ext[7] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
||||
ext[8] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
||||
ext[9] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001004030804040105030805050108060601")) // Signature Algorithms
|
||||
ext[10] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
|
||||
ext[11] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
|
||||
ext[12] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
|
||||
suppVersions := decodeHex("069A9A03040303") // 9A9A needs to be a GREASE
|
||||
copy(suppVersions[1:3], makeGREASE())
|
||||
ext[13] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
|
||||
ext[14] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x02}) // compress certificate
|
||||
ext[15] = addExtRec([]byte{0x44, 0x69}, decodeHex("0003026832")) // application settings
|
||||
// End shufflable extensions
|
||||
|
||||
shuffle(ext[1:16])
|
||||
|
||||
ext[16] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
|
||||
// sniLen + len(all other ext) + len(ext[17]) = 403
|
||||
// len(all other ext) = 175
|
||||
// len(ext[17]) = 228 - sniLen
|
||||
// 2+2+len(padding) = 228 - sniLen
|
||||
// len(padding) = 224 - sniLen
|
||||
ext[17] = addExtRec([]byte{0x00, 0x15}, make([]byte, 224-sniLen)) // padding
|
||||
var ret []byte
|
||||
for _, e := range ext {
|
||||
ret = append(ret, e...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Chrome) composeClientHello(hd clientHelloFields) (ch []byte) {
|
||||
var clientHello [12][]byte
|
||||
clientHello[0] = []byte{0x01} // handshake type
|
||||
clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508
|
||||
clientHello[2] = []byte{0x03, 0x03} // client version
|
||||
clientHello[3] = hd.random // random
|
||||
clientHello[4] = []byte{0x20} // session id length 32
|
||||
clientHello[5] = hd.sessionId // session id
|
||||
clientHello[6] = []byte{0x00, 0x20} // cipher suites length 32
|
||||
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035")...) // cipher suites
|
||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
||||
clientHello[9] = []byte{0x00} // compression methods
|
||||
|
||||
extensions := c.composeExtensions(hd.serverName, hd.x25519KeyShare)
|
||||
clientHello[10] = []byte{0x00, 0x00}
|
||||
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
|
||||
clientHello[11] = extensions
|
||||
|
||||
var ret []byte
|
||||
for _, c := range clientHello {
|
||||
ret = append(ret, c...)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMakeGREASE(t *testing.T) {
|
||||
a := hex.EncodeToString(makeGREASE())
|
||||
if a[1] != 'a' || a[3] != 'a' {
|
||||
t.Errorf("GREASE got %v", a)
|
||||
}
|
||||
|
||||
var GREASEs []string
|
||||
for i := 0; i < 50; i++ {
|
||||
GREASEs = append(GREASEs, hex.EncodeToString(makeGREASE()))
|
||||
}
|
||||
var eqCount int
|
||||
for _, g := range GREASEs {
|
||||
if a == g {
|
||||
eqCount++
|
||||
}
|
||||
}
|
||||
if eqCount > 40 {
|
||||
t.Error("GREASE is not random", GREASEs)
|
||||
}
|
||||
}
|
||||
|
||||
//func TestChromeJA3(t *testing.T) {
|
||||
// result := common.AddRecordLayer((&Chrome{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
|
||||
// assert.Equal(t, 517, len(result))
|
||||
//
|
||||
// hello := tlsx.ClientHelloBasic{}
|
||||
// err := hello.Unmarshal(result)
|
||||
// assert.Nil(t, err)
|
||||
//
|
||||
// // Chrome shuffles the order of extensions, so it needs special handling
|
||||
// full := string(ja3.Bare(&hello))
|
||||
// // TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
|
||||
// parts := strings.Split(full, ",")
|
||||
//
|
||||
// // TLSVersion,Ciphers
|
||||
// assert.Equal(t,
|
||||
// []string{
|
||||
// "771",
|
||||
// "4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53",
|
||||
// }, parts[0:2])
|
||||
// // EllipticCurves,EllipticCurvePointFormats
|
||||
// assert.Equal(t,
|
||||
// []string{
|
||||
// "29-23-24", "0",
|
||||
// }, parts[3:5])
|
||||
//
|
||||
// normaliseExtensions := func(extensions string) []string {
|
||||
// extensionParts := strings.Split(parts[2], "-")
|
||||
// sort.Strings(extensionParts)
|
||||
// return extensionParts
|
||||
// }
|
||||
// assert.Equal(t, normaliseExtensions("10-5-45-0-17513-13-18-11-23-16-35-27-65281-43-51-21"), normaliseExtensions(parts[2]))
|
||||
//}
|
@ -1,73 +0,0 @@
|
||||
// Fingerprint of Firefox 112
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
)
|
||||
|
||||
type Firefox struct{}
|
||||
|
||||
func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte {
|
||||
composeKeyShare := func(hidden []byte) []byte {
|
||||
ret := make([]byte, 107)
|
||||
ret[0], ret[1] = 0x00, 0x69 // length 105
|
||||
ret[2], ret[3] = 0x00, 0x1d // group x25519
|
||||
ret[4], ret[5] = 0x00, 0x20 // length 32
|
||||
copy(ret[6:38], hidden)
|
||||
ret[38], ret[39] = 0x00, 0x17 // group secp256r1
|
||||
ret[40], ret[41] = 0x00, 0x41 // length 65
|
||||
common.CryptoRandRead(ret[42:107])
|
||||
return ret
|
||||
}
|
||||
// extension length is always 401, and server name length is variable
|
||||
var ext [13][]byte
|
||||
ext[0] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
||||
ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
|
||||
ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
|
||||
ext[3] = addExtRec([]byte{0x00, 0x0a}, decodeHex("000c001d00170018001901000101")) // supported groups
|
||||
ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
|
||||
ext[5] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
||||
ext[6] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
||||
ext[7] = addExtRec([]byte{0x00, 0x22}, decodeHex("00080403050306030203")) // delegated credentials
|
||||
ext[8] = addExtRec([]byte{0x00, 0x33}, composeKeyShare(keyShare)) // key share
|
||||
ext[9] = addExtRec([]byte{0x00, 0x2b}, decodeHex("0403040303")) // supported versions
|
||||
ext[10] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030503060308040805080604010501060102030201")) // Signature Algorithms
|
||||
ext[11] = addExtRec([]byte{0x00, 0x1c}, []byte{0x40, 0x01}) // record size limit
|
||||
// len(ext[0]) + len(all other ext) + len(len field of padding) + len(padding) = 401
|
||||
// len(all other ext) = 228
|
||||
// len(len field of padding) = 4
|
||||
// len(padding) = 169 - len(ext[0])
|
||||
ext[12] = addExtRec([]byte{0x00, 0x15}, make([]byte, 169-len(ext[0]))) // padding
|
||||
var ret []byte
|
||||
for _, e := range ext {
|
||||
ret = append(ret, e...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *Firefox) composeClientHello(hd clientHelloFields) (ch []byte) {
|
||||
var clientHello [12][]byte
|
||||
clientHello[0] = []byte{0x01} // handshake type
|
||||
clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508
|
||||
clientHello[2] = []byte{0x03, 0x03} // client version
|
||||
clientHello[3] = hd.random // random
|
||||
clientHello[4] = []byte{0x20} // session id length 32
|
||||
clientHello[5] = hd.sessionId // session id
|
||||
clientHello[6] = []byte{0x00, 0x22} // cipher suites length 34
|
||||
clientHello[7] = decodeHex("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035") // cipher suites
|
||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
||||
clientHello[9] = []byte{0x00} // compression methods
|
||||
|
||||
extensions := f.composeExtensions(hd.serverName, hd.x25519KeyShare)
|
||||
clientHello[10] = []byte{0x00, 0x00}
|
||||
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
|
||||
clientHello[11] = extensions
|
||||
|
||||
var ret []byte
|
||||
for _, c := range clientHello {
|
||||
ret = append(ret, c...)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hd = clientHelloFields{
|
||||
random: decodeHex("ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a0"),
|
||||
sessionId: decodeHex("47485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b9"),
|
||||
x25519KeyShare: decodeHex("d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa44"),
|
||||
serverName: "github.com",
|
||||
}
|
||||
|
||||
//func TestFirefoxJA3(t *testing.T) {
|
||||
// result := common.AddRecordLayer((&Firefox{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
|
||||
//
|
||||
// hello := tlsx.ClientHelloBasic{}
|
||||
// err := hello.Unmarshal(result)
|
||||
// assert.Nil(t, err)
|
||||
//
|
||||
// digest := ja3.DigestHex(&hello)
|
||||
// assert.Equal(t, "ad55557b7cbd735c2627f7ebb3b3d493", digest)
|
||||
//}
|
||||
|
||||
func TestFirefoxComposeClientHello(t *testing.T) {
|
||||
result := hex.EncodeToString((&Firefox{}).composeClientHello(hd))
|
||||
target := "010001fc0303ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a02047485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b90022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035010001910000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b000201000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d0020d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa440017004104c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47002b00050403040303000d0018001604030503060308040805080604010501060102030201001c0002400100150096000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// skip random secp256r1
|
||||
secp256r1 := "04c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47"
|
||||
start := strings.Index(target, secp256r1)
|
||||
|
||||
target = strings.Replace(target, secp256r1, "", 1)
|
||||
result = strings.Replace(result, result[start:start+len(secp256r1)], "", 1)
|
||||
|
||||
assert.Equal(t, target, result)
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
// Fingerprint of Safari 16.4
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type Safari struct{}
|
||||
|
||||
func (s *Safari) composeExtensions(serverName string, keyShare []byte) []byte {
|
||||
makeSupportedGroups := func() []byte {
|
||||
suppGroupListLen := []byte{0x00, 0x0a}
|
||||
ret := make([]byte, 2+2+8)
|
||||
copy(ret[0:2], suppGroupListLen)
|
||||
copy(ret[2:4], makeGREASE())
|
||||
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19})
|
||||
return ret
|
||||
}
|
||||
|
||||
makeKeyShare := func(hidden []byte) []byte {
|
||||
ret := make([]byte, 43)
|
||||
ret[0], ret[1] = 0x00, 0x29 // length 41
|
||||
copy(ret[2:4], makeGREASE())
|
||||
ret[4], ret[5] = 0x00, 0x01 // length 1
|
||||
ret[6] = 0x00
|
||||
ret[7], ret[8] = 0x00, 0x1d // group x25519
|
||||
ret[9], ret[10] = 0x00, 0x20 // length 32
|
||||
copy(ret[11:43], hidden)
|
||||
return ret
|
||||
}
|
||||
|
||||
// extension length is always 393, and server name length is variable
|
||||
var ext [16][]byte
|
||||
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
|
||||
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
||||
ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
|
||||
ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
|
||||
ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups
|
||||
ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
|
||||
ext[6] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
||||
ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
||||
ext[8] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030804040105030203080508050501080606010201")) // Signature Algorithms
|
||||
ext[9] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
|
||||
ext[10] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
|
||||
ext[11] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
|
||||
suppVersions := decodeHex("0a5a5a0304030303020301") // 5a5a needs to be a GREASE
|
||||
copy(suppVersions[1:3], makeGREASE())
|
||||
ext[12] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
|
||||
ext[13] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x01}) // compress certificate
|
||||
ext[14] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
|
||||
// len(ext[1]) + len(all other ext) + len(ext[15]) = 393
|
||||
// len(all other ext) = 174
|
||||
// len(ext[15]) = 219 - len(ext[1])
|
||||
// 2+2+len(padding) = 219 - len(ext[1])
|
||||
// len(padding) = 215 - len(ext[1])
|
||||
ext[15] = addExtRec([]byte{0x00, 0x15}, make([]byte, 215-len(ext[1]))) // padding
|
||||
var ret []byte
|
||||
for _, e := range ext {
|
||||
ret = append(ret, e...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Safari) composeClientHello(hd clientHelloFields) (ch []byte) {
|
||||
var clientHello [12][]byte
|
||||
clientHello[0] = []byte{0x01} // handshake type
|
||||
clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508
|
||||
clientHello[2] = []byte{0x03, 0x03} // client version
|
||||
clientHello[3] = hd.random // random
|
||||
clientHello[4] = []byte{0x20} // session id length 32
|
||||
clientHello[5] = hd.sessionId // session id
|
||||
clientHello[6] = []byte{0x00, 0x2a} // cipher suites length 42
|
||||
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a")...) // cipher suites
|
||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
||||
clientHello[9] = []byte{0x00} // compression methods
|
||||
|
||||
extensions := s.composeExtensions(hd.serverName, hd.x25519KeyShare)
|
||||
clientHello[10] = []byte{0x00, 0x00}
|
||||
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
|
||||
clientHello[11] = extensions
|
||||
|
||||
var ret []byte
|
||||
for _, c := range clientHello {
|
||||
ret = append(ret, c...)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var safariHd = clientHelloFields{
|
||||
random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"),
|
||||
sessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"),
|
||||
x25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"),
|
||||
serverName: "github.com",
|
||||
}
|
||||
|
||||
//func TestSafariJA3(t *testing.T) {
|
||||
// result := common.AddRecordLayer((&Safari{}).composeClientHello(safariHd), common.Handshake, common.VersionTLS11)
|
||||
//
|
||||
// hello := tlsx.ClientHelloBasic{}
|
||||
// err := hello.Unmarshal(result)
|
||||
// assert.Nil(t, err)
|
||||
//
|
||||
// digest := ja3.DigestHex(&hello)
|
||||
// assert.Equal(t, "773906b0efdefa24a7f2b8eb6985bf37", digest)
|
||||
//}
|
||||
|
||||
func TestSafariComposeClientHello(t *testing.T) {
|
||||
result := (&Safari{}).composeClientHello(safariHd)
|
||||
target := decodeHex("010001fc0303977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc1720c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3002acaca130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000189fafa00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000c000a7a7a001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d0018001604030804040105030203080508050501080606010201001200000033002b00297a7a000100001d0020c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a002d00020101002b000b0a2a2a0304030303020301001b00030200017a7a000100001500c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||
for p := 0; p < len(result); p++ {
|
||||
if result[p] != target[p] {
|
||||
if result[p]&0x0F == 0xA && target[p]&0x0F == 0xA &&
|
||||
((p > 0 && result[p-1] == result[p] && target[p-1] == target[p]) ||
|
||||
(p < len(result)-1 && result[p+1] == result[p] && target[p+1] == target[p])) {
|
||||
continue
|
||||
}
|
||||
t.Errorf("inequality at %v", p)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,281 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
)
|
||||
|
||||
// RawConfig represents the fields in the config json file
|
||||
// nullable means if it's empty, a default value will be chosen in ProcessRawConfig
|
||||
// jsonOptional means if the json's empty, its value will be set from environment variables or commandline args
|
||||
// but it mustn't be empty when ProcessRawConfig is called
|
||||
type RawConfig struct {
|
||||
ServerName string
|
||||
ProxyMethod string
|
||||
EncryptionMethod string
|
||||
UID []byte
|
||||
PublicKey []byte
|
||||
NumConn int
|
||||
LocalHost string // jsonOptional
|
||||
LocalPort string // jsonOptional
|
||||
RemoteHost string // jsonOptional
|
||||
RemotePort string // jsonOptional
|
||||
AlternativeNames []string // jsonOptional
|
||||
// defaults set in ProcessRawConfig
|
||||
UDP bool // nullable
|
||||
BrowserSig string // nullable
|
||||
Transport string // nullable
|
||||
CDNOriginHost string // nullable
|
||||
CDNWsUrlPath string // nullable
|
||||
StreamTimeout int // nullable
|
||||
KeepAlive int // nullable
|
||||
}
|
||||
|
||||
type RemoteConnConfig struct {
|
||||
Singleplex bool
|
||||
NumConn int
|
||||
KeepAlive time.Duration
|
||||
RemoteAddr string
|
||||
TransportMaker func() Transport
|
||||
}
|
||||
|
||||
type LocalConnConfig struct {
|
||||
LocalAddr string
|
||||
Timeout time.Duration
|
||||
MockDomainList []string
|
||||
}
|
||||
|
||||
type AuthInfo struct {
|
||||
UID []byte
|
||||
SessionId uint32
|
||||
ProxyMethod string
|
||||
EncryptionMethod byte
|
||||
Unordered bool
|
||||
ServerPubKey crypto.PublicKey
|
||||
MockDomain string
|
||||
WorldState common.WorldState
|
||||
}
|
||||
|
||||
// semi-colon separated value. This is for Android plugin options
|
||||
func ssvToJson(ssv string) (ret []byte) {
|
||||
elem := func(val string, lst []string) bool {
|
||||
for _, v := range lst {
|
||||
if val == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
unescape := func(s string) string {
|
||||
r := strings.Replace(s, `\\`, `\`, -1)
|
||||
r = strings.Replace(r, `\=`, `=`, -1)
|
||||
r = strings.Replace(r, `\;`, `;`, -1)
|
||||
return r
|
||||
}
|
||||
unquoted := []string{"NumConn", "StreamTimeout", "KeepAlive", "UDP"}
|
||||
lines := strings.Split(unescape(ssv), ";")
|
||||
ret = []byte("{")
|
||||
for _, ln := range lines {
|
||||
if ln == "" {
|
||||
break
|
||||
}
|
||||
sp := strings.SplitN(ln, "=", 2)
|
||||
if len(sp) < 2 {
|
||||
log.Errorf("Malformed config option: %v", ln)
|
||||
continue
|
||||
}
|
||||
key := sp[0]
|
||||
value := sp[1]
|
||||
if strings.HasPrefix(key, "AlternativeNames") {
|
||||
switch strings.Contains(value, ",") {
|
||||
case true:
|
||||
domains := strings.Split(value, ",")
|
||||
for index, domain := range domains {
|
||||
domains[index] = `"` + domain + `"`
|
||||
}
|
||||
value = strings.Join(domains, ",")
|
||||
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
|
||||
case false:
|
||||
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// JSON doesn't like quotation marks around int and bool
|
||||
// This is extremely ugly but it's still better than writing a tokeniser
|
||||
if elem(key, unquoted) {
|
||||
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
|
||||
} else {
|
||||
ret = append(ret, []byte(`"`+key+`":"`+value+`",`)...)
|
||||
}
|
||||
}
|
||||
ret = ret[:len(ret)-1] // remove the last comma
|
||||
ret = append(ret, '}')
|
||||
return ret
|
||||
}
|
||||
|
||||
func ParseConfig(conf string) (raw *RawConfig, err error) {
|
||||
var content []byte
|
||||
// Checking if it's a path to json or a ssv string
|
||||
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
|
||||
content = ssvToJson(conf)
|
||||
} else {
|
||||
content, err = ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
raw = new(RawConfig)
|
||||
err = json.Unmarshal(content, &raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local LocalConnConfig, remote RemoteConnConfig, auth AuthInfo, err error) {
|
||||
nullErr := func(field string) (local LocalConnConfig, remote RemoteConnConfig, auth AuthInfo, err error) {
|
||||
err = fmt.Errorf("%v cannot be empty", field)
|
||||
return
|
||||
}
|
||||
|
||||
auth.UID = raw.UID
|
||||
auth.Unordered = raw.UDP
|
||||
if raw.ServerName == "" {
|
||||
return nullErr("ServerName")
|
||||
}
|
||||
auth.MockDomain = raw.ServerName
|
||||
|
||||
var filteredAlternativeNames []string
|
||||
for _, alternativeName := range raw.AlternativeNames {
|
||||
if len(alternativeName) > 0 {
|
||||
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
|
||||
}
|
||||
}
|
||||
raw.AlternativeNames = filteredAlternativeNames
|
||||
|
||||
local.MockDomainList = raw.AlternativeNames
|
||||
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
|
||||
if raw.ProxyMethod == "" {
|
||||
return nullErr("ServerName")
|
||||
}
|
||||
auth.ProxyMethod = raw.ProxyMethod
|
||||
if len(raw.UID) == 0 {
|
||||
return nullErr("UID")
|
||||
}
|
||||
|
||||
// static public key
|
||||
if len(raw.PublicKey) == 0 {
|
||||
return nullErr("PublicKey")
|
||||
}
|
||||
pub, ok := ecdh.Unmarshal(raw.PublicKey)
|
||||
if !ok {
|
||||
err = fmt.Errorf("failed to unmarshal Public key")
|
||||
return
|
||||
}
|
||||
auth.ServerPubKey = pub
|
||||
auth.WorldState = worldState
|
||||
|
||||
// Encryption method
|
||||
switch strings.ToLower(raw.EncryptionMethod) {
|
||||
case "plain":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodPlain
|
||||
case "aes-gcm", "aes-256-gcm":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
|
||||
case "aes-128-gcm":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodAES128GCM
|
||||
case "chacha20-poly1305":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodChaha20Poly1305
|
||||
default:
|
||||
err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod)
|
||||
return
|
||||
}
|
||||
|
||||
if raw.RemoteHost == "" {
|
||||
return nullErr("RemoteHost")
|
||||
}
|
||||
if raw.RemotePort == "" {
|
||||
return nullErr("RemotePort")
|
||||
}
|
||||
remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, raw.RemotePort)
|
||||
if raw.NumConn <= 0 {
|
||||
remote.NumConn = 1
|
||||
remote.Singleplex = true
|
||||
} else {
|
||||
remote.NumConn = raw.NumConn
|
||||
remote.Singleplex = false
|
||||
}
|
||||
|
||||
// Transport and (if TLS mode), browser
|
||||
switch strings.ToLower(raw.Transport) {
|
||||
case "cdn":
|
||||
var cdnDomainPort string
|
||||
if raw.CDNOriginHost == "" {
|
||||
cdnDomainPort = net.JoinHostPort(raw.RemoteHost, raw.RemotePort)
|
||||
} else {
|
||||
cdnDomainPort = net.JoinHostPort(raw.CDNOriginHost, raw.RemotePort)
|
||||
}
|
||||
if raw.CDNWsUrlPath == "" {
|
||||
raw.CDNWsUrlPath = "/"
|
||||
}
|
||||
|
||||
remote.TransportMaker = func() Transport {
|
||||
return &WSOverTLS{
|
||||
wsUrl: "ws://" + cdnDomainPort + raw.CDNWsUrlPath,
|
||||
}
|
||||
}
|
||||
case "direct":
|
||||
fallthrough
|
||||
default:
|
||||
var browser browser
|
||||
switch strings.ToLower(raw.BrowserSig) {
|
||||
case "firefox":
|
||||
browser = &Firefox{}
|
||||
case "safari":
|
||||
browser = &Safari{}
|
||||
case "chrome":
|
||||
fallthrough
|
||||
default:
|
||||
browser = &Chrome{}
|
||||
}
|
||||
remote.TransportMaker = func() Transport {
|
||||
return &DirectTLS{
|
||||
browser: browser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KeepAlive
|
||||
if raw.KeepAlive <= 0 {
|
||||
remote.KeepAlive = -1
|
||||
} else {
|
||||
remote.KeepAlive = remote.KeepAlive * time.Second
|
||||
}
|
||||
|
||||
if raw.LocalHost == "" {
|
||||
return nullErr("LocalHost")
|
||||
}
|
||||
if raw.LocalPort == "" {
|
||||
return nullErr("LocalPort")
|
||||
}
|
||||
local.LocalAddr = net.JoinHostPort(raw.LocalHost, raw.LocalPort)
|
||||
// stream no write timeout
|
||||
if raw.StreamTimeout == 0 {
|
||||
local.Timeout = 300 * time.Second
|
||||
} else {
|
||||
local.Timeout = time.Duration(raw.StreamTimeout) * time.Second
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
ssv := "UID=iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=;PublicKey=IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=;" +
|
||||
"ServerName=www.bing.com;NumConn=4;MaskBrowser=chrome;ProxyMethod=shadowsocks;EncryptionMethod=plain"
|
||||
json := ssvToJson(ssv)
|
||||
expected := []byte(`{"UID":"iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=","PublicKey":"IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=","ServerName":"www.bing.com","NumConn":4,"MaskBrowser":"chrome","ProxyMethod":"shadowsocks","EncryptionMethod":"plain"}`)
|
||||
|
||||
t.Run("byte equality", func(t *testing.T) {
|
||||
assert.Equal(t, expected, json)
|
||||
})
|
||||
|
||||
t.Run("struct equality", func(t *testing.T) {
|
||||
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
|
||||
_, _ = tmpConfig.Write(expected)
|
||||
parsedFromSSV, err := ParseConfig(ssv)
|
||||
assert.NoError(t, err)
|
||||
parsedFromJson, err := ParseConfig(tmpConfig.Name())
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, parsedFromJson, parsedFromSSV)
|
||||
})
|
||||
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
|
||||
_, err := ParseConfig(tmpConfig.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type Transport interface {
|
||||
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
|
||||
net.Conn
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/libcloak/client/transports"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
)
|
||||
|
||||
// Config contains the configuration parameter fields for a Cloak client
|
||||
type Config struct {
|
||||
// Required fields
|
||||
// ServerName is the domain you appear to be visiting
|
||||
// to your Firewall or ISP
|
||||
ServerName string
|
||||
// ProxyMethod is the name of the underlying proxy you wish
|
||||
// to connect to, as determined by your server. The value can
|
||||
// be any string whose UTF-8 ENCODED byte length is no greater than
|
||||
// 12 bytes
|
||||
ProxyMethod string
|
||||
// UID is a 16-byte secret string unique to an authorised user
|
||||
// The same UID can be used by the same user for multiple Cloak connections
|
||||
UID []byte
|
||||
// PublicKey is the 32-byte public Curve25519 ECDH key of your server
|
||||
PublicKey []byte
|
||||
// RemoteHost is the Cloak server's hostname or IP address
|
||||
RemoteHost string
|
||||
|
||||
// Optional Fields
|
||||
// EncryptionMethod is the cryptographic algorithm used to
|
||||
// encrypt data on the wire.
|
||||
// Valid values are `aes-128-gcm`, `aes-256-gcm`, `chacha20-poly1305`, and `plain`
|
||||
// Defaults to `aes-256-gcm`
|
||||
EncryptionMethod string
|
||||
// NumConn is the amount of underlying TLS connections to establish with Cloak server.
|
||||
// Cloak multiplexes any number of incoming connections to a fixed number of underlying TLS connections.
|
||||
// If set to 0, a special singleplex mode is enabled: each incoming connection will correspond to exactly one
|
||||
// TLS connection
|
||||
// Defaults to 4
|
||||
NumConn *int
|
||||
// UDP enables UDP semantics, where packets must fit into one unit of message (below 16000 bytes by default),
|
||||
// and packets can be received out of order. Though reliable delivery is still guaranteed.
|
||||
UDP bool
|
||||
// BrowserSig is the browser signature to be used. Options are `chrome` and `firefox`
|
||||
// Defaults to `chrome`
|
||||
BrowserSig string
|
||||
// Transport is either `direct` or `cdn`. Under `direct`, the client connects to a Cloak server directly.
|
||||
// Under `cdn`, the client connects to a CDN provider such as Amazon Cloudfront, which in turn connects
|
||||
// to a Cloak server.
|
||||
// Defaults to `direct`
|
||||
Transport string
|
||||
// CDNOriginHost is the CDN Origin's (i.e. Cloak server) real hostname or IP address, which is encrypted between
|
||||
// the client and the CDN server, and therefore hidden to ISP or firewalls. This only has effect when Transport
|
||||
// is set to `cdn`
|
||||
// Defaults to RemoteHost
|
||||
CDNOriginHost string
|
||||
// KeepAlive is the interval between TCP KeepAlive packets to be sent over the underlying TLS connections
|
||||
// Defaults to -1, which means no TCP KeepAlive is ever sent
|
||||
KeepAlive int
|
||||
// RemotePort is the port Cloak server is listening to
|
||||
// Defaults to 443
|
||||
RemotePort string
|
||||
// InactivityTimeout is the number of seconds the client keeps the underlying connections to the server
|
||||
// after the last proxy connection is disconnected.
|
||||
// Defaults to 30. Always set to 0 under Singleplex mode (NumConn == 0)
|
||||
InactivityTimeout *int
|
||||
}
|
||||
|
||||
type RemoteConnConfig struct {
|
||||
NumConn int
|
||||
KeepAlive time.Duration
|
||||
RemoteAddr string
|
||||
TransportMaker func() transports.Transport
|
||||
InactivityTimeout time.Duration
|
||||
}
|
||||
|
||||
type AuthInfo = transports.AuthInfo
|
||||
|
||||
func (raw *Config) Process(worldState common.WorldState) (remote RemoteConnConfig, auth AuthInfo, err error) {
|
||||
if raw.ServerName == "" {
|
||||
err = fmt.Errorf("ServerName cannot be empty")
|
||||
return
|
||||
}
|
||||
if raw.ProxyMethod == "" {
|
||||
err = fmt.Errorf("ProxyMethod cannot be empty")
|
||||
return
|
||||
}
|
||||
if len(raw.UID) == 0 {
|
||||
err = fmt.Errorf("UID cannot be empty")
|
||||
return
|
||||
}
|
||||
if len(raw.PublicKey) == 0 {
|
||||
err = fmt.Errorf("PublicKey cannot be empty")
|
||||
return
|
||||
}
|
||||
if raw.RemoteHost == "" {
|
||||
err = fmt.Errorf("RemoteHost cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
auth.UID = raw.UID
|
||||
auth.Unordered = raw.UDP
|
||||
auth.MockDomain = raw.ServerName
|
||||
auth.ProxyMethod = raw.ProxyMethod
|
||||
auth.WorldState = worldState
|
||||
|
||||
// static public key
|
||||
pub, ok := ecdh.Unmarshal(raw.PublicKey)
|
||||
if !ok {
|
||||
err = fmt.Errorf("failed to unmarshal Public key")
|
||||
return
|
||||
}
|
||||
auth.ServerPubKey = pub
|
||||
|
||||
// Encryption method
|
||||
switch strings.ToLower(raw.EncryptionMethod) {
|
||||
case "plain":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodPlain
|
||||
case "aes-gcm", "aes-256-gcm", "":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
|
||||
case "aes-128-gcm":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodAES128GCM
|
||||
case "chacha20-poly1305":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodChaha20Poly1305
|
||||
default:
|
||||
err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod)
|
||||
return
|
||||
}
|
||||
|
||||
var remotePort string
|
||||
if raw.RemotePort == "" {
|
||||
remotePort = "443"
|
||||
} else {
|
||||
remotePort = raw.RemotePort
|
||||
}
|
||||
remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, remotePort)
|
||||
|
||||
if raw.InactivityTimeout == nil {
|
||||
remote.InactivityTimeout = 30 * time.Second
|
||||
} else {
|
||||
remote.InactivityTimeout = time.Duration(*raw.InactivityTimeout) * time.Second
|
||||
}
|
||||
|
||||
if raw.NumConn == nil {
|
||||
remote.NumConn = 4
|
||||
} else if *raw.NumConn <= 0 {
|
||||
remote.NumConn = 1
|
||||
remote.InactivityTimeout = 0
|
||||
} else {
|
||||
remote.NumConn = *raw.NumConn
|
||||
}
|
||||
|
||||
// Transport and (if TLS mode), browser
|
||||
switch strings.ToLower(raw.Transport) {
|
||||
case "cdn":
|
||||
cdnPort := raw.RemotePort
|
||||
var cdnHost string
|
||||
if raw.CDNOriginHost == "" {
|
||||
cdnHost = raw.RemoteHost
|
||||
} else {
|
||||
cdnHost = raw.CDNOriginHost
|
||||
}
|
||||
|
||||
remote.TransportMaker = func() transports.Transport {
|
||||
return &transports.WSOverTLS{
|
||||
CDNHost: cdnHost,
|
||||
CDNPort: cdnPort,
|
||||
}
|
||||
}
|
||||
case "direct", "":
|
||||
var browser transports.Browser
|
||||
switch strings.ToLower(raw.BrowserSig) {
|
||||
case "firefox":
|
||||
browser = transports.Firefox
|
||||
case "safari":
|
||||
browser = transports.Safari
|
||||
case "chrome", "":
|
||||
browser = transports.Chrome
|
||||
default:
|
||||
err = fmt.Errorf("unknown browser signature %v", raw.BrowserSig)
|
||||
return
|
||||
}
|
||||
remote.TransportMaker = func() transports.Transport {
|
||||
return &transports.DirectTLS{
|
||||
Browser: browser,
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unknown transport %v", raw.Transport)
|
||||
return
|
||||
}
|
||||
|
||||
// KeepAlive
|
||||
if raw.KeepAlive <= 0 {
|
||||
remote.KeepAlive = -1
|
||||
} else {
|
||||
remote.KeepAlive = remote.KeepAlive * time.Second
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var baseConfig = Config{
|
||||
ServerName: "www.bing.com",
|
||||
ProxyMethod: "ssh",
|
||||
UID: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf},
|
||||
PublicKey: make([]byte, 32),
|
||||
RemoteHost: "12.34.56.78",
|
||||
}
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
remote, auth, err := baseConfig.Process(common.RealWorldState)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 4, remote.NumConn)
|
||||
assert.EqualValues(t, mux.EncryptionMethodAES256GCM, auth.EncryptionMethod)
|
||||
assert.EqualValues(t, -1, remote.KeepAlive)
|
||||
assert.False(t, auth.Unordered)
|
||||
}
|
||||
|
||||
func TestValidation(t *testing.T) {
|
||||
_, _, err := baseConfig.Process(common.RealWorldState)
|
||||
assert.NoError(t, err)
|
||||
|
||||
type test struct {
|
||||
fieldToChange string
|
||||
newValue any
|
||||
errPattern string
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{
|
||||
fieldToChange: "ServerName",
|
||||
newValue: "",
|
||||
errPattern: "empty",
|
||||
},
|
||||
{
|
||||
fieldToChange: "UID",
|
||||
newValue: []byte{},
|
||||
errPattern: "empty",
|
||||
},
|
||||
{
|
||||
fieldToChange: "PublicKey",
|
||||
newValue: []byte{0x1},
|
||||
errPattern: "unmarshal",
|
||||
},
|
||||
{
|
||||
fieldToChange: "RemoteHost",
|
||||
newValue: "",
|
||||
errPattern: "empty",
|
||||
},
|
||||
{
|
||||
fieldToChange: "BrowserSig",
|
||||
newValue: "not-a-browser",
|
||||
errPattern: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
config := baseConfig
|
||||
reflect.ValueOf(&config).Elem().FieldByName(test.fieldToChange).Set(reflect.ValueOf(test.newValue))
|
||||
_, _, err := config.Process(common.RealWorldState)
|
||||
assert.ErrorContains(t, err, test.errPattern)
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package transports
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
)
|
||||
|
||||
type clientHelloFields struct {
|
||||
random []byte
|
||||
sessionId []byte
|
||||
x25519KeyShare []byte
|
||||
serverName string
|
||||
}
|
||||
|
||||
type Browser int
|
||||
|
||||
const (
|
||||
Chrome = iota
|
||||
Firefox
|
||||
Safari
|
||||
)
|
||||
|
||||
type DirectTLS struct {
|
||||
*common.TLSConn
|
||||
Browser Browser
|
||||
}
|
||||
|
||||
func buildClientHello(browser Browser, fields clientHelloFields) ([]byte, error) {
|
||||
// We don't use utls to handle connections (as it'll attempt a real TLS negotiation)
|
||||
// We only want it to build the ClientHello locally
|
||||
fakeConn := net.TCPConn{}
|
||||
var helloID utls.ClientHelloID
|
||||
switch browser {
|
||||
case Chrome:
|
||||
helloID = utls.HelloChrome_Auto
|
||||
case Firefox:
|
||||
helloID = utls.HelloFirefox_Auto
|
||||
case Safari:
|
||||
helloID = utls.HelloSafari_Auto
|
||||
}
|
||||
|
||||
uclient := utls.UClient(&fakeConn, &utls.Config{ServerName: fields.serverName}, helloID)
|
||||
if err := uclient.BuildHandshakeState(); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
if err := uclient.SetClientRandom(fields.random); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
uclient.HandshakeState.Hello.SessionId = make([]byte, 32)
|
||||
copy(uclient.HandshakeState.Hello.SessionId, fields.sessionId)
|
||||
|
||||
// Find the X25519 key share and overwrite it
|
||||
var extIndex int
|
||||
var keyShareIndex int
|
||||
for i, ext := range uclient.Extensions {
|
||||
ext, ok := ext.(*utls.KeyShareExtension)
|
||||
if ok {
|
||||
extIndex = i
|
||||
for j, keyShare := range ext.KeyShares {
|
||||
if keyShare.Group == utls.X25519 {
|
||||
keyShareIndex = j
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
copy(uclient.Extensions[extIndex].(*utls.KeyShareExtension).KeyShares[keyShareIndex].Data, fields.x25519KeyShare)
|
||||
|
||||
if err := uclient.BuildHandshakeState(); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return uclient.HandshakeState.Hello.Raw, nil
|
||||
}
|
||||
|
||||
// Handshake handles the TLS handshake for a given conn and returns the sessionKey
|
||||
// if the server proceed with Cloak authentication
|
||||
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
|
||||
payload, sharedSecret := makeAuthenticationPayload(authInfo)
|
||||
|
||||
fields := clientHelloFields{
|
||||
random: payload.randPubKey[:],
|
||||
sessionId: payload.ciphertextWithTag[0:32],
|
||||
x25519KeyShare: payload.ciphertextWithTag[32:64],
|
||||
serverName: authInfo.MockDomain,
|
||||
}
|
||||
var ch []byte
|
||||
ch, err = buildClientHello(tls.Browser, fields)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
chWithRecordLayer := common.AddRecordLayer(ch, common.Handshake, common.VersionTLS11)
|
||||
_, err = rawConn.Write(chWithRecordLayer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Trace("client hello sent successfully")
|
||||
tls.TLSConn = common.NewTLSConn(rawConn)
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
log.Trace("waiting for ServerHello")
|
||||
_, err = tls.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encrypted := append(buf[6:38], buf[84:116]...)
|
||||
nonce := encrypted[0:12]
|
||||
ciphertextWithTag := encrypted[12:60]
|
||||
sessionKeySlice, err := common.AESGCMDecrypt(nonce, sharedSecret[:], ciphertextWithTag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
copy(sessionKey[:], sessionKeySlice)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
// ChangeCipherSpec and EncryptedCert (in the format of application data)
|
||||
_, err = tls.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return sessionKey, nil
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package client
|
||||
package transports
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
@ -1,4 +1,4 @@
|
||||
package client
|
||||
package transports
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -0,0 +1,23 @@
|
||||
package transports
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Transport interface {
|
||||
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
|
||||
net.Conn
|
||||
}
|
||||
|
||||
type AuthInfo struct {
|
||||
UID []byte
|
||||
SessionId uint32
|
||||
ProxyMethod string
|
||||
EncryptionMethod byte
|
||||
Unordered bool
|
||||
ServerPubKey crypto.PublicKey
|
||||
MockDomain string
|
||||
WorldState common.WorldState
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"packagePatterns": ["*"],
|
||||
"excludePackagePatterns": ["utls"],
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue