mirror of
https://github.com/unchainese/unchain.git
synced 2026-04-22 15:27:03 +08:00
init
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "$SSH_KEY" > key.pem
|
||||
chmod 600 key.pem
|
||||
|
||||
scp -o StrictHostKeyChecking=no -i key.pem cmd/node/unchain cmd/node/unchain.service $SSH_USER@$SSH_HOST:~
|
||||
ssh -o StrictHostKeyChecking=no -i key.pem $SSH_USER@$SSH_HOST << EOF
|
||||
cd ~ && pwd
|
||||
sudo rm -rf /app && sudo mkdir /app
|
||||
sudo mv unchain /app/unchain
|
||||
sudo chmod +x /app/unchain
|
||||
echo "$CONFIG_TOML" | sudo tee /app/config.toml > /dev/null
|
||||
sudo mv unchain.service /etc/systemd/system/unchain.service
|
||||
sudo rm -rf /etc/systemd/system/emissary.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl stop emissary.service || true
|
||||
sudo systemctl stop unchain.service || true
|
||||
sudo systemctl start unchain.service
|
||||
sudo systemctl status unchain.service
|
||||
EOF
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
env:
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
GIT_REF_NAME: ${{ github.ref_name }}
|
||||
GIT_REPO: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: install ssh-scp
|
||||
run: sudo apt-get install -y openssh-client
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Get build time
|
||||
id: build-time
|
||||
run: echo "buildTime=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CGO_ENABLED: '1'
|
||||
GOARCH: 'amd64'
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
GIT_REF_NAME: ${{ github.ref_name }}
|
||||
GIT_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
echo "Building Go application with gitHash=${{ env.gitHash }} and buildTime=${{ env.buildTime }}"
|
||||
go build -ldflags="-X 'github.com/unchainese/unchain/internal/global.gitHash=${{ github.sha }}' -X 'github.com/unchainese/unchain/internal/global.buildTime=${{ env.buildTime }}'" -o cmd/node/unchain cmd/node/main.go
|
||||
|
||||
- name: Deploy to AWS EC2
|
||||
env:
|
||||
SSH_HOST: ${{ secrets.EC2_HOST }}
|
||||
SSH_USER: ${{ secrets.EC2_USER }}
|
||||
SSH_KEY: ${{ secrets.EC2_KEY }}
|
||||
CONFIG_TOML: ${{ vars.CONFIG_TOML }}
|
||||
run: bash .github/workflows/deploy.sh
|
||||
|
||||
|
||||
- name: Deploy to AliCould ECS
|
||||
env:
|
||||
SSH_HOST: ${{ secrets.ECS_HOST }}
|
||||
SSH_USER: ${{ secrets.ECS_USER }}
|
||||
SSH_KEY: ${{ secrets.ECS_KEY }}
|
||||
CONFIG_TOML: ${{ vars.CONFIG_TOML }}
|
||||
run: bash .github/workflows/deploy.sh
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
*.log
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,67 @@
|
||||
# unchain
|
||||
|
||||
"Unchainese" – a blend of "unchain" and "Chinese" – represents breaking free from internet restrictions and embracing true digital freedom.
|
||||
|
||||
Unchain is design in Go to be a simple and easy to use proxy server
|
||||
that can be used to bypass network restrictions,censorship and surveillance.
|
||||
|
||||
|
||||
Unchain accepts traffic from the client eg: v2rayN,v2rayA,v2rayNG,shadowRocket etc.
|
||||
Processes the traffic and forwards it to the destination server.
|
||||
|
||||
## Unchain Architecture
|
||||
|
||||
Unchain is a dead simple VLESS over websocket proxy server.
|
||||
The core biz logic is only 200 lines of code. [app_ws_vless.go](/internal/node/app_ws_vless.go).
|
||||
|
||||
Unchain server uses a simple architecture that is VLESS over WebSocket (WS) + TLS.
|
||||
|
||||
|
||||
```
|
||||
V2rayN,V2rayA,Clash or ShadowRocket
|
||||
+------------------+
|
||||
| VLESS Client |
|
||||
| +-----------+ |
|
||||
| | TLS Layer | |
|
||||
| +-----------+ |
|
||||
| | WebSocket | |
|
||||
| +-----------+ |
|
||||
+--------|---------+
|
||||
|
|
||||
| Encrypted VLESS Traffic (wss://)
|
||||
|
|
||||
+--------------------------------------+
|
||||
| Internet (TLS Secured) |
|
||||
+--------------------------------------+
|
||||
|
|
||||
|
|
||||
+-----------------------------------+
|
||||
| Reverse Proxy Server |
|
||||
| (e.g., Nginx or Cloudflare) |
|
||||
| |
|
||||
| +---------------------------+ |
|
||||
| | HTTPS/TLS Termination | |
|
||||
| +---------------------------+ |
|
||||
| | WebSocket Proxy (wss://) | |
|
||||
| +---------------------------+ |
|
||||
| Forward to VLESS Server |
|
||||
+------------------|----------------+
|
||||
|
|
||||
+--------------------------------+
|
||||
| Unchain Server |
|
||||
| |
|
||||
| +------------------------+ |
|
||||
| | WebSocket Handler | |
|
||||
| +------------------------+ |
|
||||
| | VLESS Core Processing | |
|
||||
| +------------------------+ |
|
||||
| |
|
||||
| Forward Traffic to Target |
|
||||
+------------------|-------------+
|
||||
|
|
||||
+-----------------+
|
||||
| Target Server |
|
||||
| or Destination |
|
||||
+-----------------+
|
||||
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
config.toml
|
||||
@@ -0,0 +1,10 @@
|
||||
#
|
||||
#
|
||||
# Warnning use ' for string not " if you want to use deploy.sh
|
||||
SubAddresses = ['n-us1.libgragen.cn:80'] # host can be visited by internet,addr of cloudflare or nginx
|
||||
ListenAddr = '0.0.0.0:80' # websocket server listen address
|
||||
RegisterUrl = '' #the master admin server for user auth,data traffic. can be empty if you only want to use the node for yourself
|
||||
RegisterToken = ''# can be empty string if you only want to use the node for yourself
|
||||
AllowUsers = '6fe57e3f-e618-4873-ba96-a76adec22ccd,6fe57e3f-e618-4873-ba96-a76adec22cce' # UUID string eg. '6fe57e3f-e618-4873-ba96-a76adec22ccd,6fe57e3f-e618-4873-ba96-a76adec22cce' can not be empty if you want to use the node for yourself in standalone mode
|
||||
LogFile = '' # can be empty if you don't want to log to file, so the log will be print to stdout
|
||||
DebugLevel = 'debug' # debug, info, warn, error
|
||||
@@ -0,0 +1,10 @@
|
||||
#
|
||||
#
|
||||
# Warnning use ' for string not " if you want to use deploy.sh
|
||||
SubAddresses = ['cf-us1.libragen.cn:443', 'n-us1.libgragen.cn:80'] # hosts for generate vless URL
|
||||
ListenAddr = '0.0.0.0:80' # websocket server listen address
|
||||
RegisterUrl = 'https://admin.cf.workers.cn/api/nodes' #the master admin server for user auth,data traffic. can be empty if you only want to use the node for stand alone
|
||||
RegisterToken = 'unchain.people.from.censorship.and.surveillance'# can be empty string if you only want to use the node for yourself
|
||||
AllowUsers = '' # UUID string eg. '6fe57e3f-e618-4873-ba96-a76adec22ccd,6fe57e3f-e618-4873-ba96-a76adec22cce' can not be empty if you want to use the node for yourself in standalone mode
|
||||
LogFile = 'unchain.log' # can be empty if you don't want to log to file, so the log will be print to stdout
|
||||
DebugLevel = 'debug' # debug, info, warn, error
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/unchainese/unchain/internal/global"
|
||||
"github.com/unchainese/unchain/internal/node"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := global.Cfg()
|
||||
fd := global.SetupLogger(c)
|
||||
defer fd.Close()
|
||||
|
||||
// Channel to listen for OS signals
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
|
||||
app := node.NewApp(c, stop)
|
||||
app.PushNode()
|
||||
app.PrintVLESSConnectionURLS()
|
||||
go app.Run()
|
||||
<-stop
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
app.Shutdown(ctx)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Emissary Proxy Node Sever of Vless over Websocket
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/app/unchain
|
||||
Restart=always
|
||||
User=root
|
||||
WorkingDirectory=/app
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,10 @@
|
||||
module github.com/unchainese/unchain
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
)
|
||||
|
||||
require github.com/BurntSushi/toml v1.4.0
|
||||
@@ -0,0 +1,6 @@
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@@ -0,0 +1,103 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/BurntSushi/toml"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SubAddresses []string `desc:"sub addresses" example:"node1.xxx.cn:80,node2.xxx.cn:443"`
|
||||
ListenAddr string `desc:"net listen addr" def:"0.0.0.0:80"`
|
||||
RegisterUrl string `desc:"register url" def:"https://admin.unchain.people.from.censorship"`
|
||||
RegisterToken string `desc:"register token" def:"unchain people from censorship and surveillance"`
|
||||
AllowUsers string `desc:"allow users" def:"" example:"903bcd04-79e7-429c-bf0c-0456c7de9cdc,903bcd04-79e7-429c-bf0c-0456c7de9cd1"`
|
||||
LogFile string `desc:"log file path" def:""`
|
||||
DebugLevel string `desc:"debug level" def:"DEBUG"`
|
||||
PushIntervalSecond int `desc:"push interval" def:"360"` //seconds
|
||||
GitHash string `desc:"git hash" def:""`
|
||||
BuildTime string `desc:"build time" def:""`
|
||||
}
|
||||
|
||||
func (c Config) ListenPort() int {
|
||||
parts := strings.Split(c.ListenAddr, ":")
|
||||
if len(parts) < 2 {
|
||||
return 80
|
||||
}
|
||||
iv, err := strconv.ParseInt(parts[1], 10, 32)
|
||||
if err != nil {
|
||||
log.Println("failed to parse port:", err)
|
||||
return 80
|
||||
}
|
||||
return int(iv)
|
||||
}
|
||||
|
||||
var (
|
||||
gitHash string
|
||||
buildTime string
|
||||
)
|
||||
|
||||
var cfg *Config
|
||||
|
||||
func Cfg() *Config {
|
||||
if cfg != nil {
|
||||
return cfg
|
||||
}
|
||||
cfgIns, err := loadFromToml("config.toml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
cfg = cfgIns
|
||||
}
|
||||
cfg.GitHash = gitHash
|
||||
cfg.BuildTime = buildTime
|
||||
return cfg
|
||||
}
|
||||
|
||||
func loadFromToml(file string) (*Config, error) {
|
||||
opt := Config{}
|
||||
_, err := toml.DecodeFile(file, &opt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load config file:%s %w", file, err)
|
||||
}
|
||||
return &opt, nil
|
||||
}
|
||||
|
||||
func (c Config) LogLevel() slog.Level {
|
||||
l := slog.LevelDebug
|
||||
switch strings.ToUpper(c.DebugLevel) {
|
||||
case "DEBUG":
|
||||
l = slog.LevelDebug
|
||||
case "INFO":
|
||||
l = slog.LevelInfo
|
||||
case "WARN":
|
||||
l = slog.LevelWarn
|
||||
case "ERROR":
|
||||
l = slog.LevelError
|
||||
default:
|
||||
l = slog.LevelError
|
||||
}
|
||||
return l
|
||||
}
|
||||
func (c Config) UserIDS() []string {
|
||||
parts := strings.Split(c.AllowUsers, ",")
|
||||
ids := make([]string, 0)
|
||||
for _, uid := range parts {
|
||||
uid = strings.TrimSpace(uid)
|
||||
if uid != "" {
|
||||
ids = append(ids, uid)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (c Config) PushInterval() time.Duration {
|
||||
if c.PushIntervalSecond <= 0 {
|
||||
return time.Minute * 60
|
||||
}
|
||||
return time.Second * time.Duration(c.PushIntervalSecond)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
func SetupLogger(c *Config) (fd *os.File) {
|
||||
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
|
||||
slog.SetLogLoggerLevel(c.LogLevel())
|
||||
fd = os.Stdout
|
||||
if c.LogFile != "" {
|
||||
file, err := os.OpenFile(c.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Println("Failed to open log file:", err, c.LogFile)
|
||||
} else {
|
||||
fd = file
|
||||
}
|
||||
}
|
||||
log.SetOutput(fd)
|
||||
return fd
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/unchainese/unchain/internal/global"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg *global.Config
|
||||
mu sync.Mutex
|
||||
allowedUsers map[string]int64
|
||||
trafficUserKB sync.Map
|
||||
reqCount atomic.Int64
|
||||
svr *http.Server
|
||||
exitSignal chan os.Signal
|
||||
}
|
||||
|
||||
func (app *App) httpSvr() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/wsv/{uid}", app.WsVLESS)
|
||||
mux.HandleFunc("/sub/{uid}", app.Sub)
|
||||
mux.HandleFunc("/ws-vless", app.WsVLESS)
|
||||
mux.HandleFunc("/", app.Ping)
|
||||
server := &http.Server{
|
||||
Addr: app.cfg.ListenAddr,
|
||||
Handler: mux,
|
||||
}
|
||||
app.svr = server
|
||||
|
||||
}
|
||||
|
||||
func NewApp(c *global.Config, sig chan os.Signal) *App {
|
||||
app := &App{
|
||||
cfg: c,
|
||||
mu: sync.Mutex{},
|
||||
allowedUsers: make(map[string]int64),
|
||||
trafficUserKB: sync.Map{},
|
||||
reqCount: atomic.Int64{},
|
||||
exitSignal: sig,
|
||||
svr: nil,
|
||||
}
|
||||
for _, userID := range c.UserIDS() {
|
||||
app.allowedUsers[userID] = 1
|
||||
}
|
||||
app.httpSvr()
|
||||
go app.loopPush()
|
||||
return app
|
||||
}
|
||||
|
||||
func (app *App) Run() {
|
||||
log.Println("server starting on http://", app.cfg.ListenAddr)
|
||||
if err := app.svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("Could not listen on %s: %v\n", app.cfg.ListenAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) PrintVLESSConnectionURLS() {
|
||||
listenPort := app.cfg.ListenPort()
|
||||
|
||||
fmt.Printf("\n\n\nvist to get VLESS connection info: http://127.0.0.1:%d/sub/<YOUR_CONFIGED_UUID> \n", listenPort)
|
||||
fmt.Printf("vist to get VLESS connection info: http://<HOST>:%d/sub/<YOUR_UUID>\n", listenPort)
|
||||
|
||||
for userID, _ := range app.allowedUsers {
|
||||
fmt.Println("\n------------- USER UUID: ", userID, " -------------")
|
||||
urls := app.vlessUrls(userID)
|
||||
for _, url := range urls {
|
||||
fmt.Println(url)
|
||||
}
|
||||
}
|
||||
fmt.Println("\n\n\n")
|
||||
}
|
||||
|
||||
func (app *App) Shutdown(ctx context.Context) {
|
||||
log.Println("Shutting down the server...")
|
||||
if err := app.svr.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
log.Println("Server exiting")
|
||||
}
|
||||
|
||||
func (app *App) loopPush() {
|
||||
url := app.cfg.RegisterUrl
|
||||
if url == "" {
|
||||
log.Println("Register url is empty, skip register, runs in standalone mode")
|
||||
return
|
||||
}
|
||||
tk := time.NewTicker(app.cfg.PushInterval())
|
||||
defer tk.Stop()
|
||||
for {
|
||||
select {
|
||||
case sig := <-app.exitSignal:
|
||||
app.exitSignal <- sig
|
||||
app.PushNode() //last push
|
||||
return
|
||||
case <-tk.C:
|
||||
app.PushNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) reqInc() {
|
||||
app.reqCount.Add(1)
|
||||
}
|
||||
|
||||
func (app *App) trafficInc(uid string, byteN int64) {
|
||||
kb := byteN/1024 + 1 //floor
|
||||
value, ok := app.trafficUserKB.Load(uid)
|
||||
if !ok {
|
||||
app.trafficUserKB.Store(uid, kb)
|
||||
return
|
||||
}
|
||||
app.trafficUserKB.Store(uid, value.(int64)+kb)
|
||||
}
|
||||
|
||||
func (app *App) stat() *AppStat {
|
||||
data := make(map[string]int64)
|
||||
app.trafficUserKB.Range(func(key, value interface{}) bool {
|
||||
data[key.(string)] = value.(int64)
|
||||
return true
|
||||
})
|
||||
app.trafficUserKB.Clear()
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = "unknown"
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
res := &AppStat{
|
||||
Traffic: data,
|
||||
Hostname: hostname,
|
||||
ReqCount: app.reqCount.Load(),
|
||||
Goroutine: int64(runtime.NumGoroutine()),
|
||||
VersionInfo: app.cfg.GitHash + " -> " + app.cfg.BuildTime,
|
||||
}
|
||||
res.SubAddresses = app.cfg.SubAddresses
|
||||
app.reqCount.Store(0)
|
||||
return res
|
||||
}
|
||||
|
||||
type AppStat struct {
|
||||
Traffic map[string]int64 `json:"traffic"`
|
||||
Hostname string `json:"hostname"`
|
||||
SubAddresses []string `json:"sub_addresses"`
|
||||
ReqCount int64 `json:"req_count"`
|
||||
Goroutine int64 `json:"goroutine"`
|
||||
VersionInfo string `json:"version_info"`
|
||||
}
|
||||
|
||||
func (app *App) PushNode() {
|
||||
url := app.cfg.RegisterUrl
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
args := app.stat()
|
||||
body := bytes.NewBuffer(nil)
|
||||
err := json.NewEncoder(body).Encode(args)
|
||||
if err != nil {
|
||||
log.Println("Error encoding request:", err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
log.Println("Error registering:", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", app.cfg.RegisterToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Error registering:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
users := make(map[string]int64)
|
||||
err = json.NewDecoder(resp.Body).Decode(&users)
|
||||
if err != nil {
|
||||
log.Println("Error decoding response:", err)
|
||||
return
|
||||
}
|
||||
app.mu.Lock()
|
||||
app.allowedUsers = users
|
||||
app.mu.Unlock()
|
||||
}
|
||||
|
||||
func (app *App) IsUserNotAllowed(uuid string) (isNotAllowed bool) {
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
_, ok := app.allowedUsers[uuid]
|
||||
if !ok {
|
||||
log.Println("Unauthorized user:", uuid)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (app *App) Stat(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
all, err := json.Marshal(app.stat())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
//json response hello world
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(all)
|
||||
}
|
||||
|
||||
func (app *App) Ping(w http.ResponseWriter, _ *http.Request) {
|
||||
//json response hello world
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
lines := []string{
|
||||
"BUILT HASH: https://github.com/unchainese/unchain/tree/" + app.cfg.GitHash,
|
||||
"BUILT TIME: " + app.cfg.BuildTime,
|
||||
}
|
||||
w.Write([]byte(strings.Join(lines, "\n\n")))
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//vless://6fe57e3f-e618-4873-ba96-a76ad1112ccd@felix-us.xxx.cn:443?
|
||||
//encryption=none&
|
||||
//security=tls&
|
||||
//sni=felix-us.xxx.cn&
|
||||
//allowInsecure=1&
|
||||
//type=ws&
|
||||
//hostSni=felix-us.xxx.cn
|
||||
//&path=%2Fws-vless%3Fed%3D2560
|
||||
//#felix-us.xxx.cn
|
||||
|
||||
// vless://6fe57e3f-e618-4873-ba96-a76ad1112ccd@aws.xxx.cn:80?encryption=none&security=none&sni=s5cf.xxx.cn&allowInsecure=1
|
||||
// &type=ws
|
||||
// &hostSni=aws.xxx.cn&path=%2Fws-vless%3Fed%3D2560#locaol-clone
|
||||
type vlessSub struct {
|
||||
remark string
|
||||
addrWithPort string //eg node.cloudflare.cn:443 or node.cloudflare.cn:80
|
||||
UID string
|
||||
path string //eg /ws-vless?ed=2560
|
||||
}
|
||||
|
||||
func (s vlessSub) vlessURL(hostSni string, isTLS bool) string {
|
||||
|
||||
u := url.Values{
|
||||
"encryption": {"none"},
|
||||
"allowInsecure": {"1"},
|
||||
"type": {"ws"},
|
||||
"path": {s.path},
|
||||
}
|
||||
if hostSni != "" {
|
||||
u["host"] = []string{hostSni}
|
||||
u["sni"] = []string{hostSni}
|
||||
}
|
||||
|
||||
if !isTLS {
|
||||
u["security"] = []string{"none"}
|
||||
u.Del("sni")
|
||||
} else {
|
||||
u["security"] = []string{"tls"}
|
||||
}
|
||||
//&security=none&allowInsecure=1&type=ws&path=#n-cn1.libgragen.cn%3A80
|
||||
return fmt.Sprintf("vless://%s@%s?%s#%s", s.UID, s.addrWithPort, u.Encode(), s.remark)
|
||||
}
|
||||
|
||||
func (app *App) Sub(w http.ResponseWriter, r *http.Request) {
|
||||
uid := r.PathValue("uid")
|
||||
if app.IsUserNotAllowed(uid) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
subURLs := app.vlessUrls(uid)
|
||||
|
||||
//json response hello world
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
lines := []string{
|
||||
app.cfg.GitHash,
|
||||
app.cfg.BuildTime,
|
||||
"VLESS Subscription URL:",
|
||||
}
|
||||
lines = append(lines, subURLs...)
|
||||
w.Write([]byte(strings.Join(lines, "\n\n")))
|
||||
}
|
||||
|
||||
func (app *App) vlessUrls(uid string) []string {
|
||||
var subURLs []string
|
||||
for _, subAddr := range app.cfg.SubAddresses {
|
||||
sub := vlessSub{
|
||||
remark: subAddr,
|
||||
addrWithPort: subAddr,
|
||||
UID: uid,
|
||||
path: "/wsv/" + uid + "?ed=2560",
|
||||
}
|
||||
isTLS := strings.HasSuffix(subAddr, ":443")
|
||||
subURL := sub.vlessURL("", isTLS)
|
||||
subURLs = append(subURLs, subURL)
|
||||
}
|
||||
return subURLs
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/unchainese/unchain/internal/schema"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const buffSize = 8 << 10
|
||||
|
||||
var upGrader = websocket.Upgrader{
|
||||
ReadBufferSize: buffSize,
|
||||
WriteBufferSize: buffSize,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// Allow all connections by default
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func startDstConnection(vd *schema.ProtoVLESS, timeout time.Duration) (net.Conn, []byte, error) {
|
||||
conn, err := net.DialTimeout(vd.DstProtocol, vd.HostPort(), timeout)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connecting to destination: %w", err)
|
||||
}
|
||||
return conn, []byte{vd.Version, 0x00}, nil
|
||||
}
|
||||
|
||||
func (app *App) WsVLESS(w http.ResponseWriter, r *http.Request) {
|
||||
app.reqInc()
|
||||
uid := r.PathValue("uid")
|
||||
//check can upgrade websocket
|
||||
if r.Header.Get("Upgrade") != "websocket" {
|
||||
//json response hello world
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
data := map[string]string{"msg": "pong", "uid": uid}
|
||||
json.NewEncoder(w).Encode(data)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
earlyDataHeader := r.Header.Get("sec-websocket-protocol")
|
||||
earlyData, err := base64.RawURLEncoding.DecodeString(earlyDataHeader)
|
||||
if err != nil {
|
||||
log.Println("Error decoding early data:", err)
|
||||
}
|
||||
|
||||
ws, err := upGrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error upgrading to websocket:", err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
if len(earlyData) == 0 {
|
||||
mt, p, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading message:", err)
|
||||
return
|
||||
}
|
||||
if mt == websocket.BinaryMessage {
|
||||
earlyData = p
|
||||
}
|
||||
}
|
||||
|
||||
vData, err := schema.VlessParse(earlyData)
|
||||
if err != nil {
|
||||
log.Println("Error parsing vless data:", err)
|
||||
return
|
||||
}
|
||||
if app.IsUserNotAllowed(vData.UUID()) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionTrafficByteN := int64(len(earlyData))
|
||||
|
||||
if vData.DstProtocol == "udp" {
|
||||
sessionTrafficByteN += vlessUDP(ctx, vData, ws)
|
||||
} else if vData.DstProtocol == "tcp" {
|
||||
sessionTrafficByteN += vlessTCP(ctx, vData, ws)
|
||||
}
|
||||
app.trafficInc(vData.UUID(), sessionTrafficByteN)
|
||||
}
|
||||
|
||||
func vlessTCP(_ context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) int64 {
|
||||
logger := sv.Logger()
|
||||
conn, headerVLESS, err := startDstConnection(sv, time.Millisecond*1000)
|
||||
if err != nil {
|
||||
logger.Error("Error starting session:", "err", err)
|
||||
return 0
|
||||
}
|
||||
defer conn.Close()
|
||||
logger.Info("Session started tcp")
|
||||
|
||||
//write early data
|
||||
_, err = conn.Write(sv.DataTcp())
|
||||
if err != nil {
|
||||
logger.Error("Error writing early data to TCP connection:", "err", err)
|
||||
return 0
|
||||
}
|
||||
var trafficMeter atomic.Int64
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
mt, message, err := ws.ReadMessage()
|
||||
trafficMeter.Add(int64(len(message)))
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("Error reading message:", "err", err)
|
||||
return
|
||||
}
|
||||
if mt != websocket.BinaryMessage {
|
||||
continue
|
||||
}
|
||||
_, err = conn.Write(message)
|
||||
if err != nil {
|
||||
logger.Error("Error writing to TCP connection:", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hasNotSentHeader := true
|
||||
buf := make([]byte, buffSize)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
trafficMeter.Add(int64(n))
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("Error reading from TCP connection:", "err", err)
|
||||
return
|
||||
}
|
||||
data := buf[:n]
|
||||
// send header data only for the first time
|
||||
if hasNotSentHeader {
|
||||
hasNotSentHeader = false
|
||||
data = append(headerVLESS, data...)
|
||||
}
|
||||
err = ws.WriteMessage(websocket.BinaryMessage, data)
|
||||
if err != nil {
|
||||
logger.Error("Error writing to websocket:", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return trafficMeter.Load()
|
||||
}
|
||||
|
||||
func vlessUDP(_ context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) (trafficMeter int64) {
|
||||
logger := sv.Logger()
|
||||
conn, headerVLESS, err := startDstConnection(sv, time.Millisecond*1000)
|
||||
if err != nil {
|
||||
logger.Error("Error starting session:", "err", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
trafficMeter += int64(len(sv.DataUdp()))
|
||||
//write early data
|
||||
_, err = conn.Write(sv.DataUdp())
|
||||
if err != nil {
|
||||
logger.Error("Error writing early data to TCP connection:", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, buffSize)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
logger.Error("Error reading from TCP connection:", "err", err)
|
||||
return
|
||||
}
|
||||
udpDataLen1 := (n >> 8) & 0xff
|
||||
udpDataLen2 := n & 0xff
|
||||
headerVLESS = append(headerVLESS, byte(udpDataLen1), byte(udpDataLen2))
|
||||
headerVLESS = append(headerVLESS, buf[:n]...)
|
||||
|
||||
trafficMeter += int64(len(headerVLESS))
|
||||
err = ws.WriteMessage(websocket.BinaryMessage, headerVLESS)
|
||||
if err != nil {
|
||||
logger.Error("Error writing to websocket:", "err", err)
|
||||
return
|
||||
}
|
||||
return trafficMeter
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ProtoTrojan is the structure of trojan protocol
|
||||
// https://github.com/trojan-gfw/trojan/blob/master/docs/protocol.md
|
||||
type ProtoTrojan struct {
|
||||
sha224password []byte //trojan password
|
||||
DstProtocol string //tcp or udp
|
||||
dstHost string
|
||||
dstHostType string //ipv6 or ipv4,domain
|
||||
dstPort uint16
|
||||
Version byte
|
||||
payload []byte
|
||||
}
|
||||
|
||||
const (
|
||||
byteCR = '\r'
|
||||
byteLF = '\n'
|
||||
)
|
||||
|
||||
func parseTrojanHeader(buffer []byte) (*ProtoTrojan, error) {
|
||||
if len(buffer) < 56 {
|
||||
return nil, errors.New("invalid data")
|
||||
}
|
||||
bytes.Split(buffer, []byte{byteCR, byteLF})
|
||||
|
||||
crLfIndex := 56
|
||||
if buffer[56] != byteCR || buffer[57] != byteLF {
|
||||
return nil, errors.New("invalid header format (missing CR LF)")
|
||||
}
|
||||
p := &ProtoTrojan{
|
||||
sha224password: buffer[:crLfIndex],
|
||||
}
|
||||
|
||||
socks5DataBuffer := buffer[crLfIndex+2:]
|
||||
if len(socks5DataBuffer) < 6 {
|
||||
return nil, errors.New("invalid SOCKS5 request data")
|
||||
}
|
||||
|
||||
cmd := socks5DataBuffer[0]
|
||||
if cmd == 0x01 { //connect
|
||||
p.DstProtocol = "tcp"
|
||||
} else if cmd == 0x03 { //udp
|
||||
p.DstProtocol = "udp"
|
||||
//todo:: udp
|
||||
} else {
|
||||
return nil, errors.New("unsupported command, only TCP (CONNECT) is allowed")
|
||||
}
|
||||
|
||||
atype := socks5DataBuffer[1]
|
||||
var addressLength int
|
||||
addressIndex := 2
|
||||
switch atype {
|
||||
case 1:
|
||||
addressLength = 4
|
||||
ip := net.IP(socks5DataBuffer[addressIndex : addressIndex+addressLength])
|
||||
p.dstHost = ip.String()
|
||||
p.dstHostType = "ipv4"
|
||||
p.dstPort = binary.BigEndian.Uint16(socks5DataBuffer[addressIndex+addressLength : addressIndex+addressLength+2])
|
||||
p.payload = socks5DataBuffer[addressIndex+addressLength+4:]
|
||||
case 3: //domain
|
||||
addressLength = int(socks5DataBuffer[addressIndex])
|
||||
addressIndex++
|
||||
p.dstHostType = "domain"
|
||||
p.dstHost = string(socks5DataBuffer[addressIndex : addressIndex+addressLength])
|
||||
p.dstPort = binary.BigEndian.Uint16(socks5DataBuffer[addressIndex+addressLength : addressIndex+addressLength+2])
|
||||
p.payload = socks5DataBuffer[addressIndex+addressLength+4:]
|
||||
case 4:
|
||||
addressLength = 16
|
||||
ip := net.IP(socks5DataBuffer[addressIndex : addressIndex+addressLength])
|
||||
p.dstPort = binary.BigEndian.Uint16(socks5DataBuffer[addressIndex+addressLength : addressIndex+addressLength+2])
|
||||
p.dstHost = ip.String()
|
||||
p.dstHostType = "ipv6"
|
||||
p.payload = socks5DataBuffer[addressIndex+addressLength+4:]
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid addressType is %d", atype)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"log/slog"
|
||||
"net"
|
||||
)
|
||||
|
||||
type ProtoVLESS struct {
|
||||
userID uuid.UUID
|
||||
DstProtocol string //tcp or udp
|
||||
dstHost string
|
||||
dstHostType string //ipv6 or ipv4,domain
|
||||
dstPort uint16
|
||||
Version byte
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func (p ProtoTrojan) AuthUser(password string) (isOk bool) {
|
||||
sha224Hash := sha256.New224()
|
||||
sha224Hash.Write([]byte(password))
|
||||
sha224Sum := sha224Hash.Sum(nil) //28 bytes
|
||||
hexSha224Bytes := []byte(fmt.Sprintf("%x", sha224Sum))
|
||||
return bytes.Equal(p.sha224password, hexSha224Bytes)
|
||||
}
|
||||
|
||||
func (h ProtoVLESS) UUID() string {
|
||||
return h.userID.String()
|
||||
}
|
||||
|
||||
func (h ProtoVLESS) DataUdp() []byte {
|
||||
allData := make([]byte, 0)
|
||||
chunk := h.payload
|
||||
for index := 0; index < len(chunk); {
|
||||
if index+2 > len(chunk) {
|
||||
fmt.Println("Incomplete length buffer")
|
||||
return nil
|
||||
}
|
||||
lengthBuffer := chunk[index : index+2]
|
||||
udpPacketLength := binary.BigEndian.Uint16(lengthBuffer)
|
||||
if index+2+int(udpPacketLength) > len(chunk) {
|
||||
fmt.Println("Incomplete UDP packet")
|
||||
return nil
|
||||
}
|
||||
udpData := chunk[index+2 : index+2+int(udpPacketLength)]
|
||||
index = index + 2 + int(udpPacketLength)
|
||||
allData = append(allData, udpData...)
|
||||
}
|
||||
return allData
|
||||
}
|
||||
func (h ProtoVLESS) DataTcp() []byte {
|
||||
return h.payload
|
||||
}
|
||||
|
||||
func (h ProtoVLESS) AddrUdp() *net.UDPAddr {
|
||||
return &net.UDPAddr{IP: h.HostIP(), Port: int(h.dstPort)}
|
||||
}
|
||||
func (h ProtoVLESS) HostIP() net.IP {
|
||||
ip := net.ParseIP(h.dstHost)
|
||||
if ip == nil {
|
||||
ips, err := net.LookupIP(h.dstHost)
|
||||
if err != nil {
|
||||
h.Logger().Error("failed to resolve domain", "err", err.Error())
|
||||
return net.IPv4zero
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return net.IPv4zero
|
||||
}
|
||||
return ips[0]
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (h ProtoVLESS) HostPort() string {
|
||||
return net.JoinHostPort(h.dstHost, fmt.Sprintf("%d", h.dstPort))
|
||||
}
|
||||
func (h ProtoVLESS) Logger() *slog.Logger {
|
||||
return slog.With("userID", h.userID.String(), "network", h.DstProtocol, "addr", h.HostPort())
|
||||
}
|
||||
|
||||
// VlessParse https://xtls.github.io/development/protocols/vless.html
|
||||
func VlessParse(buf []byte) (*ProtoVLESS, error) {
|
||||
payload := &ProtoVLESS{
|
||||
userID: uuid.Nil,
|
||||
DstProtocol: "",
|
||||
dstHost: "",
|
||||
dstPort: 0,
|
||||
Version: 0,
|
||||
payload: nil,
|
||||
}
|
||||
|
||||
if len(buf) < 24 {
|
||||
return payload, errors.New("invalid payload length")
|
||||
}
|
||||
|
||||
payload.Version = buf[0]
|
||||
payload.userID = uuid.Must(uuid.FromBytes(buf[1:17]))
|
||||
extraInfoProtoBufLen := buf[17]
|
||||
|
||||
command := buf[18+extraInfoProtoBufLen]
|
||||
switch command {
|
||||
case 1:
|
||||
payload.DstProtocol = "tcp"
|
||||
case 2:
|
||||
payload.DstProtocol = "udp"
|
||||
default:
|
||||
return payload, fmt.Errorf("command %d is not supported, command 01-tcp, 02-udp, 03-mux", command)
|
||||
}
|
||||
|
||||
portIndex := 18 + extraInfoProtoBufLen + 1
|
||||
payload.dstPort = binary.BigEndian.Uint16(buf[portIndex : portIndex+2])
|
||||
|
||||
addressIndex := portIndex + 2
|
||||
addressType := buf[addressIndex]
|
||||
addressValueIndex := addressIndex + 1
|
||||
|
||||
switch addressType {
|
||||
case 1: // IPv4
|
||||
if len(buf) < int(addressValueIndex+net.IPv4len) {
|
||||
return nil, fmt.Errorf("invalid IPv4 address length")
|
||||
}
|
||||
payload.dstHost = net.IP(buf[addressValueIndex : addressValueIndex+net.IPv4len]).String()
|
||||
payload.payload = buf[addressValueIndex+net.IPv4len:]
|
||||
payload.dstHostType = "ipv4"
|
||||
case 2: // domain
|
||||
addressLength := buf[addressValueIndex]
|
||||
addressValueIndex++
|
||||
if len(buf) < int(addressValueIndex)+int(addressLength) {
|
||||
return nil, fmt.Errorf("invalid domain address length")
|
||||
}
|
||||
payload.dstHost = string(buf[addressValueIndex : int(addressValueIndex)+int(addressLength)])
|
||||
payload.payload = buf[int(addressValueIndex)+int(addressLength):]
|
||||
payload.dstHostType = "domain"
|
||||
|
||||
case 3: // IPv6
|
||||
if len(buf) < int(addressValueIndex+net.IPv6len) {
|
||||
return nil, fmt.Errorf("invalid IPv6 address length")
|
||||
}
|
||||
payload.dstHost = net.IP(buf[addressValueIndex : addressValueIndex+net.IPv6len]).String()
|
||||
payload.payload = buf[addressValueIndex+net.IPv6len:]
|
||||
payload.dstHostType = "ipv6"
|
||||
default:
|
||||
return nil, fmt.Errorf("addressType %d is not supported", addressType)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
Reference in New Issue
Block a user