├── .github └── workflows │ ├── docs.yml │ ├── node-vless-docker.yml │ ├── nodejs.yml │ ├── remove-old-artifacts.yml │ └── version-comment.yml ├── .gitignore ├── LICENSE ├── README.md ├── config ├── config-client-with-dns-lcoal.json └── config-client-without-dns-lcoal.json ├── jsonconfig.json ├── package.json ├── src ├── worker-vless.js └── worker-with-socks5-experimental.js ├── test ├── cidr.js ├── webstream.mjs └── worker │ ├── cf-cdn-cgi-trace.js │ ├── cf-worker-http-header.js │ ├── cidr.js │ ├── ipaddr-test.js │ ├── stream-ws-test.js │ ├── worker-connect-test.js │ ├── wrangler.toml │ └── ws-send-issue.js └── wrangler.toml /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | # trigger deployment on every push to main branch 5 | push: 6 | branches: [docs] 7 | # trigger deployment manually 8 | workflow_dispatch: 9 | 10 | permissions: 11 | pages: write 12 | contents: write 13 | id-token: write 14 | 15 | jobs: 16 | docs: 17 | environment: 18 | name: github-pages 19 | url: ${{ steps.deployment.outputs.page_url }} 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | # fetch all commits to get last updated time or other git log info 26 | fetch-depth: 0 27 | ref: docs 28 | 29 | # - name: Setup pnpm 30 | # uses: pnpm/action-setup@v2 31 | # with: 32 | # # choose pnpm version to use 33 | # version: 7 34 | # # install deps with pnpm 35 | # run_install: true 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v3 39 | with: 40 | # choose node.js version to use 41 | node-version: 18 42 | cache: npm 43 | 44 | # run build script 45 | - name: Build VuePress site 46 | run: | 47 | npm install 48 | npm run build 49 | - name: Setup Pages 50 | uses: actions/configure-pages@v3 51 | - name: Upload artifact 52 | uses: actions/upload-pages-artifact@v1 53 | with: 54 | # Upload entire repository 55 | path: './docs/.vuepress/dist' 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v2 59 | -------------------------------------------------------------------------------- /.github/workflows/node-vless-docker.yml: -------------------------------------------------------------------------------- 1 | name: node-vless-docker 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | # - name: Use Node.js 11 | # uses: actions/setup-node@v1 12 | # with: 13 | # node-version: 18.x 14 | # - name: Run a multi-line script 15 | # run: | 16 | # npm install 17 | # npm run node-vless:bunled 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v1 20 | - name: Set up Docker Buildx 21 | id: buildx 22 | uses: docker/setup-buildx-action@v1 23 | with: 24 | install: true 25 | - name: Login to DockerHub 26 | uses: docker/login-action@v2 27 | with: 28 | username: ${{ secrets.DOCKERHUB_USERNAME }} 29 | password: ${{ secrets.DOCKERHUB_TOKEN }} 30 | - name: Build and push 31 | uses: docker/build-push-action@v3 32 | with: 33 | # context: ./temp11 34 | platforms: linux/amd64,linux/arm64 35 | push: true 36 | tags: zizifn/node-vless:latest,zizifn/node-vless:1.0.${{ github.run_number }} -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: npm-install 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | # push: 9 | # branches: [master] 10 | # pull_request: 11 | # branches: [master] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: 18.x 31 | 32 | # 33 | # - name: add ssh script 34 | # run: | 35 | # echo Hello, world! 36 | # # check ssh or install ssh 37 | # command -v ssh-agent >/dev/null || (apt-get update -y && apt-get install openssh-client -y) 38 | # eval $(ssh-agent -s) 39 | # echo $SSH_PRIVATE_KEY | tr -d '\r' | ssh-add - 40 | # mkdir -p ~/.ssh && chmod 700 ~./.ssh 41 | # echo $SSH_KNOW_HOSTS >> ~./.ssh/known_hosts && chmod 644 ~./.ssh/known_hosts 42 | # ssh root@host 43 | 44 | # Runs a set of commands using the runners shell 45 | - name: Run a multi-line script 46 | run: | 47 | npm install -------------------------------------------------------------------------------- /.github/workflows/remove-old-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Remove old artifacts 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | remove-old-artifacts: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | 11 | steps: 12 | - name: Remove old artifacts 13 | uses: c-hive/gha-remove-artifacts@v1 14 | with: 15 | age: '2 years' # ' ', e.g. 5 days, 2 years, 90 seconds, parsed by Moment.js 16 | # Optional inputs 17 | # skip-tags: true 18 | # skip-recent: 5 19 | -------------------------------------------------------------------------------- /.github/workflows/version-comment.yml: -------------------------------------------------------------------------------- 1 | name: Add Version Comment 2 | 3 | on: 4 | # trigger deployment on every push to main branch 5 | push: 6 | branches: [main] 7 | # trigger deployment manually 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | version-comment: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Modify version worker-vless.js 20 | uses: jaywcjlove/github-action-modify-file-content@main 21 | with: 22 | path: src/worker-vless.js 23 | body: "version base on commit ${{ github.sha }}, time is {{date:YYYY-MM-DD HH:mm:ss}} UTC" 24 | message: "Update version worker-vless.js" 25 | - name: Modify version worker-with-socks5-experimental.js 26 | uses: jaywcjlove/github-action-modify-file-content@main 27 | with: 28 | path: src/worker-with-socks5-experimental.js 29 | body: "version base on commit ${{ github.sha }}, time is {{date:YYYY-MM-DD HH:mm:ss}} UTC" 30 | message: "Update version worker-with-socks5-experimental.js" 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | *-lock.* 4 | *.lock 5 | *.log 6 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ------ 2 | -------------------------------------------------------------------------------- /config/config-client-with-dns-lcoal.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "loglevel": "debug" 4 | }, 5 | "inbounds": [ 6 | { 7 | "listen": "0.0.0.0", 8 | "port": "4080", 9 | "protocol": "socks", 10 | "settings": { 11 | "auth": "noauth", 12 | "udp": true, 13 | "ip": "0.0.0.0" 14 | } 15 | }, 16 | { 17 | "listen": "0.0.0.0", 18 | "port": "4081", 19 | "protocol": "http" 20 | } 21 | ], 22 | "dns": { 23 | "servers": ["8.8.8.8"] 24 | }, 25 | "outbounds": [ 26 | { 27 | "protocol": "vless", 28 | "settings": { 29 | "vnext": [ 30 | { 31 | "address": "127.0.0.1", 32 | "port": 8788, 33 | "users": [ 34 | { 35 | "id": "1a403b79-039b-4dc2-9b45-1ad13197b99a", 36 | "encryption": "none", 37 | "level": 0 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | "streamSettings": { 44 | "network": "ws", 45 | "wsSettings": { 46 | "path": "/vless" 47 | } 48 | // "security": "tls" 49 | }, 50 | "tag": "zizi-ws" 51 | }, 52 | { 53 | "protocol": "freedom", 54 | "tag": "direct" 55 | } 56 | ], 57 | "routing": { 58 | "domainStrategy": "IPIfNonMatch", 59 | "rules": [ 60 | { 61 | "type": "field", 62 | "ip": ["8.8.8.8"], 63 | "outboundTag": "zizi-ws" 64 | }, 65 | { 66 | "type": "field", 67 | "ip": ["geoip:private"], 68 | "outboundTag": "zizi-ws" 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /config/config-client-without-dns-lcoal.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "loglevel": "debug" 4 | }, 5 | "inbounds": [ 6 | { 7 | "listen": "0.0.0.0", 8 | "port": "4080", 9 | "protocol": "socks", 10 | "settings": { 11 | "auth": "noauth", 12 | "udp": true, 13 | "ip": "0.0.0.0" 14 | } 15 | }, 16 | { 17 | "listen": "0.0.0.0", 18 | "port": "4081", 19 | "protocol": "http" 20 | } 21 | ], 22 | "dns": { 23 | "servers": ["8.8.8.8"] 24 | }, 25 | "outbounds": [ 26 | { 27 | "protocol": "vless", 28 | "settings": { 29 | "vnext": [ 30 | { 31 | "address": "127.0.0.1", 32 | "port": 8787, 33 | "users": [ 34 | { 35 | "id": "1a403b79-039b-4dc2-9b45-1ad13197b99a", 36 | "encryption": "none", 37 | "level": 0 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | "streamSettings": { 44 | "network": "ws" 45 | // "wsSettings": { 46 | // "path": "/node-vless" 47 | // } 48 | // "security": "tls" 49 | }, 50 | "tag": "zizi-ws" 51 | }, 52 | { 53 | "protocol": "freedom", 54 | "tag": "direct" 55 | } 56 | ], 57 | "routing": { 58 | "domainStrategy": "IPIfNonMatch", 59 | "rules": [ 60 | { 61 | "type": "field", 62 | "ip": ["8.8.8.8"], 63 | "outboundTag": "zizi-ws" 64 | }, 65 | { 66 | "type": "field", 67 | "ip": ["geoip:private"], 68 | "outboundTag": "zizi-ws" 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /jsonconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "module": "esnext", 5 | "target": "ES2022", 6 | "lib": ["ES2022"], 7 | "strict": true, 8 | "alwaysStrict": true, 9 | "preserveConstEnums": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "types": ["@cloudflare/workers-types"] 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", "dist", "test"] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edgetunnel", 3 | "version": "1.0.0", 4 | "description": "Delete code, but keep the project for the sake of memories. I'm only create this project for fun, for try new things and for learn by doing. **I'm even not write any outside marketing post for this project. Just for my own fun...** But I feel emotional damage by the community.", 5 | "main": "index.js", 6 | "scripts": { 7 | "deploy": "wrangler publish", 8 | "dev-vless": "wrangler dev src/worker-vless.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@cloudflare/workers-types": "^4.20230518.0", 14 | "wrangler": "^3.1.0" 15 | }, 16 | "dependencies": { 17 | "dns-packet": "^5.6.0", 18 | "ip-cidr": "^3.1.0", 19 | "ip-range-check": "^0.2.0", 20 | "ipaddr.js": "^2.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/worker-vless.js: -------------------------------------------------------------------------------- 1 | // version base on commit 58686d5d125194d34a1137913b3a64ddcf55872f, time is 2024-11-27 09:26:01 UTC. 2 | // @ts-ignore 3 | import { connect } from 'cloudflare:sockets'; 4 | 5 | // How to generate your own UUID: 6 | // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" 7 | let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; 8 | 9 | let proxyIP = ''; 10 | 11 | 12 | if (!isValidUUID(userID)) { 13 | throw new Error('uuid is not valid'); 14 | } 15 | 16 | export default { 17 | /** 18 | * @param {import("@cloudflare/workers-types").Request} request 19 | * @param {{UUID: string, PROXYIP: string}} env 20 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 21 | * @returns {Promise} 22 | */ 23 | async fetch(request, env, ctx) { 24 | try { 25 | userID = env.UUID || userID; 26 | proxyIP = env.PROXYIP || proxyIP; 27 | const upgradeHeader = request.headers.get('Upgrade'); 28 | if (!upgradeHeader || upgradeHeader !== 'websocket') { 29 | const url = new URL(request.url); 30 | switch (url.pathname) { 31 | case '/': 32 | return new Response(JSON.stringify(request.cf), { status: 200 }); 33 | case `/${userID}`: { 34 | const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); 35 | return new Response(`${vlessConfig}`, { 36 | status: 200, 37 | headers: { 38 | "Content-Type": "text/plain;charset=utf-8", 39 | } 40 | }); 41 | } 42 | default: 43 | return new Response('Not found', { status: 404 }); 44 | } 45 | } else { 46 | return await vlessOverWSHandler(request); 47 | } 48 | } catch (err) { 49 | /** @type {Error} */ let e = err; 50 | return new Response(e.toString()); 51 | } 52 | }, 53 | }; 54 | 55 | 56 | 57 | 58 | /** 59 | * 60 | * @param {import("@cloudflare/workers-types").Request} request 61 | */ 62 | async function vlessOverWSHandler(request) { 63 | 64 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */ 65 | // @ts-ignore 66 | const webSocketPair = new WebSocketPair(); 67 | const [client, webSocket] = Object.values(webSocketPair); 68 | 69 | webSocket.accept(); 70 | 71 | let address = ''; 72 | let portWithRandomLog = ''; 73 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { 74 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); 75 | }; 76 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; 77 | 78 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); 79 | 80 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ 81 | let remoteSocketWapper = { 82 | value: null, 83 | }; 84 | let udpStreamWrite = null; 85 | let isDns = false; 86 | 87 | // ws --> remote 88 | readableWebSocketStream.pipeTo(new WritableStream({ 89 | async write(chunk, controller) { 90 | if (isDns && udpStreamWrite) { 91 | return udpStreamWrite(chunk); 92 | } 93 | if (remoteSocketWapper.value) { 94 | const writer = remoteSocketWapper.value.writable.getWriter() 95 | await writer.write(chunk); 96 | writer.releaseLock(); 97 | return; 98 | } 99 | 100 | const { 101 | hasError, 102 | message, 103 | portRemote = 443, 104 | addressRemote = '', 105 | rawDataIndex, 106 | vlessVersion = new Uint8Array([0, 0]), 107 | isUDP, 108 | } = processVlessHeader(chunk, userID); 109 | address = addressRemote; 110 | portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' 111 | } `; 112 | if (hasError) { 113 | // controller.error(message); 114 | throw new Error(message); // cf seems has bug, controller.error will not end stream 115 | // webSocket.close(1000, message); 116 | return; 117 | } 118 | // if UDP but port not DNS port, close it 119 | if (isUDP) { 120 | if (portRemote === 53) { 121 | isDns = true; 122 | } else { 123 | // controller.error('UDP proxy only enable for DNS which is port 53'); 124 | throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream 125 | return; 126 | } 127 | } 128 | // ["version", "附加信息长度 N"] 129 | const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); 130 | const rawClientData = chunk.slice(rawDataIndex); 131 | 132 | // TODO: support udp here when cf runtime has udp support 133 | if (isDns) { 134 | const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); 135 | udpStreamWrite = write; 136 | udpStreamWrite(rawClientData); 137 | return; 138 | } 139 | handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); 140 | }, 141 | close() { 142 | log(`readableWebSocketStream is close`); 143 | }, 144 | abort(reason) { 145 | log(`readableWebSocketStream is abort`, JSON.stringify(reason)); 146 | }, 147 | })).catch((err) => { 148 | log('readableWebSocketStream pipeTo error', err); 149 | }); 150 | 151 | return new Response(null, { 152 | status: 101, 153 | // @ts-ignore 154 | webSocket: client, 155 | }); 156 | } 157 | 158 | /** 159 | * Handles outbound TCP connections. 160 | * 161 | * @param {any} remoteSocket 162 | * @param {string} addressRemote The remote address to connect to. 163 | * @param {number} portRemote The remote port to connect to. 164 | * @param {Uint8Array} rawClientData The raw client data to write. 165 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. 166 | * @param {Uint8Array} vlessResponseHeader The VLESS response header. 167 | * @param {function} log The logging function. 168 | * @returns {Promise} The remote socket. 169 | */ 170 | async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { 171 | async function connectAndWrite(address, port) { 172 | /** @type {import("@cloudflare/workers-types").Socket} */ 173 | const tcpSocket = connect({ 174 | hostname: address, 175 | port: port, 176 | }); 177 | remoteSocket.value = tcpSocket; 178 | log(`connected to ${address}:${port}`); 179 | const writer = tcpSocket.writable.getWriter(); 180 | await writer.write(rawClientData); // first write, nomal is tls client hello 181 | writer.releaseLock(); 182 | return tcpSocket; 183 | } 184 | 185 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip 186 | async function retry() { 187 | const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) 188 | // no matter retry success or not, close websocket 189 | tcpSocket.closed.catch(error => { 190 | console.log('retry tcpSocket closed error', error); 191 | }).finally(() => { 192 | safeCloseWebSocket(webSocket); 193 | }) 194 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); 195 | } 196 | 197 | const tcpSocket = await connectAndWrite(addressRemote, portRemote); 198 | 199 | // when remoteSocket is ready, pass to websocket 200 | // remote--> ws 201 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); 202 | } 203 | 204 | /** 205 | * 206 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer 207 | * @param {string} earlyDataHeader for ws 0rtt 208 | * @param {(info: string)=> void} log for ws 0rtt 209 | */ 210 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { 211 | let readableStreamCancel = false; 212 | const stream = new ReadableStream({ 213 | start(controller) { 214 | webSocketServer.addEventListener('message', (event) => { 215 | if (readableStreamCancel) { 216 | return; 217 | } 218 | const message = event.data; 219 | controller.enqueue(message); 220 | }); 221 | 222 | // The event means that the client closed the client -> server stream. 223 | // However, the server -> client stream is still open until you call close() on the server side. 224 | // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. 225 | webSocketServer.addEventListener('close', () => { 226 | // client send close, need close server 227 | // if stream is cancel, skip controller.close 228 | safeCloseWebSocket(webSocketServer); 229 | if (readableStreamCancel) { 230 | return; 231 | } 232 | controller.close(); 233 | } 234 | ); 235 | webSocketServer.addEventListener('error', (err) => { 236 | log('webSocketServer has error'); 237 | controller.error(err); 238 | } 239 | ); 240 | // for ws 0rtt 241 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); 242 | if (error) { 243 | controller.error(error); 244 | } else if (earlyData) { 245 | controller.enqueue(earlyData); 246 | } 247 | }, 248 | 249 | pull(controller) { 250 | // if ws can stop read if stream is full, we can implement backpressure 251 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure 252 | }, 253 | cancel(reason) { 254 | // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here 255 | // 2. if readableStream is cancel, all controller.close/enqueue need skip, 256 | // 3. but from testing controller.error still work even if readableStream is cancel 257 | if (readableStreamCancel) { 258 | return; 259 | } 260 | log(`ReadableStream was canceled, due to ${reason}`) 261 | readableStreamCancel = true; 262 | safeCloseWebSocket(webSocketServer); 263 | } 264 | }); 265 | 266 | return stream; 267 | 268 | } 269 | 270 | // https://xtls.github.io/development/protocols/vless.html 271 | // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw 272 | 273 | /** 274 | * 275 | * @param { ArrayBuffer} vlessBuffer 276 | * @param {string} userID 277 | * @returns 278 | */ 279 | function processVlessHeader( 280 | vlessBuffer, 281 | userID 282 | ) { 283 | if (vlessBuffer.byteLength < 24) { 284 | return { 285 | hasError: true, 286 | message: 'invalid data', 287 | }; 288 | } 289 | const version = new Uint8Array(vlessBuffer.slice(0, 1)); 290 | let isValidUser = false; 291 | let isUDP = false; 292 | if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) { 293 | isValidUser = true; 294 | } 295 | if (!isValidUser) { 296 | return { 297 | hasError: true, 298 | message: 'invalid user', 299 | }; 300 | } 301 | 302 | const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; 303 | //skip opt for now 304 | 305 | const command = new Uint8Array( 306 | vlessBuffer.slice(18 + optLength, 18 + optLength + 1) 307 | )[0]; 308 | 309 | // 0x01 TCP 310 | // 0x02 UDP 311 | // 0x03 MUX 312 | if (command === 1) { 313 | } else if (command === 2) { 314 | isUDP = true; 315 | } else { 316 | return { 317 | hasError: true, 318 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, 319 | }; 320 | } 321 | const portIndex = 18 + optLength + 1; 322 | const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); 323 | // port is big-Endian in raw data etc 80 == 0x005d 324 | const portRemote = new DataView(portBuffer).getUint16(0); 325 | 326 | let addressIndex = portIndex + 2; 327 | const addressBuffer = new Uint8Array( 328 | vlessBuffer.slice(addressIndex, addressIndex + 1) 329 | ); 330 | 331 | // 1--> ipv4 addressLength =4 332 | // 2--> domain name addressLength=addressBuffer[1] 333 | // 3--> ipv6 addressLength =16 334 | const addressType = addressBuffer[0]; 335 | let addressLength = 0; 336 | let addressValueIndex = addressIndex + 1; 337 | let addressValue = ''; 338 | switch (addressType) { 339 | case 1: 340 | addressLength = 4; 341 | addressValue = new Uint8Array( 342 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 343 | ).join('.'); 344 | break; 345 | case 2: 346 | addressLength = new Uint8Array( 347 | vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) 348 | )[0]; 349 | addressValueIndex += 1; 350 | addressValue = new TextDecoder().decode( 351 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 352 | ); 353 | break; 354 | case 3: 355 | addressLength = 16; 356 | const dataView = new DataView( 357 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 358 | ); 359 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 360 | const ipv6 = []; 361 | for (let i = 0; i < 8; i++) { 362 | ipv6.push(dataView.getUint16(i * 2).toString(16)); 363 | } 364 | addressValue = ipv6.join(':'); 365 | // seems no need add [] for ipv6 366 | break; 367 | default: 368 | return { 369 | hasError: true, 370 | message: `invild addressType is ${addressType}`, 371 | }; 372 | } 373 | if (!addressValue) { 374 | return { 375 | hasError: true, 376 | message: `addressValue is empty, addressType is ${addressType}`, 377 | }; 378 | } 379 | 380 | return { 381 | hasError: false, 382 | addressRemote: addressValue, 383 | addressType, 384 | portRemote, 385 | rawDataIndex: addressValueIndex + addressLength, 386 | vlessVersion: version, 387 | isUDP, 388 | }; 389 | } 390 | 391 | 392 | /** 393 | * 394 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket 395 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 396 | * @param {ArrayBuffer} vlessResponseHeader 397 | * @param {(() => Promise) | null} retry 398 | * @param {*} log 399 | */ 400 | async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { 401 | // remote--> ws 402 | let remoteChunkCount = 0; 403 | let chunks = []; 404 | /** @type {ArrayBuffer | null} */ 405 | let vlessHeader = vlessResponseHeader; 406 | let hasIncomingData = false; // check if remoteSocket has incoming data 407 | await remoteSocket.readable 408 | .pipeTo( 409 | new WritableStream({ 410 | start() { 411 | }, 412 | /** 413 | * 414 | * @param {Uint8Array} chunk 415 | * @param {*} controller 416 | */ 417 | async write(chunk, controller) { 418 | hasIncomingData = true; 419 | // remoteChunkCount++; 420 | if (webSocket.readyState !== WS_READY_STATE_OPEN) { 421 | controller.error( 422 | 'webSocket.readyState is not open, maybe close' 423 | ); 424 | } 425 | if (vlessHeader) { 426 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 427 | vlessHeader = null; 428 | } else { 429 | // seems no need rate limit this, CF seems fix this??.. 430 | // if (remoteChunkCount > 20000) { 431 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M 432 | // await delay(1); 433 | // } 434 | webSocket.send(chunk); 435 | } 436 | }, 437 | close() { 438 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); 439 | // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. 440 | }, 441 | abort(reason) { 442 | console.error(`remoteConnection!.readable abort`, reason); 443 | }, 444 | }) 445 | ) 446 | .catch((error) => { 447 | console.error( 448 | `remoteSocketToWS has exception `, 449 | error.stack || error 450 | ); 451 | safeCloseWebSocket(webSocket); 452 | }); 453 | 454 | // seems is cf connect socket have error, 455 | // 1. Socket.closed will have error 456 | // 2. Socket.readable will be close without any data coming 457 | if (hasIncomingData === false && retry) { 458 | log(`retry`) 459 | retry(); 460 | } 461 | } 462 | 463 | /** 464 | * 465 | * @param {string} base64Str 466 | * @returns 467 | */ 468 | function base64ToArrayBuffer(base64Str) { 469 | if (!base64Str) { 470 | return { error: null }; 471 | } 472 | try { 473 | // go use modified Base64 for URL rfc4648 which js atob not support 474 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); 475 | const decode = atob(base64Str); 476 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); 477 | return { earlyData: arryBuffer.buffer, error: null }; 478 | } catch (error) { 479 | return { error }; 480 | } 481 | } 482 | 483 | /** 484 | * This is not real UUID validation 485 | * @param {string} uuid 486 | */ 487 | function isValidUUID(uuid) { 488 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 489 | return uuidRegex.test(uuid); 490 | } 491 | 492 | const WS_READY_STATE_OPEN = 1; 493 | const WS_READY_STATE_CLOSING = 2; 494 | /** 495 | * Normally, WebSocket will not has exceptions when close. 496 | * @param {import("@cloudflare/workers-types").WebSocket} socket 497 | */ 498 | function safeCloseWebSocket(socket) { 499 | try { 500 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { 501 | socket.close(); 502 | } 503 | } catch (error) { 504 | console.error('safeCloseWebSocket error', error); 505 | } 506 | } 507 | 508 | const byteToHex = []; 509 | for (let i = 0; i < 256; ++i) { 510 | byteToHex.push((i + 256).toString(16).slice(1)); 511 | } 512 | function unsafeStringify(arr, offset = 0) { 513 | return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); 514 | } 515 | function stringify(arr, offset = 0) { 516 | const uuid = unsafeStringify(arr, offset); 517 | if (!isValidUUID(uuid)) { 518 | throw TypeError("Stringified UUID is invalid"); 519 | } 520 | return uuid; 521 | } 522 | 523 | 524 | /** 525 | * 526 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 527 | * @param {ArrayBuffer} vlessResponseHeader 528 | * @param {(string)=> void} log 529 | */ 530 | async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { 531 | 532 | let isVlessHeaderSent = false; 533 | const transformStream = new TransformStream({ 534 | start(controller) { 535 | 536 | }, 537 | transform(chunk, controller) { 538 | // udp message 2 byte is the the length of udp data 539 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message 540 | for (let index = 0; index < chunk.byteLength;) { 541 | const lengthBuffer = chunk.slice(index, index + 2); 542 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); 543 | const udpData = new Uint8Array( 544 | chunk.slice(index + 2, index + 2 + udpPakcetLength) 545 | ); 546 | index = index + 2 + udpPakcetLength; 547 | controller.enqueue(udpData); 548 | } 549 | }, 550 | flush(controller) { 551 | } 552 | }); 553 | 554 | // only handle dns udp for now 555 | transformStream.readable.pipeTo(new WritableStream({ 556 | async write(chunk) { 557 | const resp = await fetch('https://1.1.1.1/dns-query', 558 | { 559 | method: 'POST', 560 | headers: { 561 | 'content-type': 'application/dns-message', 562 | }, 563 | body: chunk, 564 | }) 565 | const dnsQueryResult = await resp.arrayBuffer(); 566 | const udpSize = dnsQueryResult.byteLength; 567 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); 568 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); 569 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 570 | log(`doh success and dns message length is ${udpSize}`); 571 | if (isVlessHeaderSent) { 572 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 573 | } else { 574 | webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 575 | isVlessHeaderSent = true; 576 | } 577 | } 578 | } 579 | })).catch((error) => { 580 | log('dns udp has error' + error) 581 | }); 582 | 583 | const writer = transformStream.writable.getWriter(); 584 | 585 | return { 586 | /** 587 | * 588 | * @param {Uint8Array} chunk 589 | */ 590 | write(chunk) { 591 | writer.write(chunk); 592 | } 593 | }; 594 | } 595 | 596 | /** 597 | * 598 | * @param {string} userID 599 | * @param {string | null} hostName 600 | * @returns {string} 601 | */ 602 | function getVLESSConfig(userID, hostName) { 603 | const protocol = "vless"; 604 | const vlessMain = 605 | `${protocol}` + 606 | `://${userID}@${hostName}:443`+ 607 | `?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`; 608 | 609 | return ` 610 | ################################################################ 611 | v2ray 612 | --------------------------------------------------------------- 613 | ${vlessMain} 614 | --------------------------------------------------------------- 615 | ################################################################ 616 | clash-meta 617 | --------------------------------------------------------------- 618 | - type: vless 619 | name: ${hostName} 620 | server: ${hostName} 621 | port: 443 622 | uuid: ${userID} 623 | network: ws 624 | tls: true 625 | udp: false 626 | sni: ${hostName} 627 | client-fingerprint: chrome 628 | ws-opts: 629 | path: "/?ed=2048" 630 | headers: 631 | host: ${hostName} 632 | --------------------------------------------------------------- 633 | ################################################################ 634 | `; 635 | } 636 | 637 | -------------------------------------------------------------------------------- /src/worker-with-socks5-experimental.js: -------------------------------------------------------------------------------- 1 | // version base on commit 58686d5d125194d34a1137913b3a64ddcf55872f, time is 2024-11-27 09:26:02 UTC. 2 | // @ts-ignore 3 | import { connect } from 'cloudflare:sockets'; 4 | 5 | // How to generate your own UUID: 6 | // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" 7 | let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; 8 | 9 | let proxyIP = ''; 10 | 11 | // The user name and password do not contain special characters 12 | // Setting the address will ignore proxyIP 13 | // Example: user:pass@host:port or host:port 14 | let socks5Address = ''; 15 | 16 | if (!isValidUUID(userID)) { 17 | throw new Error('uuid is not valid'); 18 | } 19 | 20 | let parsedSocks5Address = {}; 21 | let enableSocks = false; 22 | 23 | export default { 24 | /** 25 | * @param {import("@cloudflare/workers-types").Request} request 26 | * @param {{UUID: string, PROXYIP: string}} env 27 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 28 | * @returns {Promise} 29 | */ 30 | async fetch(request, env, ctx) { 31 | try { 32 | userID = env.UUID || userID; 33 | proxyIP = env.PROXYIP || proxyIP; 34 | socks5Address = env.SOCKS5 || socks5Address; 35 | if (socks5Address) { 36 | try { 37 | parsedSocks5Address = socks5AddressParser(socks5Address); 38 | enableSocks = true; 39 | } catch (err) { 40 | /** @type {Error} */ let e = err; 41 | console.log(e.toString()); 42 | enableSocks = false; 43 | } 44 | } 45 | const upgradeHeader = request.headers.get('Upgrade'); 46 | if (!upgradeHeader || upgradeHeader !== 'websocket') { 47 | const url = new URL(request.url); 48 | switch (url.pathname) { 49 | case '/': 50 | return new Response(JSON.stringify(request.cf), { status: 200 }); 51 | case `/${userID}`: { 52 | const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); 53 | return new Response(`${vlessConfig}`, { 54 | status: 200, 55 | headers: { 56 | "Content-Type": "text/plain;charset=utf-8", 57 | } 58 | }); 59 | } 60 | default: 61 | return new Response('Not found', { status: 404 }); 62 | } 63 | } else { 64 | return await vlessOverWSHandler(request); 65 | } 66 | } catch (err) { 67 | /** @type {Error} */ let e = err; 68 | return new Response(e.toString()); 69 | } 70 | }, 71 | }; 72 | 73 | 74 | 75 | 76 | /** 77 | * 78 | * @param {import("@cloudflare/workers-types").Request} request 79 | */ 80 | async function vlessOverWSHandler(request) { 81 | 82 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */ 83 | // @ts-ignore 84 | const webSocketPair = new WebSocketPair(); 85 | const [client, webSocket] = Object.values(webSocketPair); 86 | 87 | webSocket.accept(); 88 | 89 | let address = ''; 90 | let portWithRandomLog = ''; 91 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { 92 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); 93 | }; 94 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; 95 | 96 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); 97 | 98 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ 99 | let remoteSocketWapper = { 100 | value: null, 101 | }; 102 | let isDns = false; 103 | 104 | // ws --> remote 105 | readableWebSocketStream.pipeTo(new WritableStream({ 106 | async write(chunk, controller) { 107 | if (isDns) { 108 | return await handleDNSQuery(chunk, webSocket, null, log); 109 | } 110 | if (remoteSocketWapper.value) { 111 | const writer = remoteSocketWapper.value.writable.getWriter() 112 | await writer.write(chunk); 113 | writer.releaseLock(); 114 | return; 115 | } 116 | 117 | const { 118 | hasError, 119 | message, 120 | addressType, 121 | portRemote = 443, 122 | addressRemote = '', 123 | rawDataIndex, 124 | vlessVersion = new Uint8Array([0, 0]), 125 | isUDP, 126 | } = processVlessHeader(chunk, userID); 127 | address = addressRemote; 128 | portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' 129 | } `; 130 | if (hasError) { 131 | // controller.error(message); 132 | throw new Error(message); // cf seems has bug, controller.error will not end stream 133 | // webSocket.close(1000, message); 134 | return; 135 | } 136 | // if UDP but port not DNS port, close it 137 | if (isUDP) { 138 | if (portRemote === 53) { 139 | isDns = true; 140 | } else { 141 | // controller.error('UDP proxy only enable for DNS which is port 53'); 142 | throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream 143 | return; 144 | } 145 | } 146 | // ["version", "附加信息长度 N"] 147 | const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); 148 | const rawClientData = chunk.slice(rawDataIndex); 149 | 150 | if (isDns) { 151 | return handleDNSQuery(rawClientData, webSocket, vlessResponseHeader, log); 152 | } 153 | handleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); 154 | }, 155 | close() { 156 | log(`readableWebSocketStream is close`); 157 | }, 158 | abort(reason) { 159 | log(`readableWebSocketStream is abort`, JSON.stringify(reason)); 160 | }, 161 | })).catch((err) => { 162 | log('readableWebSocketStream pipeTo error', err); 163 | }); 164 | 165 | return new Response(null, { 166 | status: 101, 167 | // @ts-ignore 168 | webSocket: client, 169 | }); 170 | } 171 | 172 | /** 173 | * Handles outbound TCP connections. 174 | * 175 | * @param {any} remoteSocket 176 | * @param {number} addressType The remote address type to connect to. 177 | * @param {string} addressRemote The remote address to connect to. 178 | * @param {number} portRemote The remote port to connect to. 179 | * @param {Uint8Array} rawClientData The raw client data to write. 180 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. 181 | * @param {Uint8Array} vlessResponseHeader The VLESS response header. 182 | * @param {function} log The logging function. 183 | * @returns {Promise} The remote socket. 184 | */ 185 | async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { 186 | async function connectAndWrite(address, port, socks = false) { 187 | /** @type {import("@cloudflare/workers-types").Socket} */ 188 | const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) 189 | : connect({ 190 | hostname: address, 191 | port: port, 192 | }); 193 | remoteSocket.value = tcpSocket; 194 | log(`connected to ${address}:${port}`); 195 | const writer = tcpSocket.writable.getWriter(); 196 | await writer.write(rawClientData); // first write, normal is tls client hello 197 | writer.releaseLock(); 198 | return tcpSocket; 199 | } 200 | 201 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip 202 | async function retry() { 203 | if (enableSocks) { 204 | tcpSocket = await connectAndWrite(addressRemote, portRemote, true); 205 | } else { 206 | tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote); 207 | } 208 | // no matter retry success or not, close websocket 209 | tcpSocket.closed.catch(error => { 210 | console.log('retry tcpSocket closed error', error); 211 | }).finally(() => { 212 | safeCloseWebSocket(webSocket); 213 | }) 214 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); 215 | } 216 | 217 | let tcpSocket = await connectAndWrite(addressRemote, portRemote); 218 | 219 | // when remoteSocket is ready, pass to websocket 220 | // remote--> ws 221 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); 222 | } 223 | 224 | /** 225 | * 226 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer 227 | * @param {string} earlyDataHeader for ws 0rtt 228 | * @param {(info: string)=> void} log for ws 0rtt 229 | */ 230 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { 231 | let readableStreamCancel = false; 232 | const stream = new ReadableStream({ 233 | start(controller) { 234 | webSocketServer.addEventListener('message', (event) => { 235 | if (readableStreamCancel) { 236 | return; 237 | } 238 | const message = event.data; 239 | controller.enqueue(message); 240 | }); 241 | 242 | // The event means that the client closed the client -> server stream. 243 | // However, the server -> client stream is still open until you call close() on the server side. 244 | // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. 245 | webSocketServer.addEventListener('close', () => { 246 | // client send close, need close server 247 | // if stream is cancel, skip controller.close 248 | safeCloseWebSocket(webSocketServer); 249 | if (readableStreamCancel) { 250 | return; 251 | } 252 | controller.close(); 253 | } 254 | ); 255 | webSocketServer.addEventListener('error', (err) => { 256 | log('webSocketServer has error'); 257 | controller.error(err); 258 | } 259 | ); 260 | // for ws 0rtt 261 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); 262 | if (error) { 263 | controller.error(error); 264 | } else if (earlyData) { 265 | controller.enqueue(earlyData); 266 | } 267 | }, 268 | 269 | pull(controller) { 270 | // if ws can stop read if stream is full, we can implement backpressure 271 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure 272 | }, 273 | cancel(reason) { 274 | // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here 275 | // 2. if readableStream is cancel, all controller.close/enqueue need skip, 276 | // 3. but from testing controller.error still work even if readableStream is cancel 277 | if (readableStreamCancel) { 278 | return; 279 | } 280 | log(`ReadableStream was canceled, due to ${reason}`) 281 | readableStreamCancel = true; 282 | safeCloseWebSocket(webSocketServer); 283 | } 284 | }); 285 | 286 | return stream; 287 | 288 | } 289 | 290 | // https://xtls.github.io/development/protocols/vless.html 291 | // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw 292 | 293 | /** 294 | * 295 | * @param { ArrayBuffer} vlessBuffer 296 | * @param {string} userID 297 | * @returns 298 | */ 299 | function processVlessHeader( 300 | vlessBuffer, 301 | userID 302 | ) { 303 | if (vlessBuffer.byteLength < 24) { 304 | return { 305 | hasError: true, 306 | message: 'invalid data', 307 | }; 308 | } 309 | const version = new Uint8Array(vlessBuffer.slice(0, 1)); 310 | let isValidUser = false; 311 | let isUDP = false; 312 | if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) { 313 | isValidUser = true; 314 | } 315 | if (!isValidUser) { 316 | return { 317 | hasError: true, 318 | message: 'invalid user', 319 | }; 320 | } 321 | 322 | const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; 323 | //skip opt for now 324 | 325 | const command = new Uint8Array( 326 | vlessBuffer.slice(18 + optLength, 18 + optLength + 1) 327 | )[0]; 328 | 329 | // 0x01 TCP 330 | // 0x02 UDP 331 | // 0x03 MUX 332 | if (command === 1) { 333 | } else if (command === 2) { 334 | isUDP = true; 335 | } else { 336 | return { 337 | hasError: true, 338 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, 339 | }; 340 | } 341 | const portIndex = 18 + optLength + 1; 342 | const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); 343 | // port is big-Endian in raw data etc 80 == 0x005d 344 | const portRemote = new DataView(portBuffer).getUint16(0); 345 | 346 | let addressIndex = portIndex + 2; 347 | const addressBuffer = new Uint8Array( 348 | vlessBuffer.slice(addressIndex, addressIndex + 1) 349 | ); 350 | 351 | // 1--> ipv4 addressLength =4 352 | // 2--> domain name addressLength=addressBuffer[1] 353 | // 3--> ipv6 addressLength =16 354 | const addressType = addressBuffer[0]; 355 | let addressLength = 0; 356 | let addressValueIndex = addressIndex + 1; 357 | let addressValue = ''; 358 | switch (addressType) { 359 | case 1: 360 | addressLength = 4; 361 | addressValue = new Uint8Array( 362 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 363 | ).join('.'); 364 | break; 365 | case 2: 366 | addressLength = new Uint8Array( 367 | vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) 368 | )[0]; 369 | addressValueIndex += 1; 370 | addressValue = new TextDecoder().decode( 371 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 372 | ); 373 | break; 374 | case 3: 375 | addressLength = 16; 376 | const dataView = new DataView( 377 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 378 | ); 379 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 380 | const ipv6 = []; 381 | for (let i = 0; i < 8; i++) { 382 | ipv6.push(dataView.getUint16(i * 2).toString(16)); 383 | } 384 | addressValue = ipv6.join(':'); 385 | // seems no need add [] for ipv6 386 | break; 387 | default: 388 | return { 389 | hasError: true, 390 | message: `invild addressType is ${addressType}`, 391 | }; 392 | } 393 | if (!addressValue) { 394 | return { 395 | hasError: true, 396 | message: `addressValue is empty, addressType is ${addressType}`, 397 | }; 398 | } 399 | 400 | return { 401 | hasError: false, 402 | addressRemote: addressValue, 403 | addressType, 404 | portRemote, 405 | rawDataIndex: addressValueIndex + addressLength, 406 | vlessVersion: version, 407 | isUDP, 408 | }; 409 | } 410 | 411 | 412 | /** 413 | * 414 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket 415 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 416 | * @param {ArrayBuffer} vlessResponseHeader 417 | * @param {(() => Promise) | null} retry 418 | * @param {*} log 419 | */ 420 | async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { 421 | // remote--> ws 422 | let remoteChunkCount = 0; 423 | let chunks = []; 424 | /** @type {ArrayBuffer | null} */ 425 | let vlessHeader = vlessResponseHeader; 426 | let hasIncomingData = false; // check if remoteSocket has incoming data 427 | await remoteSocket.readable 428 | .pipeTo( 429 | new WritableStream({ 430 | start() { 431 | }, 432 | /** 433 | * 434 | * @param {Uint8Array} chunk 435 | * @param {*} controller 436 | */ 437 | async write(chunk, controller) { 438 | hasIncomingData = true; 439 | // remoteChunkCount++; 440 | if (webSocket.readyState !== WS_READY_STATE_OPEN) { 441 | controller.error( 442 | 'webSocket.readyState is not open, maybe close' 443 | ); 444 | } 445 | if (vlessHeader) { 446 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 447 | vlessHeader = null; 448 | } else { 449 | // seems no need rate limit this, CF seems fix this??.. 450 | // if (remoteChunkCount > 20000) { 451 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M 452 | // await delay(1); 453 | // } 454 | webSocket.send(chunk); 455 | } 456 | }, 457 | close() { 458 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); 459 | // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. 460 | }, 461 | abort(reason) { 462 | console.error(`remoteConnection!.readable abort`, reason); 463 | }, 464 | }) 465 | ) 466 | .catch((error) => { 467 | console.error( 468 | `remoteSocketToWS has exception `, 469 | error.stack || error 470 | ); 471 | safeCloseWebSocket(webSocket); 472 | }); 473 | 474 | // seems is cf connect socket have error, 475 | // 1. Socket.closed will have error 476 | // 2. Socket.readable will be close without any data coming 477 | if (hasIncomingData === false && retry) { 478 | log(`retry`) 479 | retry(); 480 | } 481 | } 482 | 483 | /** 484 | * 485 | * @param {string} base64Str 486 | * @returns 487 | */ 488 | function base64ToArrayBuffer(base64Str) { 489 | if (!base64Str) { 490 | return { error: null }; 491 | } 492 | try { 493 | // go use modified Base64 for URL rfc4648 which js atob not support 494 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); 495 | const decode = atob(base64Str); 496 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); 497 | return { earlyData: arryBuffer.buffer, error: null }; 498 | } catch (error) { 499 | return { error }; 500 | } 501 | } 502 | 503 | /** 504 | * This is not real UUID validation 505 | * @param {string} uuid 506 | */ 507 | function isValidUUID(uuid) { 508 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 509 | return uuidRegex.test(uuid); 510 | } 511 | 512 | const WS_READY_STATE_OPEN = 1; 513 | const WS_READY_STATE_CLOSING = 2; 514 | /** 515 | * Normally, WebSocket will not has exceptions when close. 516 | * @param {import("@cloudflare/workers-types").WebSocket} socket 517 | */ 518 | function safeCloseWebSocket(socket) { 519 | try { 520 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { 521 | socket.close(); 522 | } 523 | } catch (error) { 524 | console.error('safeCloseWebSocket error', error); 525 | } 526 | } 527 | 528 | const byteToHex = []; 529 | for (let i = 0; i < 256; ++i) { 530 | byteToHex.push((i + 256).toString(16).slice(1)); 531 | } 532 | function unsafeStringify(arr, offset = 0) { 533 | return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); 534 | } 535 | function stringify(arr, offset = 0) { 536 | const uuid = unsafeStringify(arr, offset); 537 | if (!isValidUUID(uuid)) { 538 | throw TypeError("Stringified UUID is invalid"); 539 | } 540 | return uuid; 541 | } 542 | 543 | /** 544 | * 545 | * @param {ArrayBuffer} udpChunk 546 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 547 | * @param {ArrayBuffer} vlessResponseHeader 548 | * @param {(string)=> void} log 549 | */ 550 | async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { 551 | // no matter which DNS server client send, we alwasy use hard code one. 552 | // beacsue someof DNS server is not support DNS over TCP 553 | try { 554 | const dnsServer = '8.8.4.4'; // change to 1.1.1.1 after cf fix connect own ip bug 555 | const dnsPort = 53; 556 | /** @type {ArrayBuffer | null} */ 557 | let vlessHeader = vlessResponseHeader; 558 | /** @type {import("@cloudflare/workers-types").Socket} */ 559 | const tcpSocket = connect({ 560 | hostname: dnsServer, 561 | port: dnsPort, 562 | }); 563 | 564 | log(`connected to ${dnsServer}:${dnsPort}`); 565 | const writer = tcpSocket.writable.getWriter(); 566 | await writer.write(udpChunk); 567 | writer.releaseLock(); 568 | await tcpSocket.readable.pipeTo(new WritableStream({ 569 | async write(chunk) { 570 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 571 | if (vlessHeader) { 572 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 573 | vlessHeader = null; 574 | } else { 575 | webSocket.send(chunk); 576 | } 577 | } 578 | }, 579 | close() { 580 | log(`dns server(${dnsServer}) tcp is close`); 581 | }, 582 | abort(reason) { 583 | console.error(`dns server(${dnsServer}) tcp is abort`, reason); 584 | }, 585 | })); 586 | } catch (error) { 587 | console.error( 588 | `handleDNSQuery have exception, error: ${error.message}` 589 | ); 590 | } 591 | } 592 | 593 | /** 594 | * 595 | * @param {number} addressType 596 | * @param {string} addressRemote 597 | * @param {number} portRemote 598 | * @param {function} log The logging function. 599 | */ 600 | async function socks5Connect(addressType, addressRemote, portRemote, log) { 601 | const { username, password, hostname, port } = parsedSocks5Address; 602 | // Connect to the SOCKS server 603 | const socket = connect({ 604 | hostname, 605 | port, 606 | }); 607 | 608 | // Request head format (Worker -> Socks Server): 609 | // +----+----------+----------+ 610 | // |VER | NMETHODS | METHODS | 611 | // +----+----------+----------+ 612 | // | 1 | 1 | 1 to 255 | 613 | // +----+----------+----------+ 614 | 615 | // https://en.wikipedia.org/wiki/SOCKS#SOCKS5 616 | // For METHODS: 617 | // 0x00 NO AUTHENTICATION REQUIRED 618 | // 0x02 USERNAME/PASSWORD https://datatracker.ietf.org/doc/html/rfc1929 619 | const socksGreeting = new Uint8Array([5, 2, 0, 2]); 620 | 621 | const writer = socket.writable.getWriter(); 622 | 623 | await writer.write(socksGreeting); 624 | log('sent socks greeting'); 625 | 626 | const reader = socket.readable.getReader(); 627 | const encoder = new TextEncoder(); 628 | let res = (await reader.read()).value; 629 | // Response format (Socks Server -> Worker): 630 | // +----+--------+ 631 | // |VER | METHOD | 632 | // +----+--------+ 633 | // | 1 | 1 | 634 | // +----+--------+ 635 | if (res[0] !== 0x05) { 636 | log(`socks server version error: ${res[0]} expected: 5`); 637 | return; 638 | } 639 | if (res[1] === 0xff) { 640 | log("no acceptable methods"); 641 | return; 642 | } 643 | 644 | // if return 0x0502 645 | if (res[1] === 0x02) { 646 | log("socks server needs auth"); 647 | if (!username || !password) { 648 | log("please provide username/password"); 649 | return; 650 | } 651 | // +----+------+----------+------+----------+ 652 | // |VER | ULEN | UNAME | PLEN | PASSWD | 653 | // +----+------+----------+------+----------+ 654 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | 655 | // +----+------+----------+------+----------+ 656 | const authRequest = new Uint8Array([ 657 | 1, 658 | username.length, 659 | ...encoder.encode(username), 660 | password.length, 661 | ...encoder.encode(password) 662 | ]); 663 | await writer.write(authRequest); 664 | res = (await reader.read()).value; 665 | // expected 0x0100 666 | if (res[0] !== 0x01 || res[1] !== 0x00) { 667 | log("fail to auth socks server"); 668 | return; 669 | } 670 | } 671 | 672 | // Request data format (Worker -> Socks Server): 673 | // +----+-----+-------+------+----------+----------+ 674 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 675 | // +----+-----+-------+------+----------+----------+ 676 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 677 | // +----+-----+-------+------+----------+----------+ 678 | // ATYP: address type of following address 679 | // 0x01: IPv4 address 680 | // 0x03: Domain name 681 | // 0x04: IPv6 address 682 | // DST.ADDR: desired destination address 683 | // DST.PORT: desired destination port in network octet order 684 | 685 | // addressType 686 | // 1--> ipv4 addressLength =4 687 | // 2--> domain name 688 | // 3--> ipv6 addressLength =16 689 | let DSTADDR; // DSTADDR = ATYP + DST.ADDR 690 | switch (addressType) { 691 | case 1: 692 | DSTADDR = new Uint8Array( 693 | [1, ...addressRemote.split('.').map(Number)] 694 | ); 695 | break; 696 | case 2: 697 | DSTADDR = new Uint8Array( 698 | [3, addressRemote.length, ...encoder.encode(addressRemote)] 699 | ); 700 | break; 701 | case 3: 702 | DSTADDR = new Uint8Array( 703 | [4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])] 704 | ); 705 | break; 706 | default: 707 | log(`invild addressType is ${addressType}`); 708 | return; 709 | } 710 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]); 711 | await writer.write(socksRequest); 712 | log('sent socks request'); 713 | 714 | res = (await reader.read()).value; 715 | // Response format (Socks Server -> Worker): 716 | // +----+-----+-------+------+----------+----------+ 717 | // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 718 | // +----+-----+-------+------+----------+----------+ 719 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 720 | // +----+-----+-------+------+----------+----------+ 721 | if (res[1] === 0x00) { 722 | log("socks connection opened"); 723 | } else { 724 | log("fail to open socks connection"); 725 | return; 726 | } 727 | writer.releaseLock(); 728 | reader.releaseLock(); 729 | return socket; 730 | } 731 | 732 | 733 | /** 734 | * 735 | * @param {string} address 736 | */ 737 | function socks5AddressParser(address) { 738 | let [latter, former] = address.split("@").reverse(); 739 | let username, password, hostname, port; 740 | if (former) { 741 | const formers = former.split(":"); 742 | if (formers.length !== 2) { 743 | throw new Error('Invalid SOCKS address format'); 744 | } 745 | [username, password] = formers; 746 | } 747 | const latters = latter.split(":"); 748 | port = Number(latters.pop()); 749 | if (isNaN(port)) { 750 | throw new Error('Invalid SOCKS address format'); 751 | } 752 | hostname = latters.join(":"); 753 | const regex = /^\[.*\]$/; 754 | if (hostname.includes(":") && !regex.test(hostname)) { 755 | throw new Error('Invalid SOCKS address format'); 756 | } 757 | return { 758 | username, 759 | password, 760 | hostname, 761 | port, 762 | } 763 | } 764 | 765 | /** 766 | * 767 | * @param {string} userID 768 | * @param {string | null} hostName 769 | * @returns {string} 770 | */ 771 | function getVLESSConfig(userID, hostName) { 772 | const protocol = "vless"; 773 | const vlessMain = 774 | `${protocol}` + 775 | `://${userID}@${hostName}:443`+ 776 | `?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`; 777 | 778 | return ` 779 | ################################################################ 780 | v2ray 781 | --------------------------------------------------------------- 782 | ${vlessMain} 783 | --------------------------------------------------------------- 784 | ################################################################ 785 | clash-meta 786 | --------------------------------------------------------------- 787 | - type: vless 788 | name: ${hostName} 789 | server: ${hostName} 790 | port: 443 791 | uuid: ${userID} 792 | network: ws 793 | tls: true 794 | udp: false 795 | sni: ${hostName} 796 | client-fingerprint: chrome 797 | ws-opts: 798 | path: "/?ed=2048" 799 | headers: 800 | host: ${hostName} 801 | --------------------------------------------------------------- 802 | ################################################################ 803 | `; 804 | } 805 | 806 | 807 | -------------------------------------------------------------------------------- /test/webstream.mjs: -------------------------------------------------------------------------------- 1 | let isCancel = false; 2 | const readableStream = new ReadableStream({ 3 | start(controller) { 4 | let count = 0; 5 | controller.enqueue(`Chunk ${count}`); 6 | count++; 7 | controller.enqueue(`Chunk ${count}`); 8 | 9 | // controller.error(new Error('uuid is not valid')); 10 | // setTimeout(() => { 11 | // console.log('ReadableStream was closed------valid22-------.'); 12 | // controller.error(new Error('uuid is not valid22')); 13 | // }, 1000); 14 | 15 | // const intervalId = setInterval(() => { 16 | // if(!isCancel){ 17 | // controller.enqueue(`Chunk ${count}`); 18 | // } 19 | // // controller.enqueue(`Chunk ${count}`); 20 | // count++; 21 | // if (count > 5) { 22 | // console.log('ReadableStream was closed-------------.'); 23 | // // controller.close() 24 | // controller.error(new Error('uuid is not valid')); 25 | // // clearInterval(intervalId); 26 | // } 27 | // }, 1000); 28 | }, 29 | async pull(controller) { 30 | console.log('ReadableStream Pulling data...'); 31 | // await new Promise((resolve) => setTimeout(resolve, 2000)); 32 | }, 33 | cancel() { 34 | isCancel = true; 35 | console.log('ReadableStream was canceled.'); 36 | }, 37 | }); 38 | 39 | const writableStream = new WritableStream({ 40 | write(chunk, controller) { 41 | console.log(`Received data: ${chunk}`); 42 | if(chunk === 'Chunk 1'){ 43 | controller.error('eroorooororo') 44 | return; 45 | } 46 | // throw new Error('uuid is not valid'); 47 | 48 | // setTimeout( ()=>{ 49 | // try { 50 | // throw new Error('setTimeout hasve error valid'); 51 | // }catch(error){ 52 | // console.log('////setTimeout hasve error valid'); 53 | // } 54 | 55 | // }, 2000) 56 | 57 | // controller.error(new Error('Received error')); 58 | if(chunk === 'Chunk 3'){ 59 | throw new Error('uuid is not valid'); 60 | } 61 | }, 62 | close() { 63 | console.log('WritableStream was closed'); 64 | }, 65 | abort() { 66 | console.log('WritableStream was aborted'); 67 | } 68 | }); 69 | 70 | readableStream.pipeTo(writableStream).catch((err) => { 71 | console.log('-----------------------error-------------------'); 72 | console.log(err); 73 | }); -------------------------------------------------------------------------------- /test/worker/cf-cdn-cgi-trace.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async fetch(request, env, ctx) { 3 | const url = new URL(request.url); 4 | const address = url.searchParams.get("address"); 5 | if(!address){ 6 | return new Response('not pass address', { status: 200 }); 7 | } 8 | const resp = fetch(`http://${address}/cdn-cgi/trace`); 9 | return new Response((await resp).body, { status: 200 }); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/worker/cf-worker-http-header.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * @param {import("@cloudflare/workers-types").Request} request 4 | * @param {{uuid: string}} env 5 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 6 | * @returns {Promise} 7 | */ 8 | async fetch(request, env, ctx) { 9 | const headers = {}; 10 | for (const [name, value] of request.headers.entries()) { 11 | headers[name] = value; 12 | } 13 | 14 | const result = { 15 | "http-header": headers, 16 | "cf": request.cf 17 | } 18 | const headersJson = JSON.stringify(result); 19 | console.log(headersJson); 20 | return new Response(headersJson, { status: 200 }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/worker/cidr.js: -------------------------------------------------------------------------------- 1 | 2 | import IPCIDR from 'ip-cidr'; 3 | 4 | const chunk = '0'.repeat(1024 * 5); 5 | export default { 6 | async fetch(request, env, ctx) { 7 | const isin = checkIPInCIDR("192.168.1.1", "102.1.5.2/24"); 8 | 9 | return new Response(null, { 10 | status: 101 11 | }); 12 | 13 | 14 | }, 15 | }; 16 | 17 | function checkIPInCIDR(ip, cidr) { 18 | const cidrObject = new IPCIDR(cidr); 19 | 20 | // Check if the IP address is valid 21 | // if (!cidrObject.isValidAddress(ip)) { 22 | // return false; 23 | // } 24 | 25 | // Check if the IP address is within the CIDR range 26 | return cidrObject.contains(ip); 27 | } 28 | 29 | function delay(ms) { 30 | return new Promise((resolve) => { 31 | setTimeout(resolve, ms) 32 | }) 33 | } 34 | 35 | 36 | 37 | /** 38 | * Checks if an IPv4 address is within a CIDR range. 39 | * 40 | * @param {string} address The IPv4 address to check. 41 | * @param {string} cidr The CIDR range to check against. 42 | * @returns {boolean} `true` if the address is within the CIDR range, `false` otherwise. 43 | */ 44 | function isIPv4InRange(address, cidr) { 45 | // Parse the address and CIDR range 46 | const addressParts = address.split('.').map(part => parseInt(part, 10)); 47 | const [rangeAddress, rangePrefix] = cidr.split('/'); 48 | const rangeParts = rangeAddress.split('.').map(part => parseInt(part, 10)); 49 | const prefix = parseInt(rangePrefix, 10); 50 | 51 | // Convert the address and range to binary format 52 | const addressBinary = addressParts.reduce((acc, part) => acc + part.toString(2).padStart(8, '0'), ''); 53 | const rangeBinary = rangeParts.reduce((acc, part) => acc + part.toString(2).padStart(8, '0'), ''); 54 | 55 | // Compare the bits up to the prefix length 56 | for (let i = 0; i < prefix; i++) { 57 | if (addressBinary[i] !== rangeBinary[i]) { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * Checks if an IPv6 address is within a CIDR range. 67 | * 68 | * @param {string} address The IPv6 address to check. 69 | * @param {string} cidr The CIDR range to check against. 70 | * @returns {boolean} `true` if the address is within the CIDR range, `false` otherwise. 71 | */ 72 | function isIPv6InRange(address, cidr) { 73 | // Parse the address and CIDR range 74 | const addressParts = address.split(':').map(part => parseInt(part, 16)); 75 | const [rangeAddress, rangePrefix] = cidr.split('/'); 76 | const rangeParts = rangeAddress.split(':').map(part => parseInt(part, 16)); 77 | const prefix = parseInt(rangePrefix, 10); 78 | 79 | // Convert the address and range to binary format 80 | const addressBinary = addressParts.reduce((acc, part) => acc + part.toString(2).padStart(16, '0'), ''); 81 | const rangeBinary = rangeParts.reduce((acc, part) => acc + part.toString(2).padStart(16, '0'), ''); 82 | 83 | // Compare the bits up to the prefix length 84 | for (let i = 0; i < prefix; i++) { 85 | if (addressBinary[i] !== rangeBinary[i]) { 86 | return false; 87 | } 88 | } 89 | 90 | return true; 91 | } 92 | 93 | 94 | -------------------------------------------------------------------------------- /test/worker/ipaddr-test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * @param {import("@cloudflare/workers-types").Request} request 4 | * @param {{uuid: string}} env 5 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 6 | * @returns {Promise} 7 | */ 8 | async fetch(request, env, ctx) { 9 | const headers = {}; 10 | for (const [name, value] of request.headers.entries()) { 11 | headers[name] = value; 12 | } 13 | 14 | const result = { 15 | "http-header": headers, 16 | "cf": request.cf 17 | } 18 | const headersJson = JSON.stringify(result); 19 | console.log(headersJson); 20 | const addr = ipaddr.parse('2001:db8:1234::1'); 21 | const range = ipaddr.parse('2001:db8::'); 22 | 23 | addr.match(range, 32); // => true 24 | return new Response(headersJson, { status: 200 }); 25 | } 26 | }; 27 | 28 | 29 | //ipadder.js 30 | (function (root) { 31 | 'use strict'; 32 | // A list of regular expressions that match arbitrary IPv4 addresses, 33 | // for which a number of weird notations exist. 34 | // Note that an address like 0010.0xa5.1.1 is considered legal. 35 | const ipv4Part = '(0?\\d+|0x[a-f0-9]+)'; 36 | const ipv4Regexes = { 37 | fourOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), 38 | threeOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), 39 | twoOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}$`, 'i'), 40 | longValue: new RegExp(`^${ipv4Part}$`, 'i') 41 | }; 42 | 43 | // Regular Expression for checking Octal numbers 44 | const octalRegex = new RegExp(`^0[0-7]+$`, 'i'); 45 | const hexRegex = new RegExp(`^0x[a-f0-9]+$`, 'i'); 46 | 47 | const zoneIndex = '%[0-9a-z]{1,}'; 48 | 49 | // IPv6-matching regular expressions. 50 | // For IPv6, the task is simpler: it is enough to match the colon-delimited 51 | // hexadecimal IPv6 and a transitional variant with dotted-decimal IPv4 at 52 | // the end. 53 | const ipv6Part = '(?:[0-9a-f]+::?)+'; 54 | const ipv6Regexes = { 55 | zoneIndex: new RegExp(zoneIndex, 'i'), 56 | 'native': new RegExp(`^(::)?(${ipv6Part})?([0-9a-f]+)?(::)?(${zoneIndex})?$`, 'i'), 57 | deprecatedTransitional: new RegExp(`^(?:::)(${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?)$`, 'i'), 58 | transitional: new RegExp(`^((?:${ipv6Part})|(?:::)(?:${ipv6Part})?)${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?$`, 'i') 59 | }; 60 | 61 | // Expand :: in an IPv6 address or address part consisting of `parts` groups. 62 | function expandIPv6 (string, parts) { 63 | // More than one '::' means invalid adddress 64 | if (string.indexOf('::') !== string.lastIndexOf('::')) { 65 | return null; 66 | } 67 | 68 | let colonCount = 0; 69 | let lastColon = -1; 70 | let zoneId = (string.match(ipv6Regexes.zoneIndex) || [])[0]; 71 | let replacement, replacementCount; 72 | 73 | // Remove zone index and save it for later 74 | if (zoneId) { 75 | zoneId = zoneId.substring(1); 76 | string = string.replace(/%.+$/, ''); 77 | } 78 | 79 | // How many parts do we already have? 80 | while ((lastColon = string.indexOf(':', lastColon + 1)) >= 0) { 81 | colonCount++; 82 | } 83 | 84 | // 0::0 is two parts more than :: 85 | if (string.substr(0, 2) === '::') { 86 | colonCount--; 87 | } 88 | 89 | if (string.substr(-2, 2) === '::') { 90 | colonCount--; 91 | } 92 | 93 | // The following loop would hang if colonCount > parts 94 | if (colonCount > parts) { 95 | return null; 96 | } 97 | 98 | // replacement = ':' + '0:' * (parts - colonCount) 99 | replacementCount = parts - colonCount; 100 | replacement = ':'; 101 | while (replacementCount--) { 102 | replacement += '0:'; 103 | } 104 | 105 | // Insert the missing zeroes 106 | string = string.replace('::', replacement); 107 | 108 | // Trim any garbage which may be hanging around if :: was at the edge in 109 | // the source strin 110 | if (string[0] === ':') { 111 | string = string.slice(1); 112 | } 113 | 114 | if (string[string.length - 1] === ':') { 115 | string = string.slice(0, -1); 116 | } 117 | 118 | parts = (function () { 119 | const ref = string.split(':'); 120 | const results = []; 121 | 122 | for (let i = 0; i < ref.length; i++) { 123 | results.push(parseInt(ref[i], 16)); 124 | } 125 | 126 | return results; 127 | })(); 128 | 129 | return { 130 | parts: parts, 131 | zoneId: zoneId 132 | }; 133 | } 134 | 135 | // A generic CIDR (Classless Inter-Domain Routing) RFC1518 range matcher. 136 | function matchCIDR (first, second, partSize, cidrBits) { 137 | if (first.length !== second.length) { 138 | throw new Error('ipaddr: cannot match CIDR for objects with different lengths'); 139 | } 140 | 141 | let part = 0; 142 | let shift; 143 | 144 | while (cidrBits > 0) { 145 | shift = partSize - cidrBits; 146 | if (shift < 0) { 147 | shift = 0; 148 | } 149 | 150 | if (first[part] >> shift !== second[part] >> shift) { 151 | return false; 152 | } 153 | 154 | cidrBits -= partSize; 155 | part += 1; 156 | } 157 | 158 | return true; 159 | } 160 | 161 | function parseIntAuto (string) { 162 | // Hexadedimal base 16 (0x#) 163 | if (hexRegex.test(string)) { 164 | return parseInt(string, 16); 165 | } 166 | // While octal representation is discouraged by ECMAScript 3 167 | // and forbidden by ECMAScript 5, we silently allow it to 168 | // work only if the rest of the string has numbers less than 8. 169 | if (string[0] === '0' && !isNaN(parseInt(string[1], 10))) { 170 | if (octalRegex.test(string)) { 171 | return parseInt(string, 8); 172 | } 173 | throw new Error(`ipaddr: cannot parse ${string} as octal`); 174 | } 175 | // Always include the base 10 radix! 176 | return parseInt(string, 10); 177 | } 178 | 179 | function padPart (part, length) { 180 | while (part.length < length) { 181 | part = `0${part}`; 182 | } 183 | 184 | return part; 185 | } 186 | 187 | const ipaddr = {}; 188 | 189 | // An IPv4 address (RFC791). 190 | ipaddr.IPv4 = (function () { 191 | // Constructs a new IPv4 address from an array of four octets 192 | // in network order (MSB first) 193 | // Verifies the input. 194 | function IPv4 (octets) { 195 | if (octets.length !== 4) { 196 | throw new Error('ipaddr: ipv4 octet count should be 4'); 197 | } 198 | 199 | let i, octet; 200 | 201 | for (i = 0; i < octets.length; i++) { 202 | octet = octets[i]; 203 | if (!((0 <= octet && octet <= 255))) { 204 | throw new Error('ipaddr: ipv4 octet should fit in 8 bits'); 205 | } 206 | } 207 | 208 | this.octets = octets; 209 | } 210 | 211 | // Special IPv4 address ranges. 212 | // See also https://en.wikipedia.org/wiki/Reserved_IP_addresses 213 | IPv4.prototype.SpecialRanges = { 214 | unspecified: [[new IPv4([0, 0, 0, 0]), 8]], 215 | broadcast: [[new IPv4([255, 255, 255, 255]), 32]], 216 | // RFC3171 217 | multicast: [[new IPv4([224, 0, 0, 0]), 4]], 218 | // RFC3927 219 | linkLocal: [[new IPv4([169, 254, 0, 0]), 16]], 220 | // RFC5735 221 | loopback: [[new IPv4([127, 0, 0, 0]), 8]], 222 | // RFC6598 223 | carrierGradeNat: [[new IPv4([100, 64, 0, 0]), 10]], 224 | // RFC1918 225 | 'private': [ 226 | [new IPv4([10, 0, 0, 0]), 8], 227 | [new IPv4([172, 16, 0, 0]), 12], 228 | [new IPv4([192, 168, 0, 0]), 16] 229 | ], 230 | // Reserved and testing-only ranges; RFCs 5735, 5737, 2544, 1700 231 | reserved: [ 232 | [new IPv4([192, 0, 0, 0]), 24], 233 | [new IPv4([192, 0, 2, 0]), 24], 234 | [new IPv4([192, 88, 99, 0]), 24], 235 | [new IPv4([198, 18, 0, 0]), 15], 236 | [new IPv4([198, 51, 100, 0]), 24], 237 | [new IPv4([203, 0, 113, 0]), 24], 238 | [new IPv4([240, 0, 0, 0]), 4] 239 | ] 240 | }; 241 | 242 | // The 'kind' method exists on both IPv4 and IPv6 classes. 243 | IPv4.prototype.kind = function () { 244 | return 'ipv4'; 245 | }; 246 | 247 | // Checks if this address matches other one within given CIDR range. 248 | IPv4.prototype.match = function (other, cidrRange) { 249 | let ref; 250 | if (cidrRange === undefined) { 251 | ref = other; 252 | other = ref[0]; 253 | cidrRange = ref[1]; 254 | } 255 | 256 | if (other.kind() !== 'ipv4') { 257 | throw new Error('ipaddr: cannot match ipv4 address with non-ipv4 one'); 258 | } 259 | 260 | return matchCIDR(this.octets, other.octets, 8, cidrRange); 261 | }; 262 | 263 | // returns a number of leading ones in IPv4 address, making sure that 264 | // the rest is a solid sequence of 0's (valid netmask) 265 | // returns either the CIDR length or null if mask is not valid 266 | IPv4.prototype.prefixLengthFromSubnetMask = function () { 267 | let cidr = 0; 268 | // non-zero encountered stop scanning for zeroes 269 | let stop = false; 270 | // number of zeroes in octet 271 | const zerotable = { 272 | 0: 8, 273 | 128: 7, 274 | 192: 6, 275 | 224: 5, 276 | 240: 4, 277 | 248: 3, 278 | 252: 2, 279 | 254: 1, 280 | 255: 0 281 | }; 282 | let i, octet, zeros; 283 | 284 | for (i = 3; i >= 0; i -= 1) { 285 | octet = this.octets[i]; 286 | if (octet in zerotable) { 287 | zeros = zerotable[octet]; 288 | if (stop && zeros !== 0) { 289 | return null; 290 | } 291 | 292 | if (zeros !== 8) { 293 | stop = true; 294 | } 295 | 296 | cidr += zeros; 297 | } else { 298 | return null; 299 | } 300 | } 301 | 302 | return 32 - cidr; 303 | }; 304 | 305 | // Checks if the address corresponds to one of the special ranges. 306 | IPv4.prototype.range = function () { 307 | return ipaddr.subnetMatch(this, this.SpecialRanges); 308 | }; 309 | 310 | // Returns an array of byte-sized values in network order (MSB first) 311 | IPv4.prototype.toByteArray = function () { 312 | return this.octets.slice(0); 313 | }; 314 | 315 | // Converts this IPv4 address to an IPv4-mapped IPv6 address. 316 | IPv4.prototype.toIPv4MappedAddress = function () { 317 | return ipaddr.IPv6.parse(`::ffff:${this.toString()}`); 318 | }; 319 | 320 | // Symmetrical method strictly for aligning with the IPv6 methods. 321 | IPv4.prototype.toNormalizedString = function () { 322 | return this.toString(); 323 | }; 324 | 325 | // Returns the address in convenient, decimal-dotted format. 326 | IPv4.prototype.toString = function () { 327 | return this.octets.join('.'); 328 | }; 329 | 330 | return IPv4; 331 | })(); 332 | 333 | // A utility function to return broadcast address given the IPv4 interface and prefix length in CIDR notation 334 | ipaddr.IPv4.broadcastAddressFromCIDR = function (string) { 335 | 336 | try { 337 | const cidr = this.parseCIDR(string); 338 | const ipInterfaceOctets = cidr[0].toByteArray(); 339 | const subnetMaskOctets = this.subnetMaskFromPrefixLength(cidr[1]).toByteArray(); 340 | const octets = []; 341 | let i = 0; 342 | while (i < 4) { 343 | // Broadcast address is bitwise OR between ip interface and inverted mask 344 | octets.push(parseInt(ipInterfaceOctets[i], 10) | parseInt(subnetMaskOctets[i], 10) ^ 255); 345 | i++; 346 | } 347 | 348 | return new this(octets); 349 | } catch (e) { 350 | throw new Error('ipaddr: the address does not have IPv4 CIDR format'); 351 | } 352 | }; 353 | 354 | // Checks if a given string is formatted like IPv4 address. 355 | ipaddr.IPv4.isIPv4 = function (string) { 356 | return this.parser(string) !== null; 357 | }; 358 | 359 | // Checks if a given string is a valid IPv4 address. 360 | ipaddr.IPv4.isValid = function (string) { 361 | try { 362 | new this(this.parser(string)); 363 | return true; 364 | } catch (e) { 365 | return false; 366 | } 367 | }; 368 | 369 | // Checks if a given string is a full four-part IPv4 Address. 370 | ipaddr.IPv4.isValidFourPartDecimal = function (string) { 371 | if (ipaddr.IPv4.isValid(string) && string.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/)) { 372 | return true; 373 | } else { 374 | return false; 375 | } 376 | }; 377 | 378 | // A utility function to return network address given the IPv4 interface and prefix length in CIDR notation 379 | ipaddr.IPv4.networkAddressFromCIDR = function (string) { 380 | let cidr, i, ipInterfaceOctets, octets, subnetMaskOctets; 381 | 382 | try { 383 | cidr = this.parseCIDR(string); 384 | ipInterfaceOctets = cidr[0].toByteArray(); 385 | subnetMaskOctets = this.subnetMaskFromPrefixLength(cidr[1]).toByteArray(); 386 | octets = []; 387 | i = 0; 388 | while (i < 4) { 389 | // Network address is bitwise AND between ip interface and mask 390 | octets.push(parseInt(ipInterfaceOctets[i], 10) & parseInt(subnetMaskOctets[i], 10)); 391 | i++; 392 | } 393 | 394 | return new this(octets); 395 | } catch (e) { 396 | throw new Error('ipaddr: the address does not have IPv4 CIDR format'); 397 | } 398 | }; 399 | 400 | // Tries to parse and validate a string with IPv4 address. 401 | // Throws an error if it fails. 402 | ipaddr.IPv4.parse = function (string) { 403 | const parts = this.parser(string); 404 | 405 | if (parts === null) { 406 | throw new Error('ipaddr: string is not formatted like an IPv4 Address'); 407 | } 408 | 409 | return new this(parts); 410 | }; 411 | 412 | // Parses the string as an IPv4 Address with CIDR Notation. 413 | ipaddr.IPv4.parseCIDR = function (string) { 414 | let match; 415 | 416 | if ((match = string.match(/^(.+)\/(\d+)$/))) { 417 | const maskLength = parseInt(match[2]); 418 | if (maskLength >= 0 && maskLength <= 32) { 419 | const parsed = [this.parse(match[1]), maskLength]; 420 | Object.defineProperty(parsed, 'toString', { 421 | value: function () { 422 | return this.join('/'); 423 | } 424 | }); 425 | return parsed; 426 | } 427 | } 428 | 429 | throw new Error('ipaddr: string is not formatted like an IPv4 CIDR range'); 430 | }; 431 | 432 | // Classful variants (like a.b, where a is an octet, and b is a 24-bit 433 | // value representing last three octets; this corresponds to a class C 434 | // address) are omitted due to classless nature of modern Internet. 435 | ipaddr.IPv4.parser = function (string) { 436 | let match, part, value; 437 | 438 | // parseInt recognizes all that octal & hexadecimal weirdness for us 439 | if ((match = string.match(ipv4Regexes.fourOctet))) { 440 | return (function () { 441 | const ref = match.slice(1, 6); 442 | const results = []; 443 | 444 | for (let i = 0; i < ref.length; i++) { 445 | part = ref[i]; 446 | results.push(parseIntAuto(part)); 447 | } 448 | 449 | return results; 450 | })(); 451 | } else if ((match = string.match(ipv4Regexes.longValue))) { 452 | value = parseIntAuto(match[1]); 453 | if (value > 0xffffffff || value < 0) { 454 | throw new Error('ipaddr: address outside defined range'); 455 | } 456 | 457 | return ((function () { 458 | const results = []; 459 | let shift; 460 | 461 | for (shift = 0; shift <= 24; shift += 8) { 462 | results.push((value >> shift) & 0xff); 463 | } 464 | 465 | return results; 466 | })()).reverse(); 467 | } else if ((match = string.match(ipv4Regexes.twoOctet))) { 468 | return (function () { 469 | const ref = match.slice(1, 4); 470 | const results = []; 471 | 472 | value = parseIntAuto(ref[1]); 473 | if (value > 0xffffff || value < 0) { 474 | throw new Error('ipaddr: address outside defined range'); 475 | } 476 | 477 | results.push(parseIntAuto(ref[0])); 478 | results.push((value >> 16) & 0xff); 479 | results.push((value >> 8) & 0xff); 480 | results.push( value & 0xff); 481 | 482 | return results; 483 | })(); 484 | } else if ((match = string.match(ipv4Regexes.threeOctet))) { 485 | return (function () { 486 | const ref = match.slice(1, 5); 487 | const results = []; 488 | 489 | value = parseIntAuto(ref[2]); 490 | if (value > 0xffff || value < 0) { 491 | throw new Error('ipaddr: address outside defined range'); 492 | } 493 | 494 | results.push(parseIntAuto(ref[0])); 495 | results.push(parseIntAuto(ref[1])); 496 | results.push((value >> 8) & 0xff); 497 | results.push( value & 0xff); 498 | 499 | return results; 500 | })(); 501 | } else { 502 | return null; 503 | } 504 | }; 505 | 506 | // A utility function to return subnet mask in IPv4 format given the prefix length 507 | ipaddr.IPv4.subnetMaskFromPrefixLength = function (prefix) { 508 | prefix = parseInt(prefix); 509 | if (prefix < 0 || prefix > 32) { 510 | throw new Error('ipaddr: invalid IPv4 prefix length'); 511 | } 512 | 513 | const octets = [0, 0, 0, 0]; 514 | let j = 0; 515 | const filledOctetCount = Math.floor(prefix / 8); 516 | 517 | while (j < filledOctetCount) { 518 | octets[j] = 255; 519 | j++; 520 | } 521 | 522 | if (filledOctetCount < 4) { 523 | octets[filledOctetCount] = Math.pow(2, prefix % 8) - 1 << 8 - (prefix % 8); 524 | } 525 | 526 | return new this(octets); 527 | }; 528 | 529 | // An IPv6 address (RFC2460) 530 | ipaddr.IPv6 = (function () { 531 | // Constructs an IPv6 address from an array of eight 16 - bit parts 532 | // or sixteen 8 - bit parts in network order(MSB first). 533 | // Throws an error if the input is invalid. 534 | function IPv6 (parts, zoneId) { 535 | let i, part; 536 | 537 | if (parts.length === 16) { 538 | this.parts = []; 539 | for (i = 0; i <= 14; i += 2) { 540 | this.parts.push((parts[i] << 8) | parts[i + 1]); 541 | } 542 | } else if (parts.length === 8) { 543 | this.parts = parts; 544 | } else { 545 | throw new Error('ipaddr: ipv6 part count should be 8 or 16'); 546 | } 547 | 548 | for (i = 0; i < this.parts.length; i++) { 549 | part = this.parts[i]; 550 | if (!((0 <= part && part <= 0xffff))) { 551 | throw new Error('ipaddr: ipv6 part should fit in 16 bits'); 552 | } 553 | } 554 | 555 | if (zoneId) { 556 | this.zoneId = zoneId; 557 | } 558 | } 559 | 560 | // Special IPv6 ranges 561 | IPv6.prototype.SpecialRanges = { 562 | // RFC4291, here and after 563 | unspecified: [new IPv6([0, 0, 0, 0, 0, 0, 0, 0]), 128], 564 | linkLocal: [new IPv6([0xfe80, 0, 0, 0, 0, 0, 0, 0]), 10], 565 | multicast: [new IPv6([0xff00, 0, 0, 0, 0, 0, 0, 0]), 8], 566 | loopback: [new IPv6([0, 0, 0, 0, 0, 0, 0, 1]), 128], 567 | uniqueLocal: [new IPv6([0xfc00, 0, 0, 0, 0, 0, 0, 0]), 7], 568 | ipv4Mapped: [new IPv6([0, 0, 0, 0, 0, 0xffff, 0, 0]), 96], 569 | // RFC6145 570 | rfc6145: [new IPv6([0, 0, 0, 0, 0xffff, 0, 0, 0]), 96], 571 | // RFC6052 572 | rfc6052: [new IPv6([0x64, 0xff9b, 0, 0, 0, 0, 0, 0]), 96], 573 | // RFC3056 574 | '6to4': [new IPv6([0x2002, 0, 0, 0, 0, 0, 0, 0]), 16], 575 | // RFC6052, RFC6146 576 | teredo: [new IPv6([0x2001, 0, 0, 0, 0, 0, 0, 0]), 32], 577 | // RFC4291 578 | reserved: [[new IPv6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]), 32]], 579 | benchmarking: [new IPv6([0x2001, 0x2, 0, 0, 0, 0, 0, 0]), 48], 580 | amt: [new IPv6([0x2001, 0x3, 0, 0, 0, 0, 0, 0]), 32], 581 | as112v6: [new IPv6([0x2001, 0x4, 0x112, 0, 0, 0, 0, 0]), 48], 582 | deprecated: [new IPv6([0x2001, 0x10, 0, 0, 0, 0, 0, 0]), 28], 583 | orchid2: [new IPv6([0x2001, 0x20, 0, 0, 0, 0, 0, 0]), 28] 584 | }; 585 | 586 | // Checks if this address is an IPv4-mapped IPv6 address. 587 | IPv6.prototype.isIPv4MappedAddress = function () { 588 | return this.range() === 'ipv4Mapped'; 589 | }; 590 | 591 | // The 'kind' method exists on both IPv4 and IPv6 classes. 592 | IPv6.prototype.kind = function () { 593 | return 'ipv6'; 594 | }; 595 | 596 | // Checks if this address matches other one within given CIDR range. 597 | IPv6.prototype.match = function (other, cidrRange) { 598 | let ref; 599 | 600 | if (cidrRange === undefined) { 601 | ref = other; 602 | other = ref[0]; 603 | cidrRange = ref[1]; 604 | } 605 | 606 | if (other.kind() !== 'ipv6') { 607 | throw new Error('ipaddr: cannot match ipv6 address with non-ipv6 one'); 608 | } 609 | 610 | return matchCIDR(this.parts, other.parts, 16, cidrRange); 611 | }; 612 | 613 | // returns a number of leading ones in IPv6 address, making sure that 614 | // the rest is a solid sequence of 0's (valid netmask) 615 | // returns either the CIDR length or null if mask is not valid 616 | IPv6.prototype.prefixLengthFromSubnetMask = function () { 617 | let cidr = 0; 618 | // non-zero encountered stop scanning for zeroes 619 | let stop = false; 620 | // number of zeroes in octet 621 | const zerotable = { 622 | 0: 16, 623 | 32768: 15, 624 | 49152: 14, 625 | 57344: 13, 626 | 61440: 12, 627 | 63488: 11, 628 | 64512: 10, 629 | 65024: 9, 630 | 65280: 8, 631 | 65408: 7, 632 | 65472: 6, 633 | 65504: 5, 634 | 65520: 4, 635 | 65528: 3, 636 | 65532: 2, 637 | 65534: 1, 638 | 65535: 0 639 | }; 640 | let part, zeros; 641 | 642 | for (let i = 7; i >= 0; i -= 1) { 643 | part = this.parts[i]; 644 | if (part in zerotable) { 645 | zeros = zerotable[part]; 646 | if (stop && zeros !== 0) { 647 | return null; 648 | } 649 | 650 | if (zeros !== 16) { 651 | stop = true; 652 | } 653 | 654 | cidr += zeros; 655 | } else { 656 | return null; 657 | } 658 | } 659 | 660 | return 128 - cidr; 661 | }; 662 | 663 | 664 | // Checks if the address corresponds to one of the special ranges. 665 | IPv6.prototype.range = function () { 666 | return ipaddr.subnetMatch(this, this.SpecialRanges); 667 | }; 668 | 669 | // Returns an array of byte-sized values in network order (MSB first) 670 | IPv6.prototype.toByteArray = function () { 671 | let part; 672 | const bytes = []; 673 | const ref = this.parts; 674 | for (let i = 0; i < ref.length; i++) { 675 | part = ref[i]; 676 | bytes.push(part >> 8); 677 | bytes.push(part & 0xff); 678 | } 679 | 680 | return bytes; 681 | }; 682 | 683 | // Returns the address in expanded format with all zeroes included, like 684 | // 2001:0db8:0008:0066:0000:0000:0000:0001 685 | IPv6.prototype.toFixedLengthString = function () { 686 | const addr = ((function () { 687 | const results = []; 688 | for (let i = 0; i < this.parts.length; i++) { 689 | results.push(padPart(this.parts[i].toString(16), 4)); 690 | } 691 | 692 | return results; 693 | }).call(this)).join(':'); 694 | 695 | let suffix = ''; 696 | 697 | if (this.zoneId) { 698 | suffix = `%${this.zoneId}`; 699 | } 700 | 701 | return addr + suffix; 702 | }; 703 | 704 | // Converts this address to IPv4 address if it is an IPv4-mapped IPv6 address. 705 | // Throws an error otherwise. 706 | IPv6.prototype.toIPv4Address = function () { 707 | if (!this.isIPv4MappedAddress()) { 708 | throw new Error('ipaddr: trying to convert a generic ipv6 address to ipv4'); 709 | } 710 | 711 | const ref = this.parts.slice(-2); 712 | const high = ref[0]; 713 | const low = ref[1]; 714 | 715 | return new ipaddr.IPv4([high >> 8, high & 0xff, low >> 8, low & 0xff]); 716 | }; 717 | 718 | // Returns the address in expanded format with all zeroes included, like 719 | // 2001:db8:8:66:0:0:0:1 720 | // 721 | // Deprecated: use toFixedLengthString() instead. 722 | IPv6.prototype.toNormalizedString = function () { 723 | const addr = ((function () { 724 | const results = []; 725 | 726 | for (let i = 0; i < this.parts.length; i++) { 727 | results.push(this.parts[i].toString(16)); 728 | } 729 | 730 | return results; 731 | }).call(this)).join(':'); 732 | 733 | let suffix = ''; 734 | 735 | if (this.zoneId) { 736 | suffix = `%${this.zoneId}`; 737 | } 738 | 739 | return addr + suffix; 740 | }; 741 | 742 | // Returns the address in compact, human-readable format like 743 | // 2001:db8:8:66::1 744 | // in line with RFC 5952 (see https://tools.ietf.org/html/rfc5952#section-4) 745 | IPv6.prototype.toRFC5952String = function () { 746 | const regex = /((^|:)(0(:|$)){2,})/g; 747 | const string = this.toNormalizedString(); 748 | let bestMatchIndex = 0; 749 | let bestMatchLength = -1; 750 | let match; 751 | 752 | while ((match = regex.exec(string))) { 753 | if (match[0].length > bestMatchLength) { 754 | bestMatchIndex = match.index; 755 | bestMatchLength = match[0].length; 756 | } 757 | } 758 | 759 | if (bestMatchLength < 0) { 760 | return string; 761 | } 762 | 763 | return `${string.substring(0, bestMatchIndex)}::${string.substring(bestMatchIndex + bestMatchLength)}`; 764 | }; 765 | 766 | // Returns the address in compact, human-readable format like 767 | // 2001:db8:8:66::1 768 | // Calls toRFC5952String under the hood. 769 | IPv6.prototype.toString = function () { 770 | return this.toRFC5952String(); 771 | }; 772 | 773 | return IPv6; 774 | 775 | })(); 776 | 777 | // A utility function to return broadcast address given the IPv6 interface and prefix length in CIDR notation 778 | ipaddr.IPv6.broadcastAddressFromCIDR = function (string) { 779 | try { 780 | const cidr = this.parseCIDR(string); 781 | const ipInterfaceOctets = cidr[0].toByteArray(); 782 | const subnetMaskOctets = this.subnetMaskFromPrefixLength(cidr[1]).toByteArray(); 783 | const octets = []; 784 | let i = 0; 785 | while (i < 16) { 786 | // Broadcast address is bitwise OR between ip interface and inverted mask 787 | octets.push(parseInt(ipInterfaceOctets[i], 10) | parseInt(subnetMaskOctets[i], 10) ^ 255); 788 | i++; 789 | } 790 | 791 | return new this(octets); 792 | } catch (e) { 793 | throw new Error(`ipaddr: the address does not have IPv6 CIDR format (${e})`); 794 | } 795 | }; 796 | 797 | // Checks if a given string is formatted like IPv6 address. 798 | ipaddr.IPv6.isIPv6 = function (string) { 799 | return this.parser(string) !== null; 800 | }; 801 | 802 | // Checks to see if string is a valid IPv6 Address 803 | ipaddr.IPv6.isValid = function (string) { 804 | 805 | // Since IPv6.isValid is always called first, this shortcut 806 | // provides a substantial performance gain. 807 | if (typeof string === 'string' && string.indexOf(':') === -1) { 808 | return false; 809 | } 810 | 811 | try { 812 | const addr = this.parser(string); 813 | new this(addr.parts, addr.zoneId); 814 | return true; 815 | } catch (e) { 816 | return false; 817 | } 818 | }; 819 | 820 | // A utility function to return network address given the IPv6 interface and prefix length in CIDR notation 821 | ipaddr.IPv6.networkAddressFromCIDR = function (string) { 822 | let cidr, i, ipInterfaceOctets, octets, subnetMaskOctets; 823 | 824 | try { 825 | cidr = this.parseCIDR(string); 826 | ipInterfaceOctets = cidr[0].toByteArray(); 827 | subnetMaskOctets = this.subnetMaskFromPrefixLength(cidr[1]).toByteArray(); 828 | octets = []; 829 | i = 0; 830 | while (i < 16) { 831 | // Network address is bitwise AND between ip interface and mask 832 | octets.push(parseInt(ipInterfaceOctets[i], 10) & parseInt(subnetMaskOctets[i], 10)); 833 | i++; 834 | } 835 | 836 | return new this(octets); 837 | } catch (e) { 838 | throw new Error(`ipaddr: the address does not have IPv6 CIDR format (${e})`); 839 | } 840 | }; 841 | 842 | // Tries to parse and validate a string with IPv6 address. 843 | // Throws an error if it fails. 844 | ipaddr.IPv6.parse = function (string) { 845 | const addr = this.parser(string); 846 | 847 | if (addr.parts === null) { 848 | throw new Error('ipaddr: string is not formatted like an IPv6 Address'); 849 | } 850 | 851 | return new this(addr.parts, addr.zoneId); 852 | }; 853 | 854 | ipaddr.IPv6.parseCIDR = function (string) { 855 | let maskLength, match, parsed; 856 | 857 | if ((match = string.match(/^(.+)\/(\d+)$/))) { 858 | maskLength = parseInt(match[2]); 859 | if (maskLength >= 0 && maskLength <= 128) { 860 | parsed = [this.parse(match[1]), maskLength]; 861 | Object.defineProperty(parsed, 'toString', { 862 | value: function () { 863 | return this.join('/'); 864 | } 865 | }); 866 | return parsed; 867 | } 868 | } 869 | 870 | throw new Error('ipaddr: string is not formatted like an IPv6 CIDR range'); 871 | }; 872 | 873 | // Parse an IPv6 address. 874 | ipaddr.IPv6.parser = function (string) { 875 | let addr, i, match, octet, octets, zoneId; 876 | 877 | if ((match = string.match(ipv6Regexes.deprecatedTransitional))) { 878 | return this.parser(`::ffff:${match[1]}`); 879 | } 880 | if (ipv6Regexes.native.test(string)) { 881 | return expandIPv6(string, 8); 882 | } 883 | if ((match = string.match(ipv6Regexes.transitional))) { 884 | zoneId = match[6] || ''; 885 | addr = expandIPv6(match[1].slice(0, -1) + zoneId, 6); 886 | if (addr.parts) { 887 | octets = [ 888 | parseInt(match[2]), 889 | parseInt(match[3]), 890 | parseInt(match[4]), 891 | parseInt(match[5]) 892 | ]; 893 | for (i = 0; i < octets.length; i++) { 894 | octet = octets[i]; 895 | if (!((0 <= octet && octet <= 255))) { 896 | return null; 897 | } 898 | } 899 | 900 | addr.parts.push(octets[0] << 8 | octets[1]); 901 | addr.parts.push(octets[2] << 8 | octets[3]); 902 | return { 903 | parts: addr.parts, 904 | zoneId: addr.zoneId 905 | }; 906 | } 907 | } 908 | 909 | return null; 910 | }; 911 | 912 | // A utility function to return subnet mask in IPv6 format given the prefix length 913 | ipaddr.IPv6.subnetMaskFromPrefixLength = function (prefix) { 914 | prefix = parseInt(prefix); 915 | if (prefix < 0 || prefix > 128) { 916 | throw new Error('ipaddr: invalid IPv6 prefix length'); 917 | } 918 | 919 | const octets = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 920 | let j = 0; 921 | const filledOctetCount = Math.floor(prefix / 8); 922 | 923 | while (j < filledOctetCount) { 924 | octets[j] = 255; 925 | j++; 926 | } 927 | 928 | if (filledOctetCount < 16) { 929 | octets[filledOctetCount] = Math.pow(2, prefix % 8) - 1 << 8 - (prefix % 8); 930 | } 931 | 932 | return new this(octets); 933 | }; 934 | 935 | // Try to parse an array in network order (MSB first) for IPv4 and IPv6 936 | ipaddr.fromByteArray = function (bytes) { 937 | const length = bytes.length; 938 | 939 | if (length === 4) { 940 | return new ipaddr.IPv4(bytes); 941 | } else if (length === 16) { 942 | return new ipaddr.IPv6(bytes); 943 | } else { 944 | throw new Error('ipaddr: the binary input is neither an IPv6 nor IPv4 address'); 945 | } 946 | }; 947 | 948 | // Checks if the address is valid IP address 949 | ipaddr.isValid = function (string) { 950 | return ipaddr.IPv6.isValid(string) || ipaddr.IPv4.isValid(string); 951 | }; 952 | 953 | 954 | // Attempts to parse an IP Address, first through IPv6 then IPv4. 955 | // Throws an error if it could not be parsed. 956 | ipaddr.parse = function (string) { 957 | if (ipaddr.IPv6.isValid(string)) { 958 | return ipaddr.IPv6.parse(string); 959 | } else if (ipaddr.IPv4.isValid(string)) { 960 | return ipaddr.IPv4.parse(string); 961 | } else { 962 | throw new Error('ipaddr: the address has neither IPv6 nor IPv4 format'); 963 | } 964 | }; 965 | 966 | // Attempt to parse CIDR notation, first through IPv6 then IPv4. 967 | // Throws an error if it could not be parsed. 968 | ipaddr.parseCIDR = function (string) { 969 | try { 970 | return ipaddr.IPv6.parseCIDR(string); 971 | } catch (e) { 972 | try { 973 | return ipaddr.IPv4.parseCIDR(string); 974 | } catch (e2) { 975 | throw new Error('ipaddr: the address has neither IPv6 nor IPv4 CIDR format'); 976 | } 977 | } 978 | }; 979 | 980 | // Parse an address and return plain IPv4 address if it is an IPv4-mapped address 981 | ipaddr.process = function (string) { 982 | const addr = this.parse(string); 983 | 984 | if (addr.kind() === 'ipv6' && addr.isIPv4MappedAddress()) { 985 | return addr.toIPv4Address(); 986 | } else { 987 | return addr; 988 | } 989 | }; 990 | 991 | // An utility function to ease named range matching. See examples below. 992 | // rangeList can contain both IPv4 and IPv6 subnet entries and will not throw errors 993 | // on matching IPv4 addresses to IPv6 ranges or vice versa. 994 | ipaddr.subnetMatch = function (address, rangeList, defaultName) { 995 | let i, rangeName, rangeSubnets, subnet; 996 | 997 | if (defaultName === undefined || defaultName === null) { 998 | defaultName = 'unicast'; 999 | } 1000 | 1001 | for (rangeName in rangeList) { 1002 | if (Object.prototype.hasOwnProperty.call(rangeList, rangeName)) { 1003 | rangeSubnets = rangeList[rangeName]; 1004 | // ECMA5 Array.isArray isn't available everywhere 1005 | if (rangeSubnets[0] && !(rangeSubnets[0] instanceof Array)) { 1006 | rangeSubnets = [rangeSubnets]; 1007 | } 1008 | 1009 | for (i = 0; i < rangeSubnets.length; i++) { 1010 | subnet = rangeSubnets[i]; 1011 | if (address.kind() === subnet[0].kind() && address.match.apply(address, subnet)) { 1012 | return rangeName; 1013 | } 1014 | } 1015 | } 1016 | } 1017 | 1018 | return defaultName; 1019 | }; 1020 | 1021 | // Export for both the CommonJS and browser-like environment 1022 | if (typeof module !== 'undefined' && module.exports) { 1023 | module.exports = ipaddr; 1024 | 1025 | } else { 1026 | root.ipaddr = ipaddr; 1027 | } 1028 | 1029 | }(this)); -------------------------------------------------------------------------------- /test/worker/stream-ws-test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async fetch(request, env, ctx) { 3 | const upgradeHeader = request.headers.get('Upgrade'); 4 | if (!upgradeHeader || upgradeHeader !== 'websocket') { 5 | return new Response('not websocket', { status: 200 }); 6 | } 7 | const webSocketPair = new WebSocketPair(); 8 | const [client, webSocket] = Object.values(webSocketPair); 9 | webSocket.accept(); 10 | let count = 0; 11 | const readableStream = new ReadableStream({ 12 | start(controller) { 13 | setInterval(() => { 14 | controller.enqueue(count); 15 | count++; 16 | }, 500) 17 | 18 | }, 19 | async pull(controller) { 20 | }, 21 | cancel() { 22 | console.log('ReadableStream was canceled.'); 23 | }, 24 | }); 25 | 26 | const writableStream = new WritableStream({ 27 | write(chunk, controller) { 28 | console.log(`Received data: ${chunk}`); 29 | webSocket.send(`Received data: ${chunk}`); 30 | if (chunk === 3) { 31 | controller.error('eroorooororo') 32 | return; 33 | } 34 | 35 | }, 36 | close() { 37 | console.log('WritableStream was closed'); 38 | }, 39 | abort() { 40 | console.log('WritableStream was aborted'); 41 | } 42 | }); 43 | readableStream.pipeTo(writableStream).catch((error) => { 44 | console.log('pipeTo error', error); 45 | webSocket.close(); 46 | }); 47 | webSocket.addEventListener('close', () => { 48 | console.log('close'); 49 | }); 50 | return new Response(null, { 51 | status: 101, 52 | webSocket: client, 53 | }); 54 | } 55 | }; -------------------------------------------------------------------------------- /test/worker/worker-connect-test.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'cloudflare:sockets'; 2 | 3 | export default { 4 | async fetch(request, env, ctx) { 5 | console.log('start fetch111'); 6 | const url = new URL(request.url); 7 | const target = url.searchParams.get('target'); 8 | // if (!target) { 9 | // return new Response('target is empty', { 10 | // status: 500, 11 | // }); 12 | // } 13 | try { 14 | 15 | try { 16 | /** @type {import("@cloudflare/workers-types").Socket}*/ 17 | const socket = connect( 18 | { 19 | hostname: target, 20 | port: 443, 21 | } 22 | ); 23 | 24 | // socket.closed.then(() => { 25 | // console.log('....socket.closed.then............'); 26 | // }).catch((e) => { 27 | // console.log('.........socket.closed.error.............', e); 28 | // }).finally(() => { 29 | // console.log('.........socket.closed.finally.............'); 30 | // }) 31 | // console.log('---------------close-------'); 32 | 33 | // socket.readable.getReader().closed.then(() => { 34 | // console.log('.........socket.readabl.....closed then.............'); 35 | // }).catch((e) => { 36 | // console.log('....socket.readabl.....catch closing.............', e); 37 | // }) 38 | 39 | await socket.writable.getWriter().write(new Uint8Array([1,2,3,4,5,6,7,8,9,10])) 40 | 41 | // await delay(10) 42 | 43 | } catch (e) { 44 | console.log('connect error', e); 45 | } 46 | console.log('start conneted', target); 47 | 48 | 49 | 50 | 51 | // const writer = socket.writable.getWriter(); 52 | // const encoder = new TextEncoder(); 53 | // const encoded = encoder.encode( 54 | // `GET / HTTP/1.1\r\nHost: ${target}\r\nUser-Agent: curl/8.0.1\r\nAccept: */*\r\n\r\n` 55 | // ); 56 | // await writer.write(encoded); 57 | // // await writer.close(); 58 | // console.log('write end'); 59 | 60 | // await delay(1) 61 | return new Response('yyyyyyyyyyyyyyyyyyyyyyyyyy', { 62 | headers: { 'Content-Type': 'text/plain' }, 63 | status: 500, 64 | }); 65 | } catch (error) { 66 | console.log('Socket connection failed: ' + error); 67 | return new Response('Socket connection failed: ' + error, { 68 | status: 500, 69 | }); 70 | } 71 | }, 72 | }; 73 | 74 | function delay(timeout) { 75 | return new Promise((resolve) => { 76 | setTimeout(resolve, timeout); 77 | }); 78 | } -------------------------------------------------------------------------------- /test/worker/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cf-worker-http-header" # todo 2 | main = "./cf-worker-http-header.js" 3 | compatibility_date = "2023-05-26" 4 | 5 | [vars] 6 | UUID = "example_dev_token" -------------------------------------------------------------------------------- /test/worker/ws-send-issue.js: -------------------------------------------------------------------------------- 1 | 2 | const chunk = '0'.repeat(1024 * 5); 3 | export default { 4 | async fetch(request, env, ctx) { 5 | try { 6 | console.log('---------------'); 7 | const webSocketPair = new WebSocketPair(); 8 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */ 9 | const [client, webSocket] = Object.values(webSocketPair); 10 | webSocket.accept(); 11 | let btyes = 0; 12 | // (async () => { 13 | // const repose = await fetch('http://speed.cloudflare.com/__down?bytes=1145141919810') 14 | // const body = repose.body; 15 | // const reader = body?.getReader(); 16 | // let packets = []; 17 | // while (true && reader) { 18 | // const { done, value } = await reader.read(); 19 | // packets.push(value); 20 | // console.log(btyes += value?.length || 0); 21 | // if (packets.length > 100) { 22 | // webSocket.send(value || ''); 23 | // await delay(2); 24 | // packets = []; 25 | // } 26 | // if (done) { 27 | // break; 28 | // } 29 | // } 30 | // })() 31 | console.log('---------------'); 32 | (async () => { 33 | let packets = []; 34 | console.log('---------------'); 35 | while (true) { 36 | console.log(btyes += chunk?.length || 0); 37 | webSocket.send(chunk || ''); 38 | await delay(1) 39 | } 40 | })() 41 | // console.log(btyes += chunk?.length || 0); 42 | // webSocket.send(chunk || ''); 43 | 44 | return new Response(null, { 45 | status: 101, 46 | webSocket: client, 47 | }); 48 | 49 | } catch (err) { 50 | /** @type {Error} */ let e = err; 51 | return new Response(e.toString()); 52 | } 53 | }, 54 | }; 55 | 56 | function delay(ms) { 57 | return new Promise((resolve) => { 58 | setTimeout(resolve, ms) 59 | }) 60 | } -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cf-worker-ws-dev-0415" # todo 2 | #name = "cf-worker-connect-test" # todo 3 | #main = "test/worker/cf-cdn-cgi-trace2.js" 4 | 5 | #main = "test/worker/worker-connect-test.js" 6 | main = "src/worker-vless.js" 7 | 8 | compatibility_date = "2024-04-15" 9 | 10 | [vars] 11 | # UUID = "example_dev_token" 12 | --------------------------------------------------------------------------------