├── web
├── dist
│ ├── .nojekyll
│ ├── static
│ │ └── tuna-config-choices.json
│ ├── favicon.ico
│ ├── img
│ │ ├── grid.png
│ │ ├── point.png
│ │ ├── nkn-logo.png
│ │ └── qr_code.png
│ ├── _nuxt
│ │ ├── static
│ │ │ └── 1700863497
│ │ │ │ ├── payload.js
│ │ │ │ ├── network
│ │ │ │ └── payload.js
│ │ │ │ ├── index.html
│ │ │ │ ├── payload.js
│ │ │ │ └── state.js
│ │ │ │ └── manifest.js
│ │ ├── img
│ │ │ ├── grid.43c7c41.png
│ │ │ ├── point.77f0338.png
│ │ │ └── nkn-logo.8e7be89.png
│ │ ├── manifest.89ffc5b1.json
│ │ ├── LICENSES
│ │ ├── d63efb3.js
│ │ └── 4c0b218.js
│ ├── 200.html
│ └── sw.js
├── .gitignore
└── src
│ ├── static
│ ├── static
│ │ └── tuna-config-choices.json
│ ├── favicon.ico
│ └── img
│ │ ├── grid.png
│ │ ├── point.png
│ │ ├── nkn-logo.png
│ │ └── qr_code.png
│ ├── .editorconfig
│ ├── assets
│ ├── util.js
│ ├── variables.scss
│ ├── global.scss
│ ├── network_rpc.js
│ └── rpc.js
│ ├── store
│ └── README.md
│ ├── package.json
│ ├── layouts
│ ├── error.vue
│ └── default.vue
│ ├── plugins
│ └── i18n.js
│ ├── components
│ └── BackgroundLinear.vue
│ ├── .gitignore
│ ├── locales
│ ├── zh-CN.json
│ ├── zh-TW.json
│ └── en.json
│ ├── README.md
│ └── nuxt.config.js
├── tests
├── client_save.json
├── server_save.json
├── config.go
├── dns.go
├── config.manager.json
├── config.reverse.entry.json
├── proxy_test.go
├── client.json
├── tun_test.go
├── tools
│ └── main.go
├── server.json
├── main_test.go
├── udp.go
├── tcp.go
├── web.go
└── pub.go
├── docker
└── Dockerfile
├── config.client.json
├── .gitignore
├── config.server.json
├── arch
├── tun_test.go
├── route_darwin.go
├── route_windows.go
├── tun_windows.go
├── route_linux.go
├── tun_darwin.go
├── tun_linux.go
└── network.go
├── util
├── util_test.go
└── util.go
├── ss
├── tcp_other.go
├── log.go
├── multiconn.go
├── tcp_darwin.go
├── tcp_linux.go
├── plugin.go
├── ss.go
├── tcp.go
└── udp.go
├── .github
└── workflows
│ └── go.yml
├── config.network.json
├── admin
├── web.go
├── server.go
├── token.go
├── client.go
└── common.go
├── network
├── message.go
├── webservice.go
├── cliservice.go
└── member.go
├── bin
└── main.go
├── Makefile
├── go.mod
├── LICENSE
└── config
└── config.go
/web/dist/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /*.json
--------------------------------------------------------------------------------
/web/dist/static/tuna-config-choices.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/web/src/static/static/tuna-config-choices.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/web/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/favicon.ico
--------------------------------------------------------------------------------
/web/dist/img/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/img/grid.png
--------------------------------------------------------------------------------
/web/dist/img/point.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/img/point.png
--------------------------------------------------------------------------------
/web/dist/_nuxt/static/1700863497/payload.js:
--------------------------------------------------------------------------------
1 | __NUXT_JSONP__("/", {data:[{}],fetch:{},mutations:void 0});
--------------------------------------------------------------------------------
/web/dist/img/nkn-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/img/nkn-logo.png
--------------------------------------------------------------------------------
/web/dist/img/qr_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/img/qr_code.png
--------------------------------------------------------------------------------
/web/src/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/src/static/favicon.ico
--------------------------------------------------------------------------------
/web/src/static/img/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/src/static/img/grid.png
--------------------------------------------------------------------------------
/web/src/static/img/point.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/src/static/img/point.png
--------------------------------------------------------------------------------
/web/src/static/img/nkn-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/src/static/img/nkn-logo.png
--------------------------------------------------------------------------------
/web/src/static/img/qr_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/src/static/img/qr_code.png
--------------------------------------------------------------------------------
/tests/client_save.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "",
3 | "seed": "",
4 | "acceptAddrs": [],
5 | "adminAddrs": []
6 | }
--------------------------------------------------------------------------------
/web/dist/_nuxt/static/1700863497/network/payload.js:
--------------------------------------------------------------------------------
1 | __NUXT_JSONP__("/network", {data:[{}],fetch:{},mutations:void 0});
--------------------------------------------------------------------------------
/web/dist/_nuxt/img/grid.43c7c41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/_nuxt/img/grid.43c7c41.png
--------------------------------------------------------------------------------
/web/dist/_nuxt/img/point.77f0338.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/_nuxt/img/point.77f0338.png
--------------------------------------------------------------------------------
/web/dist/_nuxt/static/1700863497/index.html/payload.js:
--------------------------------------------------------------------------------
1 | __NUXT_JSONP__("/index.html", {data:[{}],fetch:{},mutations:void 0});
--------------------------------------------------------------------------------
/web/dist/_nuxt/img/nkn-logo.8e7be89.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nknorg/nconnect/HEAD/web/dist/_nuxt/img/nkn-logo.8e7be89.png
--------------------------------------------------------------------------------
/web/dist/_nuxt/static/1700863497/manifest.js:
--------------------------------------------------------------------------------
1 | __NUXT_JSONP__("manifest.js", {routes:["\u002F","\u002Fnetwork","\u002Findex.html"]})
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG base
2 | FROM ${base}debian:stretch-slim
3 | ARG build_dir
4 | ADD $build_dir /nConnect/
5 | WORKDIR /nConnect/data/
6 | ENTRYPOINT ["/nConnect/nConnect", "--web-root-path", "/nConnect/web/dist"]
7 |
--------------------------------------------------------------------------------
/tests/server_save.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "",
3 | "seed": "",
4 | "acceptAddrs": ["be285ff9330122cea44487a9618f96603fde6d37d5909ae1c271616772c349fe$"],
5 | "adminAddrs": ["be285ff9330122cea44487a9618f96603fde6d37d5909ae1c271616772c349fe$"]
6 | }
--------------------------------------------------------------------------------
/web/src/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/web/dist/_nuxt/manifest.89ffc5b1.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nconnect-web",
3 | "short_name": "nconnect-web",
4 | "description": "## Build Setup",
5 | "icons": [],
6 | "start_url": "/?standalone=true",
7 | "display": "standalone",
8 | "background_color": "#ffffff",
9 | "lang": "en"
10 | }
--------------------------------------------------------------------------------
/web/dist/_nuxt/static/1700863497/index.html/state.js:
--------------------------------------------------------------------------------
1 | window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1700863497",layout:"default",error:a,serverRendered:true,routePath:"\u002Findex.html",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null));
--------------------------------------------------------------------------------
/config.client.json:
--------------------------------------------------------------------------------
1 | {
2 | "Client": true,
3 | "Server": false,
4 | "identifier": "",
5 | "seed": "",
6 | "remoteAdminAddr": [],
7 | "localSocksAddr": "127.0.0.1:1080",
8 | "tuna": true,
9 | "udp": true,
10 | "acceptAddrs": null,
11 | "adminAddrs": null
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *~
3 | .DS_Store
4 | build
5 | nConnect
6 | nConnect.exe
7 | config.json
8 | aws-ip.json
9 | gcp-ip.json
10 | geolite2-country.mmdb
11 | *.favorite-node.json
12 | *.avoid-node.json
13 | *.log
14 | *.exe
15 | config.member.json
16 | member.json
17 | network.json
18 | config.manager.json
19 | config.member.json
20 |
--------------------------------------------------------------------------------
/web/src/assets/util.js:
--------------------------------------------------------------------------------
1 | export function assignDefined(target, ...sources) {
2 | for (let source of sources) {
3 | if (source) {
4 | for (let key of Object.keys(source)) {
5 | if (source[key] !== undefined) {
6 | target[key] = source[key];
7 | }
8 | }
9 | }
10 | }
11 | return target;
12 | }
13 |
--------------------------------------------------------------------------------
/config.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "Client": false,
3 | "Server": true,
4 | "identifier": "",
5 | "seed": "",
6 | "remoteAdminAddr": [],
7 | "localSocksAddr": "",
8 | "tuna": true,
9 | "udp": true,
10 | "adminIdentifier": "nConnect",
11 | "webRootPath": "web/dist",
12 | "acceptAddrs": [],
13 | "adminAddrs": []
14 | }
15 |
--------------------------------------------------------------------------------
/arch/tun_test.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import "testing"
4 |
5 | func TestOpenTunDevice(t *testing.T) {
6 | name := "tap0901"
7 | addr := "192.168.0.2"
8 | gw := "192.168.0.1"
9 | mask := "255.255.255.0"
10 | dnsServers := []string{"192.168.0.1"}
11 | persist := false
12 |
13 | openTunDevice(name, addr, gw, mask, dnsServers, persist)
14 | }
15 |
--------------------------------------------------------------------------------
/tests/config.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | var port int = 1080
4 |
5 | const (
6 | numMsgs = 10
7 |
8 | seedHex = "e68e046d13dd911594576ba0f4a196e9666790dc492071ad9ea5972c0b940435"
9 |
10 | tcpPort = ":20001"
11 | httpPort = ":20002"
12 | udpPort = ":20003"
13 |
14 | tunaNodeStarted = "tuna node is started"
15 | )
16 |
17 | var servers = []string{"127.0.0.1"} // {"10.10.0.15", "10.136.0.10"}
18 |
--------------------------------------------------------------------------------
/web/src/store/README.md:
--------------------------------------------------------------------------------
1 | # STORE
2 |
3 | **This directory is not required, you can delete it if you don't want to use it.**
4 |
5 | This directory contains your Vuex Store files.
6 | Vuex Store option is implemented in the Nuxt.js framework.
7 |
8 | Creating a file in this directory automatically activates the option in the framework.
9 |
10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
11 |
--------------------------------------------------------------------------------
/util/util_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "log"
5 | "testing"
6 |
7 | ts "github.com/nknorg/nkn-tuna-session"
8 | )
9 |
10 | // go test -v -run=TestGetFreePort
11 | func TestGetFreePort(t *testing.T) {
12 | port, err := ts.GetFreePort(0)
13 | if err != nil {
14 | log.Println(err)
15 | }
16 | log.Println(port)
17 |
18 | port, err = ts.GetFreePort(1080)
19 | if err != nil {
20 | log.Println(err)
21 | }
22 | log.Println(port)
23 | }
24 |
--------------------------------------------------------------------------------
/ss/tcp_other.go:
--------------------------------------------------------------------------------
1 | // +build !linux,!darwin
2 |
3 | package ss
4 |
5 | import (
6 | "errors"
7 | "net"
8 | "time"
9 | )
10 |
11 | func redirLocal(addr, server string, shadow func(net.Conn) net.Conn) error {
12 | return errors.New("TCP redirect not supported")
13 | }
14 |
15 | func redir6Local(addr, server string, shadow func(net.Conn) net.Conn) error {
16 | return errors.New("TCP6 redirect not supported")
17 | }
18 |
19 | func timedCork(c *net.TCPConn, d time.Duration) error { return nil }
20 |
--------------------------------------------------------------------------------
/arch/route_darwin.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "net"
5 | "os/exec"
6 | )
7 |
8 | func addRouteCmd(dest *net.IPNet, gateway, devName string) ([]byte, error) {
9 | b, err := exec.Command("route", "-n", "add", "-net", dest.String(), gateway).Output()
10 | if err == nil {
11 | return b, nil
12 | }
13 | return exec.Command("route", "-n", "change", "-net", dest.String(), gateway).Output()
14 | }
15 |
16 | func deleteRouteCmd(dest *net.IPNet, gateway, devName string) ([]byte, error) {
17 | return exec.Command("route", "-n", "delete", "-net", dest.String(), gateway).Output()
18 | }
19 |
--------------------------------------------------------------------------------
/tests/dns.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/txthinking/brook"
8 | )
9 |
10 | func dnsQuery() error {
11 | proxyAddr := fmt.Sprintf("127.0.0.1:%v", port)
12 | for i := 1; i <= numMsgs; i++ {
13 | err := brook.Socks5Test(proxyAddr, "", "", "http3.ooo", "137.184.237.95", "8.8.8.8:53")
14 | if err != nil {
15 | if strings.Contains(err.Error(), "timeout") { // sometimes the DNS reply timeout, retry
16 | continue
17 | }
18 | fmt.Printf("TestDNSProxy try %v err: %v\n", i, err)
19 | return err
20 | }
21 | }
22 | return nil
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | [push, pull_request, workflow_dispatch]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v3
18 | with:
19 | go-version: 1.19
20 |
21 | - name: Build
22 | run: go build -v ./...
23 |
24 | - name: Test
25 | run: go test -v ./...
26 |
--------------------------------------------------------------------------------
/ss/log.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | )
8 |
9 | var logger = log.New(os.Stderr, "", log.Lshortfile|log.LstdFlags)
10 |
11 | func logf(f string, v ...interface{}) {
12 | if config.Verbose {
13 | logger.Output(2, fmt.Sprintf(f, v...))
14 | }
15 | }
16 |
17 | type logHelper struct {
18 | prefix string
19 | }
20 |
21 | func (l *logHelper) Write(p []byte) (n int, err error) {
22 | if config.Verbose {
23 | logger.Printf("%s%s\n", l.prefix, p)
24 | return len(p), nil
25 | }
26 | return len(p), nil
27 | }
28 |
29 | func newLogHelper(prefix string) *logHelper {
30 | return &logHelper{prefix}
31 | }
32 |
--------------------------------------------------------------------------------
/web/dist/_nuxt/LICENSES:
--------------------------------------------------------------------------------
1 | /*!
2 | * vue-router v3.5.3
3 | * (c) 2021 Evan You
4 | * @license MIT
5 | */
6 |
7 | /*!
8 | * Vue.js v2.6.14
9 | * (c) 2014-2021 Evan You
10 | * Released under the MIT License.
11 | */
12 |
13 |
14 | /*!
15 | * vue-client-only v0.0.0-semantic-release
16 | * (c) 2021-present egoist <0x142857@gmail.com>
17 | * Released under the MIT License.
18 | */
19 |
20 | /*!
21 | * vue-i18n v8.27.0
22 | * (c) 2022 kazuya kawaguchi
23 | * Released under the MIT License.
24 | */
25 |
26 | /*!
27 | * vue-no-ssr v1.1.1
28 | * (c) 2018-present egoist <0x142857@gmail.com>
29 | * Released under the MIT License.
30 | */
31 |
--------------------------------------------------------------------------------
/tests/config.manager.json:
--------------------------------------------------------------------------------
1 | {
2 | "Client": false,
3 | "Server": false,
4 | "NetworkManager": true,
5 | "NetworkMember": false,
6 | "seed": "cb53701c451d0344943e0d15e1c84025742c993fd3e2c4768c0e3a211d381087",
7 | "identifier": "manager",
8 | "remoteAdminAddr": [],
9 | "localSocksAddr": "",
10 | "tuna": true,
11 | "udp": true,
12 | "adminIdentifier": "nConnect",
13 | "webRootPath": "../web/dist",
14 | "acceptAddrs": [],
15 | "adminAddrs": [],
16 |
17 | "AdminHTTPAddr": "127.0.0.1:8001",
18 | "nodeName": "bill",
19 | "managerAddress": "manager.0ec192083aaf67d1bf44ea862858a457c9b864b4d4416b647552e2ebcad2facb"
20 | }
--------------------------------------------------------------------------------
/config.network.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "",
3 | "seed": "",
4 |
5 | "managerAddress": "",
6 | "nodeName": "",
7 |
8 | "cipher": "dummy",
9 | "adminIdentifier": "nConnect",
10 | "adminHTTPAddr": "127.0.0.1:8000",
11 |
12 | "remoteAdminAddr": [],
13 | "localSocksAddr": "127.0.0.1:1080",
14 | "tuna": true,
15 | "udp": true,
16 | "acceptAddrs": [],
17 | "adminAddrs": [],
18 |
19 | "tunAddr": "10.0.86.2",
20 | "tunGateway": "10.0.86.1",
21 | "tunMask": "255.255.255.0",
22 | "tunDNS": ["1.1.1.1", "8.8.8.8"],
23 |
24 | "tunaDisableMeasureBandwidth": true,
25 | "tunaMeasureStoragePath": ".",
26 | "verbose": false
27 | }
28 |
--------------------------------------------------------------------------------
/web/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nconnect-web",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "nuxt",
7 | "build": "nuxt build",
8 | "start": "nuxt start",
9 | "generate": "nuxt generate"
10 | },
11 | "dependencies": {
12 | "@nuxtjs/axios": "^5.13.6",
13 | "@nuxtjs/pwa": "^3.3.5",
14 | "core-js": "^3.19.3",
15 | "js-cookie": "^3.0.1",
16 | "nuxt": "^2.15.8",
17 | "qrcode": "^1.5.0",
18 | "vue": "^2.6.14",
19 | "vue-i18n": "^8.27.0",
20 | "vue-server-renderer": "^2.6.14",
21 | "vue-template-compiler": "^2.6.14",
22 | "vuetify": "^2.6.1",
23 | "webpack": "^4.46.0"
24 | },
25 | "devDependencies": {
26 | "@nuxtjs/vuetify": "^1.12.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/arch/route_windows.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "net"
5 | "os/exec"
6 | )
7 |
8 | func addRouteCmd(dest *net.IPNet, gateway, devName string) ([]byte, error) {
9 | out, err := exec.Command("netsh", "interface", "ipv4", "add", "route", dest.String(), "nexthop="+gateway, "interface="+devName, "metric=0", "store=active").Output()
10 | if err == nil {
11 | return out, nil
12 | }
13 | return exec.Command("netsh", "interface", "ipv4", "set", "route", dest.String(), "nexthop="+gateway, "interface="+devName, "metric=0", "store=active").Output()
14 | }
15 |
16 | func deleteRouteCmd(dest *net.IPNet, gateway, devName string) ([]byte, error) {
17 | return exec.Command("netsh", "interface", "ipv4", "delete", "route", dest.String(), "interface="+devName).Output()
18 | }
19 |
--------------------------------------------------------------------------------
/ss/multiconn.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | )
7 |
8 | var routes struct {
9 | sync.RWMutex
10 | TargetToClient map[string]string // map target ip to local tunnel port
11 | DefaultClient string // the default client for the targets are not in TargetToClient map
12 | }
13 |
14 | func getClient(target string) string {
15 | tgtIp := strings.Split(target, ":")
16 |
17 | routes.RLock()
18 | defer routes.RUnlock()
19 | if server, ok := routes.TargetToClient[tgtIp[0]]; ok {
20 | return server
21 | }
22 | return routes.DefaultClient
23 | }
24 |
25 | func UpdateTargetToClient(targetToClient map[string]string) {
26 | routes.Lock()
27 | defer routes.Unlock()
28 | routes.TargetToClient = targetToClient
29 | }
30 |
--------------------------------------------------------------------------------
/tests/config.reverse.entry.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "test": {
4 | "maxPrice": "0.001",
5 | "ipFilter": {
6 | "allow": [
7 | {"countryCode": ""}
8 | ],
9 | "disallow": [
10 | {"countryCode": ""}
11 | ]
12 | }
13 | }
14 | },
15 | "downloadGeoDB": false,
16 | "geoDBPath": ".",
17 | "dialTimeout": 10,
18 | "udpTimeout": 0,
19 | "nanoPayFee": "",
20 | "minNanoPayFee": "0.00001",
21 | "nanoPayFeeRatio": 0.1,
22 | "reverse": true,
23 | "reverseBeneficiaryAddr": "",
24 | "reverseTCP": 30020,
25 | "reverseUDP": 30021,
26 | "reversePrice": "0.0",
27 | "reverseClaimInterval": 3600,
28 | "reverseSubscriptionDuration": 40000,
29 | "reverseSubscriptionFee": "0.0"
30 | }
31 |
--------------------------------------------------------------------------------
/tests/proxy_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | // go test -v -run=TestProxy
11 | func TestProxy(t *testing.T) {
12 | tuna, udp, tun := true, true, false
13 | go func() {
14 | err := startNconnect("client.json", tuna, udp, tun, nil)
15 | require.NoError(t, err)
16 | }()
17 |
18 | time.Sleep(5 * time.Second)
19 |
20 | err := waitForSSProxReady()
21 | require.NoError(t, err)
22 |
23 | err = dnsQuery()
24 | require.NoError(t, err)
25 | for _, server := range servers {
26 | err := StartWebClient("http://" + server + httpPort + "/httpEcho")
27 | require.NoError(t, err)
28 | err = StartTCPClient(server + tcpPort)
29 | require.NoError(t, err)
30 | err = StartUDPClient(server + udpPort)
31 | require.NoError(t, err)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/web/src/layouts/error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ pageNotFound }}
5 |
6 |
7 | {{ otherError }}
8 |
9 |
10 | Home page
11 |
12 |
13 |
14 |
15 |
40 |
41 |
46 |
--------------------------------------------------------------------------------
/web/src/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
41 |
--------------------------------------------------------------------------------
/arch/tun_windows.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "io"
5 | "log"
6 | "os/exec"
7 |
8 | "github.com/eycorsican/go-tun2socks/tun"
9 | )
10 |
11 | const (
12 | tunComponentID = "tap0901"
13 | )
14 |
15 | var wintapdev io.ReadWriteCloser
16 |
17 | func openTunDevice(name, addr, gw, mask string, dnsServers []string, persist bool) (io.ReadWriteCloser, error) {
18 | var err error
19 | wintapdev, err = tun.OpenTunDevice(name, addr, gw, mask, dnsServers, persist)
20 | return wintapdev, err
21 | }
22 |
23 | func SetTunIp(name, addr, mask, gw string) error {
24 | // if wintapdev != nil {
25 | // wintapdev.Close()
26 | // time.Sleep(2 * time.Second)
27 | // wintapdev = nil
28 | // }
29 | out, err := exec.Command("netsh", "interface", "ip", "add", "address", name, addr, mask).Output()
30 | log.Printf("SetTunIp: ip %s, mask %v, result: %s\n", addr, mask, string(out))
31 | // var err error
32 | // wintapdev, err = tun.OpenTunDevice(name, addr, gw, mask, []string{}, false)
33 | return err
34 | }
35 |
--------------------------------------------------------------------------------
/arch/route_linux.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "net"
5 | "os/exec"
6 | )
7 |
8 | func addRouteCmd(dest *net.IPNet, gateway, devName string) ([]byte, error) {
9 | out, err := exec.Command("ip", "route", "add", dest.String(), "via", gateway, "dev", devName).Output()
10 | if err == nil {
11 | return out, nil
12 | }
13 | out, err = exec.Command("ip", "route", "change", dest.String(), "via", gateway, "dev", devName).Output()
14 | if err == nil {
15 | return out, nil
16 | }
17 | out, err = exec.Command("route", "-n", "add", dest.String(), "gw", gateway).Output()
18 | if err == nil {
19 | return out, nil
20 | }
21 | return exec.Command("route", "-n", "change", dest.String(), "gw", gateway).Output()
22 | }
23 |
24 | func deleteRouteCmd(dest *net.IPNet, gateway, devName string) ([]byte, error) {
25 | out, err := exec.Command("ip", "route", "del", dest.String(), "via", gateway, "dev", devName).Output()
26 | if err == nil {
27 | return out, nil
28 | }
29 | return exec.Command("route", "-n", "del", dest.String(), "gw", gateway).Output()
30 | }
31 |
--------------------------------------------------------------------------------
/tests/client.json:
--------------------------------------------------------------------------------
1 | {
2 | "Client": true,
3 | "Server": false,
4 | "identifier": "alice",
5 | "seed": "e68e046d13dd911594576ba0f4a196e9666790dc492071ad9ea5972c0b940435",
6 | "cipher": "dummy",
7 | "logMaxSize": 1,
8 | "logMaxBackups": 3,
9 | "remoteAdminAddr": ["nConnect.bob.7cafe0ae02789f8eb6b293e46b0ac5cf8f92f73042199c8161e5b5f90b13dcb5"],
10 | "localSocksAddr": "127.0.0.1:1080",
11 | "tunAddr": "10.0.86.2",
12 | "tunGateway": "10.0.86.1",
13 | "tunMask": "255.255.255.0",
14 | "tunDNS": [
15 | "1.1.1.1",
16 | "8.8.8.8"
17 | ],
18 | "tuna": true,
19 | "udp": true,
20 | "tunaMinBalance": "0.01",
21 | "tunaMaxPrice": "0.01",
22 | "tunaMinFee": "0.00001",
23 | "tunaFeeRatio": 0.1,
24 | "tunaGeoDBPath": ".",
25 | "tunaDisableMeasureBandwidth": false,
26 | "tunaMeasureStoragePath": ".",
27 | "adminIdentifier": "nConnect",
28 | "webRootPath": "web/dist",
29 | "acceptAddrs": null,
30 | "adminAddrs": null,
31 | "ConfigFile": "client_save.json",
32 | "Address": false,
33 | "WalletAddress": false,
34 | "Version": false,
35 | "verbose": false
36 | }
37 |
--------------------------------------------------------------------------------
/web/src/assets/variables.scss:
--------------------------------------------------------------------------------
1 | // Ref: https://github.com/nuxt-community/vuetify-module#customvariables
2 | //
3 | // The variables you want to modify
4 | // $font-size-root: 20px;
5 | .v-btn{
6 | text-transform: none !important;
7 | }
8 |
9 | .v-text-field.v-text-field--solo:not(.v-text-field--solo-flat) > .v-input__control > .v-input__slot {
10 | border: 1.08px solid !important;
11 | border-image-source: linear-gradient(218.16deg, rgba(255, 255, 255, 0.3) -27.92%, rgba(255, 255, 255, 0) 92.11%) !important;
12 | background: rgba(255, 255, 255, 0.15) !important;
13 | box-shadow: 0px 17.5609px 16.2101px rgba(11, 39, 40, 0.07) !important;
14 | backdrop-filter: blur(18.9118px) !important;
15 | border-radius: 10px !important;
16 | }
17 | .v-text-field.v-text-field--solo .v-input__prepend-outer, .v-text-field.v-text-field--solo .v-input__append-outer{
18 | margin-top: 6px !important;
19 | }
20 | .v-text-field.v-text-field--solo .v-input__control {
21 | min-height: 36px !important;
22 | }
23 |
24 | .v-select__selection--comma{
25 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) !important;
26 | }
27 |
--------------------------------------------------------------------------------
/web/src/plugins/i18n.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 | import Cookies from 'js-cookie'
4 | import en from '~/locales/en.json'
5 | import zh from '~/locales/zh-CN.json'
6 | import zhTW from '~/locales/zh-TW.json'
7 |
8 | Vue.use(VueI18n)
9 |
10 | export default ({ app, store }) => {
11 | let messages = {}
12 | messages = {...messages, en, zh, zhTW}
13 | let lang = 'en'
14 | if (typeof navigator !== 'undefined') {
15 | let navLang = navigator.language || navigator.userLanguage
16 | if (!!navLang) lang = navLang.substr(0, 2)
17 | }
18 | app.i18n = new VueI18n({
19 | locale: Cookies.get('language') || lang,
20 | fallbackLocale: 'en',
21 | messages,
22 | });
23 |
24 | let locales = []
25 | for (let code of app.i18n.availableLocales) {
26 | let name = messages[code].language
27 | locales.push({code, name})
28 | }
29 | app.i18n.locales = locales
30 | app.i18n.path = (link) => {
31 | if (app.i18n.locale === app.i18n.fallbackLocale) {
32 | return `/${link}`;
33 | }
34 | return `/${app.i18n.locale}/${link}`;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/tun_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | // var tun = flag.Bool("tun", false, "use tun device")
12 |
13 | // go test -v -run=TestTun -tun
14 | func TestTun(t *testing.T) {
15 | fmt.Println("Make sure run this case at root or administrator shell")
16 |
17 | if !(*tun) {
18 | t.Skip("Skip TestTun, if you want to run this test, please use: go test -v -tun .")
19 | }
20 |
21 | tuna, udp, tun := true, true, true
22 | go func() {
23 | err := startNconnect("client.json", tuna, udp, tun, nil)
24 | require.NoError(t, err)
25 | }()
26 |
27 | time.Sleep(10 * time.Second)
28 |
29 | err := waitForSSProxReady()
30 | require.NoError(t, err)
31 |
32 | err = dnsQuery()
33 | require.NoError(t, err)
34 | for _, server := range servers {
35 | err := StartTunWebClient("http://" + server + httpPort + "/httpEcho")
36 | require.NoError(t, err)
37 | err = StartTCPClient(server + tcpPort)
38 | require.NoError(t, err)
39 | err = StartUDPClient(server + udpPort)
40 | require.NoError(t, err)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/web/src/components/BackgroundLinear.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
48 |
--------------------------------------------------------------------------------
/tests/tools/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 |
7 | "github.com/nknorg/nconnect/tests"
8 | )
9 |
10 | const (
11 | port = ":12345"
12 | )
13 |
14 | func main() {
15 | var udp = flag.Bool("udp", false, "udp mode")
16 | var server = flag.Bool("server", false, "run as server")
17 | var serverAddr = flag.String("serverAddr", "127.0.0.1", "server's ip")
18 | flag.Parse()
19 |
20 | if *server { // server, both tcp and udp
21 | go func() {
22 | err := tests.StartTCPServer(port)
23 | if err != nil {
24 | log.Printf("StartTCPServer err: %v\n", err)
25 | }
26 | }()
27 |
28 | err := tests.StartUDPServer(port)
29 | if err != nil {
30 | log.Printf("StartUDPServer err: %v\n", err)
31 | }
32 | } else { // client
33 | if *udp { // udp client
34 | err := tests.StartUDPTunClient(*serverAddr + port)
35 | if err != nil {
36 | log.Printf("StartUDPTunClient err: %v\n", err)
37 | }
38 | } else { // tcp client
39 | err := tests.StartTCPTunClient(*serverAddr + port)
40 | if err != nil {
41 | log.Printf("StartTCPTunClient err: %v\n", err)
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ss/tcp_darwin.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "syscall"
7 | "time"
8 |
9 | "github.com/shadowsocks/go-shadowsocks2/pfutil"
10 | "github.com/shadowsocks/go-shadowsocks2/socks"
11 | )
12 |
13 | func redirLocal(addr, server string, shadow func(net.Conn) net.Conn) error {
14 | return tcpLocal(addr, server, shadow, natLookup)
15 | }
16 |
17 | func redir6Local(addr, server string, shadow func(net.Conn) net.Conn) error {
18 | return errors.New("TCP6 redirect not supported")
19 | }
20 |
21 | func natLookup(c net.Conn) (socks.Addr, error) {
22 | if tc, ok := c.(*net.TCPConn); ok {
23 | addr, err := pfutil.NatLookup(tc)
24 | return socks.ParseAddr(addr.String()), err
25 | }
26 | panic("not TCP connection")
27 | }
28 |
29 | func timedCork(c *net.TCPConn, d time.Duration) error {
30 | rc, err := c.SyscallConn()
31 | if err != nil {
32 | return err
33 | }
34 | rc.Control(func(fd uintptr) { err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_NOPUSH, 1) })
35 | if err != nil {
36 | return err
37 | }
38 | time.AfterFunc(d, func() {
39 | rc.Control(func(fd uintptr) { syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_NOPUSH, 0) })
40 | })
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/web/src/assets/global.scss:
--------------------------------------------------------------------------------
1 | .bg-linear-1 {
2 | border: 1.08px solid !important;
3 | border-image-source: linear-gradient(218.16deg, rgba(255, 255, 255, 0.3) -27.92%, rgba(255, 255, 255, 0) 92.11%) !important;
4 | background: rgba(255, 255, 255, 0.15) !important;
5 | box-shadow: 0px 17.5609px 16.2101px rgba(11, 39, 40, 0.07) !important;
6 | backdrop-filter: blur(18.9118px) !important;
7 | border-radius: 10px !important;
8 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) !important;
9 | * {
10 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) !important;
11 | }
12 | }
13 |
14 | .bg-linear-1.active {
15 | background: rgba(255, 255, 255, 0.79) !important;
16 | color: #120E34 !important;
17 | }
18 |
19 | .bg-linear-2 {
20 | background: #FFFFFF !important;
21 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) !important;
22 | border-radius: 7px !important;
23 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) !important;
24 | * {
25 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) !important;
26 | }
27 | }
28 |
29 | .br-10 {
30 | border-radius: 10px !important;
31 | }
32 |
33 | .btn-1 {
34 | background: rgba(0, 163, 255, 0.58) !important;
35 | box-shadow: 0px 17.5609px 16.2101px rgba(11, 39, 40, 0.07) !important;
36 | backdrop-filter: blur(18.9118px) !important;
37 | }
38 |
--------------------------------------------------------------------------------
/tests/server.json:
--------------------------------------------------------------------------------
1 | {
2 | "Client": false,
3 | "Server": true,
4 | "identifier": "bob",
5 | "seed": "444a1e625c4d5b36f8059832a521e88fb219fa31ddf14da8fe23346f59081fb8",
6 | "cipher": "dummy",
7 | "logMaxSize": 1,
8 | "logMaxBackups": 3,
9 | "localSocksAddr": "127.0.0.1:1080",
10 | "tunAddr": "10.0.86.2",
11 | "tunGateway": "10.0.86.1",
12 | "tunMask": "255.255.255.0",
13 | "tunDNS": [
14 | "1.1.1.1",
15 | "8.8.8.8"
16 | ],
17 | "tunName": "nConnect-tap0",
18 | "tuna": true,
19 | "udp": true,
20 | "tunaMinBalance": "0.01",
21 | "tunaMaxPrice": "0.01",
22 | "tunaMinFee": "0.00001",
23 | "tunaFeeRatio": 0.1,
24 | "tunaServiceName": "reverse",
25 | "tunaGeoDBPath": ".",
26 | "tunaDisableMeasureBandwidth": false,
27 | "tunaMeasureStoragePath": ".",
28 | "adminIdentifier": "nConnect",
29 | "webRootPath": "web/dist",
30 | "acceptAddrs": [
31 | "7aafe088fed1a3d2b161437208ce61e26ed1b2d0b83fcef5ec55c273defac1da$",
32 | "7cafe0ae02789f8eb6b293e46b0ac5cf8f92f73042199c8161e5b5f90b13dcb5$",
33 | "be285ff9330122cea44487a9618f96603fde6d37d5909ae1c271616772c349fe$"
34 | ],
35 | "adminAddrs": [
36 | "7aafe088fed1a3d2b161437208ce61e26ed1b2d0b83fcef5ec55c273defac1da$",
37 | "be285ff9330122cea44487a9618f96603fde6d37d5909ae1c271616772c349fe$"
38 | ],
39 | "ConfigFile": "server_save.json",
40 | "Address": false,
41 | "WalletAddress": false,
42 | "Version": false,
43 | "verbose": false
44 | }
45 |
--------------------------------------------------------------------------------
/ss/tcp_linux.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "net"
5 | "syscall"
6 | "time"
7 |
8 | "github.com/shadowsocks/go-shadowsocks2/nfutil"
9 | "github.com/shadowsocks/go-shadowsocks2/socks"
10 | )
11 |
12 | func getOrigDst(c net.Conn, ipv6 bool) (socks.Addr, error) {
13 | if tc, ok := c.(*net.TCPConn); ok {
14 | addr, err := nfutil.GetOrigDst(tc, ipv6)
15 | return socks.ParseAddr(addr.String()), err
16 | }
17 | panic("not a TCP connection")
18 | }
19 |
20 | // Listen on addr for netfilter redirected TCP connections
21 | func redirLocal(addr, server string, shadow func(net.Conn) net.Conn) error {
22 | logf("TCP redirect %s <-> %s", addr, server)
23 | return tcpLocal(addr, server, shadow, func(c net.Conn) (socks.Addr, error) { return getOrigDst(c, false) })
24 | }
25 |
26 | // Listen on addr for netfilter redirected TCP IPv6 connections.
27 | func redir6Local(addr, server string, shadow func(net.Conn) net.Conn) error {
28 | logf("TCP6 redirect %s <-> %s", addr, server)
29 | return tcpLocal(addr, server, shadow, func(c net.Conn) (socks.Addr, error) { return getOrigDst(c, true) })
30 | }
31 |
32 | func timedCork(c *net.TCPConn, d time.Duration) error {
33 | rc, err := c.SyscallConn()
34 | if err != nil {
35 | return err
36 | }
37 | rc.Control(func(fd uintptr) { err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_CORK, 1) })
38 | if err != nil {
39 | return err
40 | }
41 | time.AfterFunc(d, func() {
42 | rc.Control(func(fd uintptr) { syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_CORK, 0) })
43 | })
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/arch/tun_darwin.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "net"
8 | "os/exec"
9 | "strings"
10 |
11 | "github.com/eycorsican/go-tun2socks/tun"
12 | "github.com/songgao/water"
13 | )
14 |
15 | var tundev *water.Interface
16 |
17 | func openTunDevice(name, addr, gw, mask string, dnsServers []string, persist bool) (io.ReadWriteCloser, error) {
18 | rwc, err := tun.OpenTunDevice(name, addr, gw, mask, dnsServers, persist)
19 | if err == nil {
20 | tundev = rwc.(*water.Interface)
21 | }
22 | return rwc, err
23 | }
24 |
25 | func SetTunIp(tunName, addr, mask, gw string) error {
26 |
27 | var params string
28 | params = fmt.Sprintf("lo0 alias %v %v ", addr, mask)
29 |
30 | out, err := exec.Command("ifconfig", strings.Split(params, " ")...).Output()
31 | if err != nil {
32 | if len(out) != 0 {
33 | return errors.New(fmt.Sprintf("%v, output: %s", err, out))
34 | }
35 | return err
36 | }
37 |
38 | return nil
39 | }
40 |
41 | func SetTunIp_old(tunName, addr, mask, gw string) error {
42 | ip := net.ParseIP(addr)
43 | if ip == nil {
44 | return errors.New("invalid IP address")
45 | }
46 |
47 | if tundev == nil {
48 | return errors.New("tun device is not open")
49 | }
50 |
51 | tunName = tundev.Name()
52 |
53 | var params string
54 | params = fmt.Sprintf("%s inet %s netmask %s %s", tunName, addr, mask, gw)
55 |
56 | out, err := exec.Command("ifconfig", strings.Split(params, " ")...).Output()
57 | if err != nil {
58 | if len(out) != 0 {
59 | return errors.New(fmt.Sprintf("%v, output: %s", err, out))
60 | }
61 | return err
62 | }
63 |
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/tests/main_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 | "testing"
9 | "time"
10 |
11 | "github.com/nknorg/tuna/types"
12 | )
13 |
14 | const (
15 | tunaIp = "127.0.0.1" // "147.182.210.189" // DO No.9 test server
16 | )
17 |
18 | var remoteTuna = flag.Bool("remoteTuna", false, "use remote tuna nodes")
19 | var tun = flag.Bool("tun", false, "use tun device")
20 |
21 | func TestMain(m *testing.M) {
22 | flag.Parse()
23 | if *remoteTuna {
24 | fmt.Println("We are using remote tuna node")
25 | } else {
26 | fmt.Println("Using local tuna node. If want to use remote tuna nodes, please run: go test -v -remoteTuna .")
27 | }
28 |
29 | go func() {
30 | err := StartTCPServer(tcpPort)
31 | if err != nil {
32 | log.Fatalf("StartTcpServer err %v", err)
33 | return
34 | }
35 | }()
36 | go func() {
37 | err := StartWebServer()
38 | if err != nil {
39 | log.Fatalf("StartWebServer err %v", err)
40 | return
41 | }
42 | }()
43 | go func() {
44 | err := StartUDPServer(udpPort)
45 | if err != nil {
46 | log.Fatalf("StartUdpServer err %v", err)
47 | return
48 | }
49 | }()
50 |
51 | var tunaNode *types.Node
52 | var err error
53 | if !(*remoteTuna) {
54 | tunaNode, err = getTunaNode(tunaIp)
55 | if err != nil {
56 | log.Fatalf("getTunaNode err %v", err)
57 | return
58 | }
59 | }
60 |
61 | err = startNconnect("server.json", true, true, false, tunaNode)
62 | if err != nil {
63 | log.Fatalf("start nconnect server err: %v", err)
64 | return
65 | }
66 |
67 | time.Sleep(10 * time.Second)
68 |
69 | exitVal := m.Run()
70 | os.Exit(exitVal)
71 | }
72 |
--------------------------------------------------------------------------------
/admin/web.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "path"
7 |
8 | "github.com/gin-contrib/gzip"
9 | "github.com/gin-gonic/gin"
10 | "github.com/nknorg/nconnect/config"
11 | tunnel "github.com/nknorg/nkn-tunnel"
12 | )
13 |
14 | var (
15 | errAdminHTTPAPIDisabled = errors.New("Web API is disabled")
16 | )
17 |
18 | func StartWebServer(listenAddr string, tun *tunnel.Tunnel, persistConf, mergedConf *config.Config) error {
19 | gin.SetMode(gin.ReleaseMode)
20 |
21 | r := gin.Default()
22 |
23 | r.Use(gzip.Gzip(gzip.DefaultCompression))
24 |
25 | r.POST("/rpc/admin", func(c *gin.Context) {
26 | req := &RpcReq{}
27 | if err := c.ShouldBindJSON(req); err != nil {
28 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
29 | return
30 | }
31 | if mergedConf.DisableAdminHTTPAPI {
32 | c.JSON(http.StatusOK, &RpcResp{Error: errAdminHTTPAPIDisabled.Error()})
33 | return
34 | }
35 | resp := handleRequest(req, persistConf, mergedConf, tun, rpcPermissionWeb)
36 | c.JSON(http.StatusOK, resp)
37 | })
38 |
39 | r.StaticFile("/", path.Join(mergedConf.WebRootPath, "index.html"))
40 | r.StaticFile("/favicon.ico", path.Join(mergedConf.WebRootPath, "favicon.ico"))
41 | r.StaticFile("/sw.js", path.Join(mergedConf.WebRootPath, "sw.js"))
42 | r.Static("/static", path.Join(mergedConf.WebRootPath, "static"))
43 | r.Static("/_nuxt", path.Join(mergedConf.WebRootPath, "_nuxt"))
44 | r.Static("/img", path.Join(mergedConf.WebRootPath, "img"))
45 | r.Static("/zh", path.Join(mergedConf.WebRootPath, "zh"))
46 | r.Static("/zh-TW", path.Join(mergedConf.WebRootPath, "zh-TW"))
47 |
48 | return r.Run(listenAddr)
49 | }
50 |
--------------------------------------------------------------------------------
/web/src/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Node template
3 | # Logs
4 | /logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # nyc test coverage
23 | .nyc_output
24 |
25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 | .grunt
27 |
28 | # Bower dependency directory (https://bower.io/)
29 | bower_components
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (https://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # TypeScript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Output of 'npm pack'
54 | *.tgz
55 |
56 | # Yarn Integrity file
57 | .yarn-integrity
58 |
59 | # dotenv environment variables file
60 | .env
61 |
62 | # parcel-bundler cache (https://parceljs.org/)
63 | .cache
64 |
65 | # next.js build output
66 | .next
67 |
68 | # nuxt.js build output
69 | .nuxt
70 |
71 | # Nuxt generate
72 | dist
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless
79 |
80 | # IDE / Editor
81 | .idea
82 |
83 | # Service worker
84 | sw.*
85 |
86 | # macOS
87 | .DS_Store
88 |
89 | # Vim swap files
90 | *.swp
91 |
--------------------------------------------------------------------------------
/admin/server.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 |
7 | "github.com/nknorg/nconnect/config"
8 | "github.com/nknorg/nconnect/util"
9 | "github.com/nknorg/nkn-sdk-go"
10 | tunnel "github.com/nknorg/nkn-tunnel"
11 | )
12 |
13 | func StartNKNServer(account *nkn.Account, identifier string, clientConfig *nkn.ClientConfig, tun *tunnel.Tunnel, persistConf, mergedConf *config.Config) error {
14 | m, err := nkn.NewMultiClient(account, identifier, 4, false, clientConfig)
15 | if err != nil {
16 | return err
17 | }
18 |
19 | <-m.OnConnect.C
20 |
21 | serverAdminAddr = m.Address()
22 |
23 | for {
24 | msg := <-m.OnMessage.C
25 |
26 | req := &RpcReq{}
27 | err := json.Unmarshal(msg.Data, req)
28 | if err != nil {
29 | log.Println("Unmarshal client request error:", err)
30 | continue
31 | }
32 |
33 | isAcceptAddr := util.MatchRegex(persistConf.GetAcceptAddrs(), msg.Src)
34 | isAdminAddr := util.MatchRegex(persistConf.GetAdminAddrs(), msg.Src)
35 |
36 | if !isAdminAddr && tokenStore.IsValid(req.Token) {
37 | isAdminAddr = true
38 | }
39 |
40 | if !isAcceptAddr && !isAdminAddr {
41 | log.Println("Ignore authorized message from", msg.Src)
42 | continue
43 | }
44 |
45 | var perm permission
46 | if isAcceptAddr {
47 | perm |= rpcPermissionAcceptClient
48 | }
49 | if isAdminAddr {
50 | perm |= rpcPermissionAdminClient
51 | }
52 |
53 | resp := handleRequest(req, persistConf, mergedConf, tun, perm)
54 |
55 | b, err := json.Marshal(resp)
56 | if err != nil {
57 | log.Println(err)
58 | continue
59 | }
60 |
61 | err = msg.Reply(string(b))
62 | if err != nil {
63 | log.Println(err)
64 | continue
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/arch/tun_linux.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net"
9 | "os/exec"
10 | "strconv"
11 | "strings"
12 |
13 | "github.com/eycorsican/go-tun2socks/tun"
14 | "github.com/nknorg/nconnect/util"
15 | )
16 |
17 | func openTunDevice(name, addr, gw, mask string, dnsServers []string, persist bool) (io.ReadWriteCloser, error) {
18 | tunDev, err := tun.OpenTunDevice(name, addr, gw, mask, dnsServers, persist)
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | err = SetTunIp(name, addr, mask, gw)
24 |
25 | return tunDev, err
26 | }
27 |
28 | func SetTunIp(tapName, ip, mask, gw string) error {
29 | out, err := func() ([]byte, error) {
30 | out, err := exec.Command("ip", "addr", "replace", ip+"/"+mask, "dev", tapName).Output()
31 | if err != nil {
32 | return out, err
33 | }
34 | return exec.Command("ip", "link", "set", "dev", tapName, "up").Output()
35 | }()
36 | if err != nil {
37 | if len(out) > 0 {
38 | log.Print(string(out))
39 | }
40 | log.Println(util.ParseExecError(err))
41 |
42 | ip := net.ParseIP(ip)
43 | if ip == nil {
44 | return errors.New("invalid IP address")
45 | }
46 |
47 | var params string
48 | if ip.To4() != nil {
49 | params = fmt.Sprintf("%s inet %s netmask %s up", tapName, ip, mask)
50 | } else {
51 | prefixlen, err := strconv.Atoi(mask)
52 | if err != nil {
53 | return fmt.Errorf("parse IPv6 prefixlen failed: %v", err)
54 | }
55 | params = fmt.Sprintf("%s inet6 %s/%d up", tapName, ip, prefixlen)
56 | }
57 |
58 | out, err := exec.Command("ifconfig", strings.Split(params, " ")...).Output()
59 | if err != nil {
60 | if len(out) > 0 {
61 | log.Print(string(out))
62 | }
63 | return errors.New(util.ParseExecError(err))
64 | }
65 | }
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/network/message.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "encoding/json"
5 | "time"
6 |
7 | "github.com/nknorg/nconnect/admin"
8 | )
9 |
10 | // msgType constants
11 | const (
12 | MT_NONE = iota
13 | JOIN_NETWORK
14 | UPDATE_MY_INFO
15 | GET_MY_INFO
16 | UPDATE_SERVER_ADDRESS
17 | GET_NODES_I_ACCEPT
18 | GET_NODES_I_CAN_ACCESS
19 | LEAVE_NETWORK
20 | NKN_PING
21 | NKN_PONG
22 |
23 | NOTI_AUTHORIZED
24 | NOTI_NEW_MEMBER
25 | NOTI_UPD_I_CAN_ACCESS
26 | NOTI_UPD_I_ACCEPT
27 | NOTI_MEMBER_ONLINE
28 | NOTI_LEAVE_NETWORK
29 | )
30 |
31 | type NodeInfo struct {
32 | IP string `json:"ip"`
33 | Netmask string `json:"netmask"`
34 | Name string `json:"name"`
35 | Address string `json:"address"` // client address
36 | ServerAddress string `json:"serverAddress"` // nconnect server listen address
37 | LastSeen time.Time `json:"lastSeen"`
38 | Server bool `json:"server"`
39 | Balance string `json:"balance"`
40 | }
41 |
42 | type networkInfo struct {
43 | Domain string `json:"domain"`
44 | Gateway string `json:"gateway"`
45 | DNS string `json:"dns"`
46 | }
47 |
48 | type memberToManager struct {
49 | MsgType int `json:"msgType"`
50 | Name string `json:"name"`
51 | ServerAddress string `json:"serverAddress"`
52 | }
53 |
54 | type managerToMember struct {
55 | MsgType int `json:"msgType"`
56 | Err string `json:"err"`
57 | NetworkInfo *networkInfo `json:"networkInfo"`
58 | NodeInfo []*NodeInfo `json:"nodeInfo"`
59 | }
60 |
61 | func SendMsg(mc *admin.Client, address string, msg interface{}, waitResponse bool) (*managerToMember, error) {
62 | reply, err := mc.SendMsg(address, msg, waitResponse)
63 | if err != nil || !waitResponse {
64 | return nil, err
65 | }
66 |
67 | var respMsg managerToMember
68 | if err = json.Unmarshal(reply.Data, &respMsg); err != nil {
69 | return nil, err
70 | }
71 | return &respMsg, nil
72 | }
73 |
--------------------------------------------------------------------------------
/bin/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strings"
8 |
9 | "github.com/jessevdk/go-flags"
10 | "github.com/nknorg/nconnect"
11 | "github.com/nknorg/nconnect/config"
12 | "github.com/nknorg/nconnect/network"
13 | )
14 |
15 | func main() {
16 | defer func() {
17 | if r := recover(); r != nil {
18 | log.Fatalf("Panic: %+v", r)
19 | }
20 | }()
21 |
22 | var opts = &config.Opts{}
23 | _, err := flags.Parse(opts)
24 | if err != nil {
25 | if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
26 | os.Exit(0)
27 | }
28 | log.Fatal(err)
29 | }
30 |
31 | if opts.Version {
32 | fmt.Println(config.Version)
33 | os.Exit(0)
34 | }
35 |
36 | if opts.Info != "" {
37 | cli(opts.Info)
38 | os.Exit(0)
39 | }
40 |
41 | nc, err := nconnect.NewNconnect(opts)
42 | if err != nil {
43 | log.Fatal(err)
44 | }
45 |
46 | if opts.NetworkManager {
47 | err = nc.StartNetworkManager()
48 | if err != nil {
49 | log.Fatal(err)
50 | }
51 | return
52 | }
53 |
54 | if opts.NetworkMember {
55 | err = nc.StartNetworkMember()
56 | if err != nil {
57 | log.Fatal(err)
58 | }
59 | return
60 | }
61 |
62 | if opts.Client {
63 | err = nc.StartClient()
64 | if err != nil {
65 | log.Fatal(err)
66 | }
67 | return
68 | }
69 |
70 | if opts.Server {
71 | err = nc.StartServer()
72 | if err != nil {
73 | log.Fatal(err)
74 | }
75 | return
76 | }
77 | }
78 |
79 | const help = `
80 | nConnect -i , to get nConnect information. The cmd can be:
81 | help: this help
82 | join: join network
83 | leave: leave network
84 | status: get network status
85 | list: list nodes I can access and nodes which can access me
86 | `
87 |
88 | func cli(cmd string) {
89 | cmd = strings.ToLower(strings.TrimSpace(cmd))
90 | switch cmd {
91 | case "help":
92 | fmt.Print(help)
93 | case "join":
94 | network.CliJoin()
95 | case "leave":
96 | network.CliLeave()
97 | case "status":
98 | network.CliStatus()
99 | case "list":
100 | network.CliList()
101 | default:
102 | fmt.Print("Unknown command: ", cmd, "\n", help)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/arch/network.go:
--------------------------------------------------------------------------------
1 | package arch
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "log"
7 | "net"
8 | "os"
9 | "time"
10 |
11 | "github.com/eycorsican/go-tun2socks/core"
12 | "github.com/eycorsican/go-tun2socks/proxy/socks"
13 | "github.com/nknorg/nconnect/util"
14 | )
15 |
16 | const (
17 | mtu = 1500
18 | )
19 |
20 | func OpenTun(tunName, ip, gateway, mask, dns, socksAddr string) error {
21 | tunDevice, err := openTunDevice(tunName, ip, gateway, mask, []string{dns}, false)
22 | if err != nil {
23 | return fmt.Errorf("failed to open TUN device: %v", err)
24 | }
25 |
26 | core.RegisterOutputFn(tunDevice.Write)
27 |
28 | proxyAddr, err := net.ResolveTCPAddr("tcp", socksAddr)
29 | if err != nil {
30 | return fmt.Errorf("invalid proxy server address %v err: %v", socksAddr, err)
31 | }
32 | proxyHost := proxyAddr.IP.String()
33 | proxyPort := uint16(proxyAddr.Port)
34 |
35 | core.RegisterTCPConnHandler(socks.NewTCPHandler(proxyHost, proxyPort))
36 | core.RegisterUDPConnHandler(socks.NewUDPHandler(proxyHost, proxyPort, 30*time.Second))
37 |
38 | lwipWriter := core.NewLWIPStack()
39 |
40 | go func() {
41 | _, err := io.CopyBuffer(lwipWriter, tunDevice, make([]byte, mtu))
42 | if err != nil {
43 | log.Fatalf("Failed to write data to network stack: %v", err)
44 | }
45 | }()
46 |
47 | return nil
48 | }
49 |
50 | func SetVPNRoutes(tunName, gateway string, cidrs []*net.IPNet) ([]*net.IPNet, error) {
51 | for _, dest := range cidrs {
52 | log.Printf("Adding route %s by %s", dest, gateway)
53 | out, err := addRouteCmd(dest, gateway, tunName)
54 | if len(out) > 0 {
55 | os.Stdout.Write(out)
56 | }
57 | if err != nil {
58 | os.Stdout.Write([]byte(util.ParseExecError(err)))
59 | os.Exit(1)
60 | }
61 | }
62 |
63 | return cidrs, nil
64 | }
65 |
66 | func RemoveVPNRoutes(tunName, gateway string, cidrs []*net.IPNet) error {
67 | for _, dest := range cidrs {
68 | log.Printf("Deleting route %s", dest)
69 | out, err := deleteRouteCmd(dest, gateway, tunName)
70 | if len(out) > 0 {
71 | os.Stdout.Write(out)
72 | }
73 | if err != nil {
74 | os.Stdout.Write([]byte(util.ParseExecError(err)))
75 | }
76 | }
77 | return nil
78 | }
79 |
--------------------------------------------------------------------------------
/admin/token.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "fmt"
7 | "sync"
8 | "time"
9 | )
10 |
11 | const (
12 | TokenSize = 32
13 | TokenExpiration = 10 * time.Minute
14 | TokenRotateInterval = 5 * time.Minute
15 | )
16 |
17 | var (
18 | tokenStore = NewTokenStore(TokenExpiration, TokenRotateInterval)
19 | )
20 |
21 | func init() {
22 | go tokenStore.Start()
23 | }
24 |
25 | type UnixTime time.Time
26 |
27 | func (t UnixTime) MarshalJSON() ([]byte, error) {
28 | return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil
29 | }
30 |
31 | type Token struct {
32 | Token string `json:"token"`
33 | ExpiresAt UnixTime `json:"expiresAt"`
34 | }
35 |
36 | func NewToken(expiration time.Duration) *Token {
37 | b := make([]byte, TokenSize)
38 | rand.Read(b)
39 | return &Token{
40 | Token: hex.EncodeToString(b),
41 | ExpiresAt: UnixTime(time.Now().Add(expiration)),
42 | }
43 | }
44 |
45 | func (t *Token) IsValid(token string) bool {
46 | if t == nil {
47 | return false
48 | }
49 | return token == t.Token && time.Now().Before(time.Time(t.ExpiresAt))
50 | }
51 |
52 | type TokenStore struct {
53 | tokenExpiration time.Duration
54 | rotateInterval time.Duration
55 |
56 | lock sync.RWMutex
57 | tokens []*Token
58 | current int
59 | }
60 |
61 | func NewTokenStore(tokenExpiration, rotateInterval time.Duration) *TokenStore {
62 | tokens := make([]*Token, tokenExpiration/rotateInterval+1)
63 | tokens[0] = NewToken(tokenExpiration)
64 | return &TokenStore{
65 | tokenExpiration: tokenExpiration,
66 | rotateInterval: rotateInterval,
67 | tokens: tokens,
68 | current: 0,
69 | }
70 | }
71 |
72 | func (tr *TokenStore) Start() {
73 | for {
74 | time.Sleep(tr.rotateInterval)
75 | tr.lock.Lock()
76 | tr.current = (tr.current + 1) % len(tr.tokens)
77 | tr.tokens[tr.current] = NewToken(tr.tokenExpiration)
78 | tr.lock.Unlock()
79 | }
80 | }
81 |
82 | func (tr *TokenStore) GetCurrentToken() *Token {
83 | tr.lock.RLock()
84 | defer tr.lock.RUnlock()
85 | return tr.tokens[tr.current]
86 | }
87 |
88 | func (tr *TokenStore) IsValid(token string) bool {
89 | tr.lock.RLock()
90 | defer tr.lock.RUnlock()
91 | for i := range tr.tokens {
92 | if tr.tokens[i] != nil && tr.tokens[i].IsValid(token) {
93 | return true
94 | }
95 | }
96 | return false
97 | }
98 |
--------------------------------------------------------------------------------
/web/dist/_nuxt/d63efb3.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(data){for(var r,n,l=data[0],f=data[1],d=data[2],i=0,h=[];i 0 {
70 | return errors.New(resp.Error)
71 | }
72 |
73 | return nil
74 | }
75 |
76 | func (c *Client) GetInfo(addr string) (*GetInfoJSON, error) {
77 | res := &GetInfoJSON{}
78 | err := c.RPCCall(addr, "getInfo", nil, res)
79 | if err != nil {
80 | return nil, err
81 | }
82 | return res, nil
83 | }
84 |
85 | func (c *Client) SendMsg(address string, msg interface{}, waitResponse bool) (reply *nkn.Message, err error) {
86 | if c.ReplyTimeout == 0 {
87 | c.ReplyTimeout = replyTimeout
88 | }
89 |
90 | reqBytes, err := json.Marshal(msg)
91 | if err != nil {
92 | return nil, err
93 | }
94 |
95 | var onReply *nkn.OnMessage
96 | for i := 0; i < 3; i++ {
97 | onReply, err = c.Send(nkn.NewStringArray(address), reqBytes, nkn.GetDefaultMessageConfig())
98 | if err != nil {
99 | return nil, err
100 | }
101 |
102 | if !waitResponse {
103 | return nil, nil
104 | }
105 |
106 | select {
107 | case reply = <-onReply.C:
108 | return reply, nil
109 |
110 | case <-time.After(c.ReplyTimeout):
111 | err = ErrReplyTimeout
112 | }
113 | }
114 |
115 | if err == ErrReplyTimeout {
116 | log.Printf("Wait for repsone timeout, please make sure the peer is running and reachable")
117 | }
118 |
119 | return nil, err
120 | }
121 |
--------------------------------------------------------------------------------
/web/src/assets/network_rpc.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import * as util from './util';
4 |
5 | const rpcAddr = '/rpc/network';
6 |
7 | const methods = {
8 | getNetworkConfig: { method: 'getNetworkConfig' },
9 | setNetworkConfig: { method: 'setNetworkConfig' },
10 | authorizeMember: { method: 'authorizeMember' },
11 | removeMember: { method: 'removeMember' },
12 | deleteWaiting: { method: 'deleteWaiting' },
13 | setAcceptAddress: { method: 'setAcceptAddress' },
14 | sendToken: { method: 'sendToken' },
15 | nknPing: { method: 'nknPing' },
16 | }
17 |
18 | var rpc = {};
19 | for (let method in methods) {
20 | if (methods.hasOwnProperty(method)) {
21 | rpc[method] = (addr, params) => {
22 | params = util.assignDefined({}, methods[method].defaultParams, params)
23 | return rpcCall(addr, methods[method].method, params);
24 | }
25 | }
26 | }
27 |
28 | async function rpcCall(addr, method, params = {}) {
29 | let headers;
30 | try {
31 | headers = await window.rpcHeaders;
32 | } catch (e) {
33 | console.error('Await rpc headers error:', e);
34 | }
35 |
36 | let response = await axios({
37 | url: addr,
38 | method: 'POST',
39 | timeout: 10000,
40 | headers,
41 | // withCredentials: true,
42 | data: {
43 | id: 'nConnect-web',
44 | jsonrpc: '2.0',
45 | method: method,
46 | params: params,
47 | },
48 | });
49 |
50 | let data = response.data;
51 |
52 | if (data.error) {
53 | throw data.error;
54 | }
55 |
56 | if (data.result !== undefined) {
57 | return data.result;
58 | }
59 |
60 | throw new Error('rpc response contains no result or error field');
61 | }
62 |
63 | export async function getNetworkConfig() {
64 | return rpc.getNetworkConfig(rpcAddr);
65 | }
66 |
67 | export async function setNetworkConfig(networkConfig) {
68 | return rpc.setNetworkConfig(rpcAddr, {domain: networkConfig.domain, ipStart: networkConfig.ipStart, ipEnd: networkConfig.ipEnd,
69 | netmask: networkConfig.netmask, gateway: networkConfig.gateway, dns: networkConfig.dns});
70 | }
71 |
72 | export async function authorizeMember(address) {
73 | return rpc.authorizeMember(rpcAddr, {address});
74 | }
75 |
76 | export async function removeMember(address) {
77 | return rpc.removeMember(rpcAddr, {address});
78 | }
79 |
80 | export async function deleteWaiting(address) {
81 | return rpc.deleteWaiting(rpcAddr, {address});
82 | }
83 |
84 | export async function setAcceptAddress(address, acceptAddresses) {
85 | return rpc.setAcceptAddress(rpcAddr, {address: address, AcceptAddresses: acceptAddresses});
86 | }
87 |
88 | export async function sendToken(address, amount) {
89 | return rpc.sendToken(rpcAddr, {address, amount});
90 | }
91 |
92 | export async function nknPing(address) {
93 | return rpc.nknPing(rpcAddr, {address});
94 | }
--------------------------------------------------------------------------------
/web/src/locales/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "简体中文",
3 | "mobile tab": "移动端连接",
4 | "desktop tab": "桌面端连接",
5 | "data plan tab": "购买流量",
6 | "unlimited": "无限",
7 | "need help tab": "需要帮助",
8 | "advance tab": "高级",
9 | "download nConnect part1": "下载 nConnect",
10 | "download nConnect part2": "并创建账户",
11 | "add device from mobile": "打开nConnect手机端扫描上面的二维码来添加此设备。",
12 | "connect from mobile": "开启到此设备的连接后就可以在任何应用中用此设备的本地 IP 地址来访问此设备。",
13 | "mobile guide": "更详尽的步骤请参见这篇教程。",
14 | "add device in mobile first": "1. 在 nConnect 中添加此设备。",
15 | "add server from desktop": "2. 下载 nConnect 桌面版客户端,选择添加服务器,屏幕上将会显示一个二维码。",
16 | "scan QR code to add server to desktop": "3. 打开 nConnect 并选择此设备,点击在 nConnect 桌面版中添加此设备,然后扫码上一步中出现的二维码来添加设备。",
17 | "connect from desktop": "4. 开启到此设备的连接后就可以在任何应用中用此设备的本地 IP 地址来访问此设备。",
18 | "desktop guide": "更详尽的步骤请参见这篇教程",
19 | "purchase method": "以下两种方式均可以购买流量,您可以选择其中任何一种:",
20 | "purchase from mobile": "1. 打开 nConnect,选择此设备并点击购买流量。",
21 | "purchase from web": "2. 或者可以在在线支付页面购买流量。",
22 | "need help method": "如果您有任何问题或建议,可以通过以下方式联系我们:",
23 | "create forum post": "1. 在论坛的 nConnect 板块发帖。",
24 | "Q&A": "2. Q&A",
25 | "send email": "3. 发送电子邮件至 {email}。",
26 | "mobile customer service": "4. 打开 nConnect,选择此设备并点击客服。",
27 | "local IP address": "本地 IP 地址",
28 | "access key": "访问密钥(5 分钟内有效)",
29 | "accept addresses": "白名单地址",
30 | "admins": "管理员地址",
31 | "save": "保存",
32 | "save success": "保存成功!",
33 | "export account": "导出账号",
34 | "import account": "导入账号",
35 | "export tip": "购买前后请先在“高级”中将本账号导出保存,为了用户隐私安全,NKN不保存任何用户信息,用户需自行保存账户信息",
36 | "exportConfirm": "确定要导出账号吗?账号导出后请保密存储。任何获得账号的人都可以使用账号中的流量或解密此账号发送的数据。如果是其他人要求您导出账号,请勿继续。",
37 | "exportSuccess": "导出成功!此账号的私钥种子是:{seed}",
38 | "importConfirm": "确定要导入账号吗?如果您未导出并备份此设备上的当前账号,该账号将永远丢失。导入完成后,所有客户端需要重新添加此设备。",
39 | "importPromptCurrent": "请输入当前账号的私钥种子,并确定您已导出并备份当前账号:",
40 | "importWrongCurrent": "当前账号的私钥种子不匹配!请确定您已导出并备份当前账号。",
41 | "importPromptNew": "请输入您要导入的账号的私钥种子:",
42 | "importSuccess": "导入成功!请重启此设备上的 nConnect 以使用新账号。",
43 | "estimatedRemainingData": "预计剩余高速流量",
44 | "currentServerRegion": "当前服务器区域",
45 | "customized": "自定义",
46 | "tunaConfigChoiceTitle": "请选择服务器区域",
47 | "cancel": "取消",
48 | "setTunaConfigSuccess": "修改服务器区域成功!到此设备的新连接将使用新配置。",
49 | "serverRegion": "服务器区域",
50 | "global": "全球",
51 | "help": "帮助",
52 | "china": "中国",
53 | "asia": "亚洲",
54 | "premium": "高速",
55 | "chinaHighSpeed": "中国极速",
56 | "chinaPlatinum": "中国铂金",
57 | "download log": "下载日志",
58 | "no log available": "没有可用的日志",
59 | "notEnabled": "未启用"
60 | }
61 |
--------------------------------------------------------------------------------
/web/src/locales/zh-TW.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "繁體中文",
3 | "mobile tab": "移動版連接",
4 | "desktop tab": "桌面版連接",
5 | "data plan tab": "購買流量",
6 | "unlimited": "無限",
7 | "need help tab": "需要幫助",
8 | "advance tab": "高級",
9 | "download nConnect part1": "下載 nConnect",
10 | "download nConnect part2": "並創建帳戶",
11 | "add device from mobile": "打開nConnect手機端掃描上面的二維碼來添加此設備。",
12 | "connect from mobile": "開啟到此設備的連接後就可以在任何APP中用此設備的本地 IP 位址來訪問此設備。",
13 | "mobile guide": "更詳盡的步驟請參見這篇教程。",
14 | "add device in mobile first": "1. 在 nConnect 中添加此設備。",
15 | "add server from desktop": "2. 下載 nConnect 桌面版用戶端,選擇添加伺服器,螢幕上將會顯示一個二維碼。",
16 | "scan QR code to add server to desktop": "3. 打開 nConnect 並選擇此設備,點擊在 nConnect 桌面版中添加此設備,然後掃碼上一步中出現的二維碼來添加設備。",
17 | "connect from desktop": "4. 開啟到此設備的連接後就可以在任何應用中用此設備的本地 IP 位址來訪問此設備。",
18 | "desktop guide": "更詳盡的步驟請參見這篇教程",
19 | "purchase method": "以下兩種方式均可以購買流量,您可以選擇其中任何一種:",
20 | "purchase from mobile": "1. 打開 nConnect,選擇此設備並點擊購買流量。",
21 | "purchase from web": "2. 或者可以在線上支付頁面購買流量。",
22 | "need help method": "如果您有任何問題或建議,可以通過以下方式聯繫我們:",
23 | "create forum post": "1. 在論壇的 nConnect 板塊發帖。",
24 | "Q&A": "2. Q&A",
25 | "send email": "3. 發送電子郵件至 {email}。",
26 | "mobile customer service": "4. 打開 nConnect,選擇此設備並點擊客服。",
27 | "local IP address": "本地 IP 地址",
28 | "access key": "訪問金鑰(5 分鐘內有效)",
29 | "accept addresses": "白名單地址",
30 | "admins": "管理員地址",
31 | "save": "保存",
32 | "save success": "保存成功!",
33 | "export account": "導出賬號",
34 | "import account": "導入賬號",
35 | "export tip": "購買前後請先在“高級”中將本賬號導出保存,為了用戶隱私安全,NKN不保存任何用戶信息,用戶需自行保存賬戶信息",
36 | "exportConfirm": "確定要導出賬號嗎?賬號導出後請保密存儲。任何獲得賬號的人都可以使用賬號中的流量或解密此賬號發送的數據。如果是其他人要求您導出賬號,請勿繼續。",
37 | "exportSuccess": "導出成功!此賬號的私鑰種子是:{seed}",
38 | "importConfirm": "確定要導入賬號嗎?如果您未導出並備份此設備上的當前賬號,該賬號將永遠丟失。導入完成後,所有客戶端需要重新添加此設備。",
39 | "importPromptCurrent": "請輸入當前賬號的私鑰種子,並確定您已導出並備份當前賬號:",
40 | "importWrongCurrent": "當前賬號的私鑰種子不匹配!請確定您已導出並備份當前賬號。",
41 | "importPromptNew": "請輸入您要導入的賬號的私鑰種子:",
42 | "importSuccess": "導入成功!請重啟此設備上的 nConnect 以使用新賬號。",
43 | "estimatedRemainingData": "預計剩餘高速流量",
44 | "currentServerRegion": "當前服務器區域",
45 | "customized": "自定義",
46 | "tunaConfigChoiceTitle": "請選擇服務器區域",
47 | "cancel": "取消",
48 | "setTunaConfigSuccess": "修改服務器區域成功!到此設備的新連接將使用新配置。",
49 | "serverRegion": "服務器區域",
50 | "global": "全球",
51 | "help": "幫助",
52 | "china": "中國",
53 | "asia": "亞洲",
54 | "premium": "高速",
55 | "chinaHighSpeed": "中國極速",
56 | "chinaPlatinum": "中國鉑金",
57 | "download log": "下載日誌",
58 | "no log available": "沒有可用的日誌",
59 | "notEnabled": "未啟用"
60 | }
61 |
--------------------------------------------------------------------------------
/web/src/README.md:
--------------------------------------------------------------------------------
1 | # nconnect-web
2 |
3 | ## Build Setup
4 |
5 | ```bash
6 | # install dependencies
7 | $ yarn install
8 |
9 | # serve with hot reload at localhost:3000
10 | $ yarn dev
11 |
12 | # build for production and launch server
13 | $ yarn build
14 | $ yarn start
15 |
16 | # generate static project
17 | $ yarn generate
18 | ```
19 |
20 | For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
21 |
22 | ## Special Directories
23 |
24 | You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
25 |
26 | ### `assets`
27 |
28 | The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
29 |
30 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
31 |
32 | ### `components`
33 |
34 | The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
35 |
36 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
37 |
38 | ### `layouts`
39 |
40 | Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
41 |
42 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
43 |
44 |
45 | ### `pages`
46 |
47 | This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
48 |
49 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
50 |
51 | ### `plugins`
52 |
53 | The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
54 |
55 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
56 |
57 | ### `static`
58 |
59 | This directory contains your static files. Each file inside this directory is mapped to `/`.
60 |
61 | Example: `/static/robots.txt` is mapped as `/robots.txt`.
62 |
63 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
64 |
65 | ### `store`
66 |
67 | This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
68 |
69 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).
70 |
--------------------------------------------------------------------------------
/ss/plugin.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "syscall"
10 | "time"
11 | )
12 |
13 | var pluginCmd *exec.Cmd
14 |
15 | func startPlugin(plugin, pluginOpts, ssAddr string, isServer bool) (newAddr string, err error) {
16 | logf("starting plugin (%s) with option (%s)....", plugin, pluginOpts)
17 | freePort, err := getFreePort()
18 | if err != nil {
19 | return "", fmt.Errorf("failed to fetch an unused port for plugin (%v)", err)
20 | }
21 | localHost := "127.0.0.1"
22 | ssHost, ssPort, err := net.SplitHostPort(ssAddr)
23 | if err != nil {
24 | return "", err
25 | }
26 | newAddr = localHost + ":" + freePort
27 | if isServer {
28 | if ssHost == "" {
29 | ssHost = "0.0.0.0"
30 | }
31 | logf("plugin (%s) will listen on %s:%s", plugin, ssHost, ssPort)
32 | } else {
33 | logf("plugin (%s) will listen on %s:%s", plugin, localHost, freePort)
34 | }
35 | err = execPlugin(plugin, pluginOpts, ssHost, ssPort, localHost, freePort)
36 | return
37 | }
38 |
39 | func killPlugin() {
40 | if pluginCmd != nil {
41 | pluginCmd.Process.Signal(syscall.SIGTERM)
42 | waitCh := make(chan struct{})
43 | go func() {
44 | pluginCmd.Wait()
45 | close(waitCh)
46 | }()
47 | timeout := time.After(3 * time.Second)
48 | select {
49 | case <-waitCh:
50 | case <-timeout:
51 | pluginCmd.Process.Kill()
52 | }
53 | }
54 | }
55 |
56 | func execPlugin(plugin, pluginOpts, remoteHost, remotePort, localHost, localPort string) (err error) {
57 | pluginFile := plugin
58 | if fileExists(plugin) {
59 | if !filepath.IsAbs(plugin) {
60 | pluginFile = "./" + plugin
61 | }
62 | } else {
63 | pluginFile, err = exec.LookPath(plugin)
64 | if err != nil {
65 | return err
66 | }
67 | }
68 | logH := newLogHelper("[" + plugin + "]: ")
69 | env := append(os.Environ(),
70 | "SS_REMOTE_HOST="+remoteHost,
71 | "SS_REMOTE_PORT="+remotePort,
72 | "SS_LOCAL_HOST="+localHost,
73 | "SS_LOCAL_PORT="+localPort,
74 | "SS_PLUGIN_OPTIONS="+pluginOpts,
75 | )
76 | cmd := &exec.Cmd{
77 | Path: pluginFile,
78 | Env: env,
79 | Stdout: logH,
80 | Stderr: logH,
81 | }
82 | if err = cmd.Start(); err != nil {
83 | return err
84 | }
85 | pluginCmd = cmd
86 | go func() {
87 | if err := cmd.Wait(); err != nil {
88 | logf("plugin exited (%v)\n", err)
89 | os.Exit(2)
90 | }
91 | logf("plugin exited\n")
92 | }()
93 | return nil
94 | }
95 |
96 | func fileExists(filename string) bool {
97 | info, err := os.Stat(filename)
98 | if os.IsNotExist(err) {
99 | return false
100 | }
101 | return !info.IsDir()
102 | }
103 |
104 | func getFreePort() (string, error) {
105 | addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
106 | if err != nil {
107 | return "", err
108 | }
109 |
110 | l, err := net.ListenTCP("tcp", addr)
111 | if err != nil {
112 | return "", err
113 | }
114 | port := fmt.Sprintf("%d", l.Addr().(*net.TCPAddr).Port)
115 | l.Close()
116 | return port, nil
117 | }
118 |
--------------------------------------------------------------------------------
/tests/udp.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "net"
9 | "time"
10 |
11 | "github.com/txthinking/socks5"
12 | )
13 |
14 | func StartUDPServer(port string) error {
15 | a, err := net.ResolveUDPAddr("udp", port)
16 | if err != nil {
17 | return err
18 | }
19 | udpServer, err := net.ListenUDP("udp", a)
20 | if err != nil {
21 | return err
22 | }
23 |
24 | defer udpServer.Close()
25 | log.Printf("UDP server is listening at %v\n", port)
26 |
27 | b := make([]byte, 1024)
28 | for {
29 | n, addr, err := udpServer.ReadFromUDP(b)
30 | if err != nil {
31 | log.Printf("StartUdpServer.ReadFromUDP err: %v\n", err)
32 | return err
33 | }
34 | log.Printf("UDP Server got: %v from %v\n", string(b[:n]), addr.String())
35 |
36 | time.Sleep(100 * time.Millisecond)
37 | _, _, err = udpServer.WriteMsgUDP(b[:n], nil, addr)
38 | if err != nil {
39 | log.Printf("StartUdpServer.WriteMsgUDP err: %v\n", err)
40 | return err
41 | }
42 | }
43 | }
44 |
45 | func StartUDPClient(serverAddr string) error {
46 | proxyAddr := fmt.Sprintf("127.0.0.1:%v", port)
47 | s5c, err := socks5.NewClient(proxyAddr, "", "", 0, 60)
48 | if err != nil {
49 | return err
50 | }
51 | uc, err := s5c.Dial("udp", serverAddr)
52 | if err != nil {
53 | log.Println("StartUDPClient.s5c.Dial err: ", err)
54 | return err
55 | }
56 | defer uc.Close()
57 |
58 | user := &Person{Name: "udp_boy", Age: 0}
59 | for i := 0; i < numMsgs; i++ {
60 | user.Age++
61 | send, _ := json.Marshal(user)
62 | if _, err := uc.Write(send); err != nil {
63 | log.Println("StartUDPClient.Write err ", err)
64 | return err
65 | }
66 |
67 | recv := make([]byte, 512)
68 | n, err := uc.Read(recv)
69 | if err != nil {
70 | log.Println("StartUDPClient.Read err ", err)
71 | return err
72 | }
73 | if !bytes.Equal(recv[:n], send) {
74 | return fmt.Errorf("StartUDPClient.recv %v is not as same as sent %v", string(recv[:n]), string(send))
75 | } else {
76 | log.Printf("StartUDPClient got echo: %v\n", string(recv[:n]))
77 | }
78 | }
79 |
80 | return nil
81 | }
82 |
83 | func StartUDPTunClient(serverAddr string) error {
84 | uc, err := net.Dial("udp", serverAddr)
85 | if err != nil {
86 | log.Println("StartUDPClient dial err: ", err)
87 | return err
88 | }
89 | defer uc.Close()
90 |
91 | user := &Person{Name: "udp_boy", Age: 0}
92 | for i := 0; i < numMsgs; i++ {
93 | user.Age++
94 | send, _ := json.Marshal(user)
95 | if _, err := uc.Write(send); err != nil {
96 | log.Println("UDP client Write err ", err)
97 | return err
98 | }
99 |
100 | recv := make([]byte, 512)
101 | n, err := uc.Read(recv)
102 | if err != nil {
103 | log.Println("UDP client Read err ", err)
104 | return err
105 | }
106 | if !bytes.Equal(recv[:n], send) {
107 | return fmt.Errorf("UDP client recv %v is not as same as sent %v", string(recv[:n]), string(send))
108 | } else {
109 | log.Printf("UDP client got echo: %v\n", string(recv[:n]))
110 | }
111 | }
112 |
113 | return nil
114 | }
115 |
--------------------------------------------------------------------------------
/web/dist/200.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | nconnect-web - nconnect-web
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/web/src/assets/rpc.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import * as util from './util';
4 |
5 | const rpcAddr = '/rpc/admin';
6 |
7 | const methods = {
8 | getAdminToken: { method: 'getAdminToken' },
9 | getAddrs: { method: 'getAddrs' },
10 | setAddrs: { method: 'setAddrs' },
11 | addAddrs: { method: 'addAddrs' },
12 | removeAddrs: { method: 'removeAddrs' },
13 | getLocalIP: { method: 'getLocalIP' },
14 | getInfo: { method: 'getInfo' },
15 | getBalance: { method: 'getBalance' },
16 | getSeed: { method: 'getSeed' },
17 | setSeed: { method: 'setSeed' },
18 | setTunaConfig: { method: 'setTunaConfig' },
19 | getLog: { method: 'getLog' }
20 | }
21 |
22 | var rpc = {};
23 | for (let method in methods) {
24 | if (methods.hasOwnProperty(method)) {
25 | rpc[method] = (addr, params) => {
26 | params = util.assignDefined({}, methods[method].defaultParams, params)
27 | return rpcCall(addr, methods[method].method, params);
28 | }
29 | }
30 | }
31 |
32 | async function rpcCall(addr, method, params = {}) {
33 | let headers;
34 | try {
35 | headers = await window.rpcHeaders;
36 | } catch (e) {
37 | console.error('Await rpc headers error:', e);
38 | }
39 |
40 | let response = await axios({
41 | url: addr,
42 | method: 'POST',
43 | timeout: 10000,
44 | headers,
45 | withCredentials: true,
46 | data: {
47 | id: 'nConnect-web',
48 | jsonrpc: '2.0',
49 | method: method,
50 | params: params,
51 | },
52 | });
53 |
54 | let data = response.data;
55 |
56 | if (data.error) {
57 | throw data.error;
58 | }
59 |
60 | if (data.result !== undefined) {
61 | return data.result;
62 | }
63 |
64 | throw new Error('rpc response contains no result or error field');
65 | }
66 |
67 | export async function getAdminToken() {
68 | return rpc.getAdminToken(rpcAddr);
69 | }
70 |
71 | export async function getAddrs() {
72 | return rpc.getAddrs(rpcAddr);
73 | }
74 |
75 | export async function setAddrs(acceptAddrs, adminAddrs) {
76 | let params = {};
77 | if (acceptAddrs) {
78 | params.acceptAddrs = acceptAddrs;
79 | }
80 | if (adminAddrs) {
81 | params.adminAddrs = adminAddrs;
82 | }
83 | return rpc.setAddrs(rpcAddr, params);
84 | }
85 |
86 | export async function addAddrs(acceptAddrs, adminAddrs) {
87 | let params = {};
88 | if (acceptAddrs) {
89 | params.acceptAddrs = acceptAddrs;
90 | }
91 | if (adminAddrs) {
92 | params.adminAddrs = adminAddrs;
93 | }
94 | return rpc.addAddrs(rpcAddr, params);
95 | }
96 |
97 | export async function removeAddrs(acceptAddrs, adminAddrs) {
98 | let params = {};
99 | if (acceptAddrs) {
100 | params.acceptAddrs = acceptAddrs;
101 | }
102 | if (adminAddrs) {
103 | params.adminAddrs = adminAddrs;
104 | }
105 | return rpc.removeAddrs(rpcAddr, params);
106 | }
107 |
108 | export async function getLocalIP() {
109 | return rpc.getLocalIP(rpcAddr);
110 | }
111 |
112 | export async function getInfo() {
113 | return rpc.getInfo(rpcAddr);
114 | }
115 |
116 | export async function getBalance() {
117 | return rpc.getBalance(rpcAddr);
118 | }
119 |
120 | export async function getSeed() {
121 | return rpc.getSeed(rpcAddr);
122 | }
123 |
124 | export async function setSeed(seed) {
125 | return rpc.setSeed(rpcAddr, { seed });
126 | }
127 |
128 | export async function setTunaConfig(tunaConfig) {
129 | return rpc.setTunaConfig(rpcAddr, tunaConfig);
130 | }
131 |
132 | export async function getLog() {
133 | return rpc.getLog(rpcAddr);
134 | }
135 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL:=local_or_with_proxy
2 |
3 | USE_PROXY=GOPROXY=https://goproxy.io
4 | VERSION:=$(shell git describe --abbrev=7 --dirty --always --tags)
5 | LDFLAGS="-s -w -X github.com/nknorg/nconnect/config.Version=$(VERSION)"
6 | BUILD=CGO_ENABLED=1 go build -ldflags $(LDFLAGS)
7 | MAIN=./bin
8 | XGO_MODULE=github.com/nknorg/nconnect/bin
9 | XGO_BUILD=xgo -ldflags $(LDFLAGS) --targets=$(XGO_TARGET) $(XGOFLAGS)
10 | BUILD_DIR=build
11 | BIN_NAME=nConnect
12 |
13 | ifdef GOARM
14 | BIN_DIR=$(GOOS)-$(GOARCH)v$(GOARM)
15 | XGO_TARGET=$(GOOS)/$(GOARCH)-$(GOARM)
16 | else
17 | BIN_DIR=$(GOOS)-$(GOARCH)
18 | XGO_TARGET=$(GOOS)/$(GOARCH)
19 | endif
20 |
21 | LOCAL_EXT=$(EXT)
22 | ifeq ($(OS),Windows_NT)
23 | ifeq ($(LOCAL_EXT),)
24 | LOCAL_EXT=.exe
25 | endif
26 | endif
27 |
28 | web/dist: $(shell find web/src -type f -not -path "web/src/node_modules/*" -not -path "web/src/dist/*")
29 | -@cd web/src && yarn && yarn generate && rm -rf ./dist/index.html.html && rm -rf ../dist && cp -a ./dist ../dist
30 |
31 | .PHONY: local
32 | local: web/dist
33 | $(BUILD) -o $(BIN_NAME)$(LOCAL_EXT) $(MAIN)
34 |
35 | .PHONY: local_with_proxy
36 | local_with_proxy: web/dist
37 | $(USE_PROXY) ${MAKE} local
38 |
39 | .PHONY: local_or_with_proxy
40 | local_or_with_proxy:
41 | ${MAKE} local || ${MAKE} local_with_proxy
42 |
43 | .PHONY: build
44 | build: web/dist
45 | rm -rf $(BUILD_DIR)/$(BIN_DIR)
46 | mkdir -p $(BUILD_DIR)/$(BIN_DIR)
47 | cd $(BUILD_DIR)/$(BIN_DIR) && $(XGO_BUILD) -out $(BIN_NAME) $(XGO_MODULE) && mv $(BIN_NAME)* $(BIN_NAME)$(EXT)
48 | mkdir -p $(BUILD_DIR)/$(BIN_DIR)/web/
49 | @cp -a web/dist $(BUILD_DIR)/$(BIN_DIR)/web/
50 | ${MAKE} tar
51 |
52 | .PHONY: tar
53 | tar:
54 | cd $(BUILD_DIR) && rm -f $(BIN_DIR).tar.gz && tar --exclude ".DS_Store" --exclude "__MACOSX" -czvf $(BIN_DIR).tar.gz $(BIN_DIR)
55 |
56 | .PHONY: zip
57 | zip:
58 | cd $(BUILD_DIR) && rm -f $(BIN_DIR).zip && zip --exclude "*.DS_Store*" --exclude "*__MACOSX*" -r $(BIN_DIR).zip $(BIN_DIR)
59 |
60 | .PHONY: all
61 | all:
62 | ${MAKE} build GOOS=darwin GOARCH=amd64
63 | ${MAKE} build GOOS=linux GOARCH=amd64
64 | ${MAKE} build GOOS=linux GOARCH=arm64
65 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=5
66 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=6
67 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=7
68 | ${MAKE} build GOOS=windows GOARCH=amd64 EXT=.exe
69 | ${MAKE} build GOOS=windows GOARCH=386 EXT=.exe
70 |
71 | .PHONY: docker
72 | docker:
73 | ${MAKE} build GOOS=linux GOARCH=amd64
74 | docker build -f docker/Dockerfile --build-arg build_dir="./build/linux-amd64" -t nknorg/nconnect:latest-amd64 .
75 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=7
76 | docker build -f docker/Dockerfile --build-arg build_dir="./build/linux-armv7" --build-arg base="arm32v7/" -t nknorg/nconnect:latest-arm32v7 .
77 | ${MAKE} build GOOS=linux GOARCH=arm64
78 | docker build -f docker/Dockerfile --build-arg build_dir="./build/linux-arm64" --build-arg base="arm64v8/" -t nknorg/nconnect:latest-arm64v8 .
79 |
80 | .PHONY: docker_publish
81 | docker_publish:
82 | docker push nknorg/nconnect:latest-amd64
83 | docker push nknorg/nconnect:latest-arm32v7
84 | docker push nknorg/nconnect:latest-arm64v8
85 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create nknorg/nconnect:latest nknorg/nconnect:latest-amd64 nknorg/nconnect:latest-arm32v7 nknorg/nconnect:latest-arm64v8 --amend
86 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest annotate nknorg/nconnect:latest nknorg/nconnect:latest-arm32v7 --os linux --arch arm --variant v7
87 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest annotate nknorg/nconnect:latest nknorg/nconnect:latest-arm64v8 --os linux --arch arm64
88 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push -p nknorg/nconnect:latest
89 |
--------------------------------------------------------------------------------
/tests/tcp.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net"
8 | "strings"
9 | "time"
10 |
11 | "golang.org/x/net/proxy"
12 | )
13 |
14 | func StartTCPServer(port string) error {
15 | tcpServer, err := net.Listen("tcp", port)
16 | if err != nil {
17 | return err
18 | }
19 | log.Printf("TCP server is listening at %v\n", port)
20 |
21 | for {
22 | c, err := tcpServer.Accept()
23 | if err != nil {
24 | return err
25 | }
26 |
27 | log.Printf("TCP server get a connection from %v\n", c.RemoteAddr())
28 |
29 | go func(conn net.Conn) {
30 | defer conn.Close()
31 | b := make([]byte, 1024)
32 | for {
33 | n, err := conn.Read(b)
34 | if err != nil {
35 | if strings.Contains(err.Error(), "closed") {
36 | log.Printf("client connection closed\n")
37 | } else {
38 | log.Printf("StartTcpServer, Read err %v\n", err)
39 | }
40 | break
41 | }
42 |
43 | log.Printf("TCP Server got: %v\n", string(b[:n]))
44 | _, err = conn.Write(b[:n])
45 | if err != nil {
46 | log.Printf("StartTcpServer, write err %v\n", err)
47 | break
48 | }
49 | }
50 | }(c)
51 | }
52 | }
53 |
54 | func StartTCPClient(serverAddr string) error {
55 | auth := proxy.Auth{User: "", Password: ""}
56 | proxyAddr := fmt.Sprintf("127.0.0.1:%v", port)
57 | dailer, err := proxy.SOCKS5("tcp", proxyAddr, &auth, &net.Dialer{
58 | Timeout: 60 * time.Second,
59 | KeepAlive: 30 * time.Second,
60 | })
61 | if err != nil {
62 | log.Printf("StartTCPClient, proxy.SOCKS5 err: %v\n", err)
63 | return err
64 | }
65 |
66 | conn, err := dailer.Dial("tcp", serverAddr)
67 | if err != nil {
68 | log.Printf("StartTCPClient, dailer.Dial err: %v\n", err)
69 | return err
70 | }
71 |
72 | defer conn.Close()
73 | log.Printf("StartTCPClient, dail to %v success\n", serverAddr)
74 |
75 | user := &Person{Name: "tcp_boy", Age: 0}
76 | for i := 0; i < numMsgs; i++ {
77 | user.Age++
78 | b1, _ := json.Marshal(user)
79 | _, err = conn.Write(b1)
80 | if err != nil {
81 | log.Printf("StartTCPClient, conn.Write err: %v\n", err)
82 | return err
83 | }
84 | log.Printf("StartTCPClient, conn.Write %+v\n", user)
85 |
86 | b2 := make([]byte, 1024)
87 | n, err := conn.Read(b2)
88 | if err != nil {
89 | log.Printf("StartTCPClient, conn.Read err: %v\n", err)
90 | return err
91 | }
92 | respUser := &Person{}
93 | err = json.Unmarshal(b2[:n], respUser)
94 | if err != nil {
95 | log.Printf("StartTCPClient, json.Unmarshal err: %v\n", err)
96 | return err
97 | }
98 |
99 | if respUser.Age != user.Age {
100 | return fmt.Errorf("StartTCPClient, got wrong response, sent %+v, recv %+v", user, respUser)
101 | }
102 | log.Printf("Got echo %+v\n", respUser)
103 | }
104 |
105 | return nil
106 | }
107 |
108 | func StartTCPTunClient(serverAddr string) error {
109 | conn, err := net.Dial("tcp", serverAddr)
110 | if err != nil {
111 | log.Printf("StartTCPClient, dailer.Dial err: %v\n", err)
112 | return err
113 | }
114 |
115 | user := &Person{Name: "tcp_boy", Age: 0}
116 | for i := 0; i < numMsgs; i++ {
117 | user.Age++
118 | b1, _ := json.Marshal(user)
119 | _, err = conn.Write(b1)
120 | if err != nil {
121 | log.Printf("StartTCPClient, conn.Write err: %v\n", err)
122 | return err
123 | }
124 |
125 | b2 := make([]byte, 1024)
126 | n, err := conn.Read(b2)
127 | if err != nil {
128 | log.Printf("StartTCPClient, conn.Read err: %v\n", err)
129 | return err
130 | }
131 | respUser := &Person{}
132 | err = json.Unmarshal(b2[:n], respUser)
133 | if err != nil {
134 | log.Printf("StartTCPClient, json.Unmarshal err: %v\n", err)
135 | return err
136 | }
137 | log.Printf("TCP client got echo: %+v\n", respUser)
138 |
139 | if respUser.Age != user.Age {
140 | return fmt.Errorf("StartTCPClient, got wrong response, sent %+v, recv %+v", user, respUser)
141 | }
142 | }
143 |
144 | return nil
145 | }
146 |
--------------------------------------------------------------------------------
/tests/web.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 | "net/url"
11 | "time"
12 | )
13 |
14 | func StartWebServer() error {
15 | http.HandleFunc("/httpEcho", httpEcho)
16 | fmt.Println("WEB server is serving at ", httpPort)
17 | if err := http.ListenAndServe(httpPort, nil); err != nil {
18 | log.Fatal(err)
19 | }
20 |
21 | return nil
22 | }
23 |
24 | func httpEcho(w http.ResponseWriter, r *http.Request) {
25 | if r.Method != http.MethodPost {
26 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
27 | return
28 | }
29 |
30 | user := &Person{}
31 | err := json.NewDecoder(r.Body).Decode(user)
32 | if err != nil {
33 | http.Error(w, err.Error(), http.StatusBadRequest)
34 | return
35 | }
36 |
37 | b, _ := json.Marshal(user)
38 | w.Write(b)
39 | }
40 |
41 | func StartWebClient(httpServUrl string) error {
42 | fmt.Printf("http request to: %v\n", httpServUrl)
43 |
44 | proxyAddr := fmt.Sprintf("127.0.0.1:%v", port)
45 | socksProxy := "socks5://" + proxyAddr
46 | proxy := func(_ *http.Request) (*url.URL, error) {
47 | return url.Parse(socksProxy)
48 | }
49 |
50 | httpTransport := &http.Transport{
51 | Proxy: proxy,
52 | }
53 |
54 | httpClient := &http.Client{
55 | Transport: httpTransport,
56 | Timeout: 10 * time.Second,
57 | }
58 |
59 | user := &Person{Name: "http_boy", Age: 0}
60 | b := new(bytes.Buffer)
61 |
62 | for i := 0; i < numMsgs; i++ {
63 | user.Age++
64 | err := json.NewEncoder(b).Encode(user)
65 | if err != nil {
66 | fmt.Printf("StartWebClient.Encode err: %v\n", err)
67 | return err
68 | }
69 | req, err := http.NewRequest(http.MethodPost, httpServUrl, b)
70 | req.Header.Set("Content-type", "application/json")
71 |
72 | if err != nil {
73 | fmt.Printf("StartWebClient.http.NewRequest err: %v\n", err)
74 | return err
75 | }
76 |
77 | resp, err := httpClient.Do(req)
78 | if err != nil {
79 | fmt.Printf("StartWebClient.http.Do err: %v\n", err)
80 | return err
81 | }
82 | defer resp.Body.Close()
83 |
84 | body, err := io.ReadAll(resp.Body)
85 | if err != nil {
86 | fmt.Printf("StartWebClient.io.ReadAll err: %v\n", err)
87 | return err
88 | }
89 |
90 | respUser := &Person{}
91 | err = json.Unmarshal(body, respUser)
92 | if err != nil {
93 | fmt.Printf("StartWebClient.json.Unmarshal %v err: %v\n", string(body), err)
94 | return err
95 | }
96 |
97 | if respUser.Age != user.Age {
98 | return fmt.Errorf("StartWebClient got wrong response, sent %+v, recv %+v", user, respUser)
99 | } else {
100 | fmt.Printf("StartWebClient got echo: %+v\n", respUser)
101 | }
102 | }
103 |
104 | time.Sleep(time.Second)
105 |
106 | return nil
107 | }
108 |
109 | func StartTunWebClient(httpServUrl string) error {
110 | httpClient := &http.Client{
111 | Timeout: 10 * time.Second,
112 | }
113 |
114 | user := &Person{Name: "http_tun_boy", Age: 0}
115 | b := new(bytes.Buffer)
116 |
117 | for i := 0; i < 10; i++ {
118 | user.Age++
119 | err := json.NewEncoder(b).Encode(user)
120 | if err != nil {
121 | fmt.Printf("StartWebClient.Encode err: %v\n", err)
122 | return err
123 | }
124 | req, err := http.NewRequest(http.MethodPost, httpServUrl, b)
125 | req.Header.Set("Content-type", "application/json")
126 |
127 | if err != nil {
128 | fmt.Printf("StartWebClient.http.NewRequest err: %v\n", err)
129 | return err
130 | }
131 |
132 | resp, err := httpClient.Do(req)
133 | if err != nil {
134 | fmt.Printf("StartWebClient.http.Do err: %v\n", err)
135 | return err
136 | }
137 | defer resp.Body.Close()
138 |
139 | body, err := io.ReadAll(resp.Body)
140 | if err != nil {
141 | fmt.Printf("StartWebClient.io.ReadAll err: %v\n", err)
142 | return err
143 | }
144 |
145 | respUser := &Person{}
146 | err = json.Unmarshal(body, respUser)
147 | if err != nil {
148 | fmt.Printf("StartWebClient.json.Unmarshal err: %v\n", err)
149 | return err
150 | }
151 |
152 | if respUser.Age != user.Age {
153 | return fmt.Errorf("StartWebClient got wrong response, sent %+v, recv %+v", user, respUser)
154 | }
155 | }
156 |
157 | return nil
158 | }
159 |
--------------------------------------------------------------------------------
/web/dist/sw.js:
--------------------------------------------------------------------------------
1 | const options = {"workboxURL":"https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.4/workbox/workbox-sw.js","importScripts":[],"config":{"debug":false},"cacheOptions":{"cacheId":"nconnect-web-prod","directoryIndex":"/","revision":"8ltGyYB9ibtT"},"clientsClaim":true,"skipWaiting":true,"cleanupOutdatedCaches":true,"offlineAnalytics":false,"preCaching":[{"revision":"8ltGyYB9ibtT","url":"/?standalone=true"}],"runtimeCaching":[{"urlPattern":"/_nuxt/","handler":"CacheFirst","method":"GET","strategyPlugins":[]},{"urlPattern":"/","handler":"NetworkFirst","method":"GET","strategyPlugins":[]}],"offlinePage":null,"pagesURLPattern":"/","offlineStrategy":"NetworkFirst"}
2 |
3 | importScripts(...[options.workboxURL, ...options.importScripts])
4 |
5 | initWorkbox(workbox, options)
6 | workboxExtensions(workbox, options)
7 | precacheAssets(workbox, options)
8 | cachingExtensions(workbox, options)
9 | runtimeCaching(workbox, options)
10 | offlinePage(workbox, options)
11 | routingExtensions(workbox, options)
12 |
13 | function getProp(obj, prop) {
14 | return prop.split('.').reduce((p, c) => p[c], obj)
15 | }
16 |
17 | function initWorkbox(workbox, options) {
18 | if (options.config) {
19 | // Set workbox config
20 | workbox.setConfig(options.config)
21 | }
22 |
23 | if (options.cacheNames) {
24 | // Set workbox cache names
25 | workbox.core.setCacheNameDetails(options.cacheNames)
26 | }
27 |
28 | if (options.clientsClaim) {
29 | // Start controlling any existing clients as soon as it activates
30 | workbox.core.clientsClaim()
31 | }
32 |
33 | if (options.skipWaiting) {
34 | workbox.core.skipWaiting()
35 | }
36 |
37 | if (options.cleanupOutdatedCaches) {
38 | workbox.precaching.cleanupOutdatedCaches()
39 | }
40 |
41 | if (options.offlineAnalytics) {
42 | // Enable offline Google Analytics tracking
43 | workbox.googleAnalytics.initialize()
44 | }
45 | }
46 |
47 | function precacheAssets(workbox, options) {
48 | if (options.preCaching.length) {
49 | workbox.precaching.precacheAndRoute(options.preCaching, options.cacheOptions)
50 | }
51 | }
52 |
53 |
54 | function runtimeCaching(workbox, options) {
55 | const requestInterceptor = {
56 | requestWillFetch({ request }) {
57 | if (request.cache === 'only-if-cached' && request.mode === 'no-cors') {
58 | return new Request(request.url, { ...request, cache: 'default', mode: 'no-cors' })
59 | }
60 | return request
61 | },
62 | fetchDidFail(ctx) {
63 | ctx.error.message =
64 | '[workbox] Network request for ' + ctx.request.url + ' threw an error: ' + ctx.error.message
65 | console.error(ctx.error, 'Details:', ctx)
66 | },
67 | handlerDidError(ctx) {
68 | ctx.error.message =
69 | `[workbox] Network handler threw an error: ` + ctx.error.message
70 | console.error(ctx.error, 'Details:', ctx)
71 | return null
72 | }
73 | }
74 |
75 | for (const entry of options.runtimeCaching) {
76 | const urlPattern = new RegExp(entry.urlPattern)
77 | const method = entry.method || 'GET'
78 |
79 | const plugins = (entry.strategyPlugins || [])
80 | .map(p => new (getProp(workbox, p.use))(...p.config))
81 |
82 | plugins.unshift(requestInterceptor)
83 |
84 | const strategyOptions = { ...entry.strategyOptions, plugins }
85 |
86 | const strategy = new workbox.strategies[entry.handler](strategyOptions)
87 |
88 | workbox.routing.registerRoute(urlPattern, strategy, method)
89 | }
90 | }
91 |
92 | function offlinePage(workbox, options) {
93 | if (options.offlinePage) {
94 | // Register router handler for offlinePage
95 | workbox.routing.registerRoute(new RegExp(options.pagesURLPattern), ({ request, event }) => {
96 | const strategy = new workbox.strategies[options.offlineStrategy]
97 | return strategy
98 | .handle({ request, event })
99 | .catch(() => caches.match(options.offlinePage))
100 | })
101 | }
102 | }
103 |
104 | function workboxExtensions(workbox, options) {
105 |
106 | }
107 |
108 | function cachingExtensions(workbox, options) {
109 |
110 | }
111 |
112 | function routingExtensions(workbox, options) {
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/tests/pub.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/hex"
6 | "encoding/json"
7 | "fmt"
8 | "log"
9 | "net"
10 | "os"
11 | "time"
12 |
13 | "github.com/nknorg/nconnect"
14 | "github.com/nknorg/nconnect/config"
15 | nkn "github.com/nknorg/nkn-sdk-go"
16 | "github.com/nknorg/nkn/v2/vault"
17 | "github.com/nknorg/tuna"
18 | "github.com/nknorg/tuna/pb"
19 | "github.com/nknorg/tuna/types"
20 | "github.com/nknorg/tuna/util"
21 | "google.golang.org/protobuf/proto"
22 | )
23 |
24 | var ch chan string = make(chan string, 4)
25 |
26 | func startNconnect(configFile string, tuna, udp, tun bool, n *types.Node) error {
27 | b, err := os.ReadFile(configFile)
28 | if err != nil {
29 | log.Fatalf("read config file %v err: %v", configFile, err)
30 | return err
31 | }
32 | var opts = &config.Opts{}
33 | err = json.Unmarshal(b, opts)
34 | if err != nil {
35 | log.Fatalf("parse config %v err: %v", configFile, err)
36 | return err
37 | }
38 |
39 | opts.Config.Tuna = tuna
40 | opts.Config.UDP = udp
41 | opts.Config.Tun = tun
42 | if tun {
43 | opts.Config.VPN = true
44 | }
45 |
46 | if opts.Client {
47 | port, err = getFreePort(port)
48 | if err != nil {
49 | return err
50 | }
51 | opts.LocalSocksAddr = fmt.Sprintf("127.0.0.1:%v", port)
52 | }
53 |
54 | nc, _ := nconnect.NewNconnect(opts)
55 | go func() {
56 | if opts.Server {
57 | nc.SetTunaNode(n)
58 | err = nc.StartServer()
59 | if err != nil {
60 | log.Fatalf("start nconnect server err: %v", err)
61 | }
62 | } else {
63 | err = nc.StartClient()
64 | if err != nil {
65 | log.Fatalf("start nconnect client err: %v", err)
66 | }
67 | }
68 | }()
69 |
70 | time.Sleep(5 * time.Second) // wait for nconnect to create tunnels
71 |
72 | tunnels := nc.GetClientTunnels()
73 | for _, t := range tunnels {
74 | if ts := t.TunaSessionClient(); ts != nil {
75 | <-ts.OnConnect()
76 | }
77 | }
78 |
79 | return err
80 | }
81 |
82 | func getTunaNode(ip string) (*types.Node, error) {
83 | tunaSeed, _ := hex.DecodeString(seedHex)
84 | acc, err := nkn.NewAccount(tunaSeed)
85 | if err != nil {
86 | return nil, err
87 | }
88 |
89 | if ip == "127.0.0.1" {
90 | go runReverseEntry(tunaSeed)
91 | }
92 |
93 | md := &pb.ServiceMetadata{
94 | Ip: ip, // "127.0.0.1",
95 | TcpPort: 30020,
96 | UdpPort: 30021,
97 | ServiceId: 0,
98 | Price: "0.0",
99 | BeneficiaryAddr: "",
100 | }
101 |
102 | metadataRaw, err := proto.Marshal(md)
103 | if err != nil {
104 | log.Fatalln(err)
105 | }
106 | metadata := base64.StdEncoding.EncodeToString(metadataRaw)
107 |
108 | n := &types.Node{
109 | Delay: 0,
110 | Bandwidth: 0,
111 | Metadata: md,
112 | Address: hex.EncodeToString(acc.PublicKey),
113 | MetadataRaw: metadata, // "CgkxMjcuMC4wLjEQxOoBGMXqAToFMC4wMDE=",
114 | }
115 |
116 | return n, nil
117 | }
118 |
119 | func runReverseEntry(seed []byte) error {
120 | entryAccount, err := vault.NewAccountWithSeed(seed)
121 | if err != nil {
122 | return err
123 | }
124 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...)
125 |
126 | walletConfig := &nkn.WalletConfig{
127 | SeedRPCServerAddr: seedRPCServerAddr,
128 | }
129 | entryWallet, err := nkn.NewWallet(&nkn.Account{Account: entryAccount}, walletConfig)
130 | if err != nil {
131 | return err
132 | }
133 |
134 | entryConfig := new(tuna.EntryConfiguration)
135 | err = util.ReadJSON("config.reverse.entry.json", entryConfig)
136 | if err != nil {
137 | return err
138 | }
139 |
140 | err = tuna.StartReverse(entryConfig, entryWallet)
141 | if err != nil {
142 | return err
143 | }
144 |
145 | ch <- tunaNodeStarted
146 |
147 | select {}
148 | }
149 |
150 | type Person struct {
151 | Name string
152 | Age int
153 | }
154 |
155 | func getFreePort(p int) (int, error) {
156 | for i := 0; i < 100; i++ {
157 | addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%v", p))
158 | if err != nil {
159 | return 0, err
160 | }
161 |
162 | l, err := net.ListenTCP("tcp", addr)
163 | if err != nil {
164 | p++
165 | continue
166 | }
167 |
168 | defer l.Close()
169 |
170 | return l.Addr().(*net.TCPAddr).Port, nil
171 | }
172 | return 0, fmt.Errorf("can't find free port")
173 | }
174 |
175 | func waitForSSProxReady() error {
176 | for i := 0; i < 100; i++ {
177 | conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
178 | if err != nil {
179 | time.Sleep(2 * time.Second)
180 | continue
181 | }
182 | if conn != nil {
183 | conn.Close()
184 | return nil
185 | }
186 | }
187 | return fmt.Errorf("ss is not ready after 200 seconds, give up")
188 | }
189 |
--------------------------------------------------------------------------------
/network/webservice.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "path"
8 |
9 | "github.com/gin-contrib/cors"
10 | "github.com/gin-gonic/gin"
11 | "github.com/nknorg/nconnect/admin"
12 | "github.com/nknorg/nconnect/util"
13 | )
14 |
15 | const (
16 | success = "success"
17 | defaultAdminAddr = "127.0.0.1:8000"
18 | )
19 |
20 | type addressData struct {
21 | Address string `json:"address"`
22 | }
23 |
24 | type addresses struct {
25 | Address string `json:"address"`
26 | AcceptAddresses []string `json:"acceptAddresses"`
27 | }
28 |
29 | type sendTokenData struct {
30 | Address string `json:"address"`
31 | Amount string `json:"amount"`
32 | }
33 |
34 | func (m *Manager) StartWebServer() error {
35 | if m.opts.AdminHTTPAddr == "" {
36 | m.opts.AdminHTTPAddr = defaultAdminAddr
37 | }
38 |
39 | gin.SetMode(gin.ReleaseMode)
40 |
41 | r := gin.New() // gin.Default()
42 |
43 | // This is for development, when start web page with "yarn dev" at ../web/src
44 | r.Use(cors.New(cors.Config{
45 | AllowOrigins: []string{"http://localhost:3000"},
46 | AllowMethods: []string{"POST", "OPTIONS"},
47 | AllowHeaders: []string{"Content-Type,access-control-allow-origin, access-control-allow-headers"},
48 | }))
49 |
50 | r.POST("/rpc/network", func(c *gin.Context) {
51 | req := &admin.RpcReq{}
52 | if err := c.ShouldBindJSON(req); err != nil {
53 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
54 | return
55 | }
56 |
57 | resp := m.handleWebRequest(req)
58 | if m.opts.Verbose {
59 | log.Printf("Web request %v, response %+v\n", req.Method, resp)
60 | }
61 |
62 | c.JSON(http.StatusOK, resp)
63 | })
64 |
65 | r.StaticFile("/network", path.Join(m.opts.WebRootPath, "network.html"))
66 | r.StaticFile("/favicon.ico", path.Join(m.opts.WebRootPath, "favicon.ico"))
67 | r.StaticFile("/sw.js", path.Join(m.opts.WebRootPath, "sw.js"))
68 | r.Static("/static", path.Join(m.opts.WebRootPath, "static"))
69 | r.Static("/_nuxt", path.Join(m.opts.WebRootPath, "_nuxt"))
70 | r.Static("/img", path.Join(m.opts.WebRootPath, "img"))
71 | r.Static("/zh", path.Join(m.opts.WebRootPath, "zh"))
72 | r.Static("/zh-TW", path.Join(m.opts.WebRootPath, "zh-TW"))
73 |
74 | log.Println("Network manager web serve at ", "http://"+m.opts.AdminHTTPAddr+"/network")
75 | return r.Run(m.opts.AdminHTTPAddr)
76 | }
77 |
78 | func (m *Manager) handleWebRequest(req *admin.RpcReq) *admin.RpcResp {
79 | resp := &admin.RpcResp{}
80 | var err error
81 |
82 | switch req.Method {
83 | case "getNetworkConfig":
84 | resp.Result = m.GetNetworkConfig()
85 |
86 | case "setNetworkConfig":
87 | params := &networkData{}
88 | if err = util.JSONConvert(req.Params, params); err != nil {
89 | break
90 | }
91 | err = m.SetNetworkConfig(params)
92 | resp.Result = success
93 |
94 | case "authorizeMember":
95 | params := &addressData{}
96 | if err = util.JSONConvert(req.Params, params); err != nil {
97 | break
98 | }
99 | m.AuthorizeMemeber(params.Address)
100 |
101 | resp.Result = success
102 |
103 | case "removeMember":
104 | params := &addressData{}
105 | if err = util.JSONConvert(req.Params, params); err != nil {
106 | break
107 | }
108 | m.RemoveMember(params.Address)
109 |
110 | resp.Result = success
111 |
112 | case "deleteWaiting":
113 | params := &addressData{}
114 | if err = util.JSONConvert(req.Params, params); err != nil {
115 | break
116 | }
117 | m.RemoveMember(params.Address)
118 |
119 | resp.Result = success
120 |
121 | case "setAcceptAddress":
122 | params := &addresses{}
123 | if err = util.JSONConvert(req.Params, params); err != nil {
124 | break
125 | }
126 |
127 | m.SetAcceptAddress(params.Address, params.AcceptAddresses)
128 | resp.Result = success
129 |
130 | case "sendToken":
131 | params := &sendTokenData{}
132 | if err = util.JSONConvert(req.Params, params); err != nil {
133 | break
134 | }
135 | if err = m.SendToken(params.Address, params.Amount); err != nil {
136 | break
137 | }
138 | resp.Result = success
139 |
140 | case "nknPing":
141 | fmt.Println("got network webservice nknPing")
142 | params := &addressData{}
143 | if err = util.JSONConvert(req.Params, params); err != nil {
144 | break
145 | }
146 | ms, err := m.NknPing(params.Address)
147 | if err != nil {
148 | break
149 | }
150 | resp.Result = fmt.Sprintf("%s, RTT time = %v ms", success, ms)
151 |
152 | default:
153 | resp.Error = "nConnect manager webservice got unknown method"
154 | }
155 |
156 | if err != nil {
157 | resp.Error = err.Error()
158 | }
159 |
160 | return resp
161 | }
162 |
--------------------------------------------------------------------------------
/ss/ss.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "encoding/base64"
5 | "errors"
6 | "net/url"
7 | "strings"
8 | "time"
9 |
10 | "github.com/shadowsocks/go-shadowsocks2/core"
11 | "github.com/shadowsocks/go-shadowsocks2/socks"
12 | )
13 |
14 | type Config struct {
15 | Client string
16 | Server string
17 | Cipher string
18 | Key string
19 | Password string
20 | Socks string
21 | RedirTCP string
22 | RedirTCP6 string
23 | TCPTun string
24 | UDPTun string
25 | UDPSocks bool
26 | UDP bool
27 | TCP bool
28 | Plugin string
29 | PluginOpts string
30 | Verbose bool
31 | UDPTimeout time.Duration
32 | TCPCork bool
33 |
34 | TargetToClient map[string]string // map target ip to local tunnel port
35 | DefaultClient string // the default client for the targets are not in Target2Client map
36 | }
37 |
38 | var config struct {
39 | Verbose bool
40 | UDPTimeout time.Duration
41 | TCPCork bool
42 | }
43 |
44 | func Start(flags *Config) error {
45 | if flags.Client == "" && flags.Server == "" {
46 | return errors.New("at least one of client/server mode should be used")
47 | }
48 |
49 | config.Verbose = flags.Verbose
50 | config.UDPTimeout = flags.UDPTimeout
51 | config.TCPCork = flags.TCPCork
52 |
53 | routes.TargetToClient = flags.TargetToClient
54 | routes.DefaultClient = flags.DefaultClient
55 |
56 | var key []byte
57 | if flags.Key != "" {
58 | k, err := base64.URLEncoding.DecodeString(flags.Key)
59 | if err != nil {
60 | return err
61 | }
62 | key = k
63 | }
64 |
65 | errChan := make(chan error, 1)
66 |
67 | if flags.Client != "" { // client mode
68 | addr := flags.Client
69 | cipher := flags.Cipher
70 | password := flags.Password
71 | var err error
72 |
73 | if strings.HasPrefix(addr, "ss://") {
74 | addr, cipher, password, err = parseURL(addr)
75 | if err != nil {
76 | return err
77 | }
78 | }
79 |
80 | udpAddr := addr
81 |
82 | ciph, err := core.PickCipher(cipher, key, password)
83 | if err != nil {
84 | return err
85 | }
86 |
87 | if flags.Plugin != "" {
88 | addr, err = startPlugin(flags.Plugin, flags.PluginOpts, addr, false)
89 | if err != nil {
90 | return err
91 | }
92 | }
93 |
94 | if flags.UDPTun != "" {
95 | for _, tun := range strings.Split(flags.UDPTun, ",") {
96 | p := strings.Split(tun, "=")
97 | go func() {
98 | sendErr(udpLocal(p[0], udpAddr, p[1], ciph.PacketConn), errChan)
99 | }()
100 | }
101 | }
102 |
103 | if flags.TCPTun != "" {
104 | for _, tun := range strings.Split(flags.TCPTun, ",") {
105 | p := strings.Split(tun, "=")
106 | go func() {
107 | sendErr(tcpTun(p[0], addr, p[1], ciph.StreamConn), errChan)
108 | }()
109 | }
110 | }
111 |
112 | if flags.Socks != "" {
113 | socks.UDPEnabled = flags.UDPSocks
114 | go func() {
115 | sendErr(socksLocal(flags.Socks, addr, ciph.StreamConn), errChan)
116 | }()
117 | if flags.UDPSocks {
118 | go func() {
119 | sendErr(udpSocksLocal(flags.Socks, udpAddr, ciph.PacketConn), errChan)
120 | }()
121 | }
122 | }
123 |
124 | if flags.RedirTCP != "" {
125 | go func() {
126 | sendErr(redirLocal(flags.RedirTCP, addr, ciph.StreamConn), errChan)
127 | }()
128 | }
129 |
130 | if flags.RedirTCP6 != "" {
131 | go func() {
132 | sendErr(redir6Local(flags.RedirTCP6, addr, ciph.StreamConn), errChan)
133 | }()
134 | }
135 | }
136 |
137 | if flags.Server != "" { // server mode
138 | addr := flags.Server
139 | cipher := flags.Cipher
140 | password := flags.Password
141 | var err error
142 |
143 | if strings.HasPrefix(addr, "ss://") {
144 | addr, cipher, password, err = parseURL(addr)
145 | if err != nil {
146 | return err
147 | }
148 | }
149 |
150 | udpAddr := addr
151 |
152 | if flags.Plugin != "" {
153 | addr, err = startPlugin(flags.Plugin, flags.PluginOpts, addr, true)
154 | if err != nil {
155 | return err
156 | }
157 | }
158 |
159 | ciph, err := core.PickCipher(cipher, key, password)
160 | if err != nil {
161 | return err
162 | }
163 |
164 | if flags.UDP {
165 | go func() {
166 | sendErr(udpRemote(udpAddr, ciph.PacketConn), errChan)
167 | }()
168 | }
169 | if flags.TCP {
170 | go func() {
171 | sendErr(tcpRemote(addr, ciph.StreamConn), errChan)
172 | }()
173 | }
174 | }
175 |
176 | defer killPlugin()
177 |
178 | return <-errChan
179 | }
180 |
181 | func parseURL(s string) (addr, cipher, password string, err error) {
182 | u, err := url.Parse(s)
183 | if err != nil {
184 | return
185 | }
186 |
187 | addr = u.Host
188 | if u.User != nil {
189 | cipher = u.User.Username()
190 | password, _ = u.User.Password()
191 | }
192 | return
193 | }
194 |
195 | func sendErr(err error, errChan chan error) {
196 | select {
197 | case errChan <- err:
198 | default:
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/nknorg/nconnect
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/eycorsican/go-tun2socks v1.16.11
7 | github.com/gin-contrib/cors v1.4.0
8 | github.com/gin-contrib/gzip v0.0.3
9 | github.com/gin-gonic/gin v1.9.0
10 | github.com/imdario/mergo v0.3.15
11 | github.com/jessevdk/go-flags v1.5.0
12 | github.com/nknorg/ncp-go v1.0.6-0.20230228002512-f4cd1740bebd
13 | github.com/nknorg/nkn-sdk-go v1.4.6-0.20230404044330-ad192f36d07e
14 | github.com/nknorg/nkn-tuna-session v0.2.6
15 | github.com/nknorg/nkn-tunnel v0.3.5
16 | github.com/nknorg/nkn/v2 v2.2.0
17 | github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9
18 | github.com/nknorg/tuna v0.0.0-20230818024750-e800a743f680
19 | github.com/shadowsocks/go-shadowsocks2 v0.1.5
20 | github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b
21 | github.com/stretchr/testify v1.8.1
22 | github.com/txthinking/brook v0.0.0-20230418095906-76ced63f1803
23 | github.com/txthinking/socks5 v0.0.0-20230307062227-0e1677eca4ba
24 | golang.org/x/net v0.8.0
25 | google.golang.org/protobuf v1.29.1
26 | gopkg.in/natefinch/lumberjack.v2 v2.0.0
27 | )
28 |
29 | require (
30 | github.com/BurntSushi/toml v1.2.1 // indirect
31 | github.com/andybalholm/brotli v1.0.4 // indirect
32 | github.com/bytedance/sonic v1.8.0 // indirect
33 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
34 | github.com/davecgh/go-spew v1.1.1 // indirect
35 | github.com/gaukas/godicttls v0.0.3 // indirect
36 | github.com/gin-contrib/sse v0.1.0 // indirect
37 | github.com/go-playground/locales v0.14.1 // indirect
38 | github.com/go-playground/universal-translator v0.18.1 // indirect
39 | github.com/go-playground/validator/v10 v10.11.2 // indirect
40 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
41 | github.com/goccy/go-json v0.10.0 // indirect
42 | github.com/golang/mock v1.6.0 // indirect
43 | github.com/golang/protobuf v1.5.3 // indirect
44 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
45 | github.com/gorilla/mux v1.8.0 // indirect
46 | github.com/gorilla/websocket v1.5.0 // indirect
47 | github.com/hashicorp/errwrap v1.1.0 // indirect
48 | github.com/hashicorp/go-multierror v1.1.1 // indirect
49 | github.com/itchyny/base58-go v0.2.1 // indirect
50 | github.com/jpillora/backoff v1.0.0 // indirect
51 | github.com/json-iterator/go v1.1.12 // indirect
52 | github.com/klauspost/compress v1.15.15 // indirect
53 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect
54 | github.com/kr/pretty v0.3.1 // indirect
55 | github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 // indirect
56 | github.com/leodido/go-urn v1.2.1 // indirect
57 | github.com/mattn/go-isatty v0.0.17 // indirect
58 | github.com/miekg/dns v1.1.51 // indirect
59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
60 | github.com/modern-go/reflect2 v1.0.2 // indirect
61 | github.com/nknorg/encrypted-stream v1.0.2-0.20230320101720-9891f770de86 // indirect
62 | github.com/onsi/ginkgo/v2 v2.2.0 // indirect
63 | github.com/oschwald/geoip2-golang v1.4.0 // indirect
64 | github.com/oschwald/maxminddb-golang v1.6.0 // indirect
65 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
66 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
67 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect
68 | github.com/phuslu/iploc v1.0.20230201 // indirect
69 | github.com/pkg/errors v0.9.1 // indirect
70 | github.com/pmezard/go-difflib v1.0.0 // indirect
71 | github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
72 | github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
73 | github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
74 | github.com/quic-go/quic-go v0.32.0 // indirect
75 | github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40 // indirect
76 | github.com/refraction-networking/utls v1.3.2 // indirect
77 | github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
78 | github.com/rogpeppe/go-internal v1.10.0 // indirect
79 | github.com/tdewolff/minify v2.3.6+incompatible // indirect
80 | github.com/tdewolff/parse v2.3.4+incompatible // indirect
81 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
82 | github.com/txthinking/crypto v0.0.0-20210716135230-de9624a415a4 // indirect
83 | github.com/txthinking/runnergroup v0.0.0-20230211072751-d11f16258c86 // indirect
84 | github.com/txthinking/x v0.0.0-20220929041811-1b4d914e9133 // indirect
85 | github.com/ugorji/go/codec v1.2.9 // indirect
86 | github.com/urfave/negroni v1.0.0 // indirect
87 | github.com/xtaci/smux v2.0.1+incompatible // indirect
88 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
89 | golang.org/x/crypto v0.7.0 // indirect
90 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
91 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
92 | golang.org/x/mod v0.8.0 // indirect
93 | golang.org/x/sys v0.6.0 // indirect
94 | golang.org/x/text v0.8.0 // indirect
95 | golang.org/x/tools v0.6.0 // indirect
96 | gopkg.in/yaml.v3 v3.0.1 // indirect
97 | )
98 |
--------------------------------------------------------------------------------
/ss/tcp.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "net"
8 | "sync"
9 | "time"
10 |
11 | "github.com/shadowsocks/go-shadowsocks2/socks"
12 | )
13 |
14 | // Create a SOCKS server listening on addr and proxy to server.
15 | func socksLocal(addr, server string, shadow func(net.Conn) net.Conn) error {
16 | logf("SOCKS proxy %s <-> %s", addr, server)
17 | return tcpLocal(addr, server, shadow, func(c net.Conn) (socks.Addr, error) { return socks.Handshake(c) })
18 | }
19 |
20 | // Create a TCP tunnel from addr to target via server.
21 | func tcpTun(addr, server, target string, shadow func(net.Conn) net.Conn) error {
22 | tgt := socks.ParseAddr(target)
23 | if tgt == nil {
24 | return fmt.Errorf("invalid target address %q", target)
25 | }
26 | logf("TCP tunnel %s <-> %s <-> %s", addr, server, target)
27 | return tcpLocal(addr, server, shadow, func(net.Conn) (socks.Addr, error) { return tgt, nil })
28 | }
29 |
30 | // Listen on addr and proxy to server to reach target from getAddr.
31 | func tcpLocal(addr, server string, shadow func(net.Conn) net.Conn, getAddr func(net.Conn) (socks.Addr, error)) error {
32 | l, err := net.Listen("tcp", addr)
33 | if err != nil {
34 | return fmt.Errorf("failed to listen on %s: %v", addr, err)
35 | }
36 |
37 | for {
38 | c, err := l.Accept()
39 | if err != nil {
40 | logf("failed to accept: %s", err)
41 | time.Sleep(time.Second)
42 | continue
43 | }
44 |
45 | go func() {
46 | defer c.Close()
47 | tgt, err := getAddr(c)
48 | if err != nil {
49 |
50 | // UDP: keep the connection until disconnect then free the UDP socket
51 | if err == socks.InfoUDPAssociate {
52 | logf("it is socks.InfoUDPAssociate, tgt is %s", tgt.String())
53 | buf := make([]byte, 1024)
54 | // block here
55 | for {
56 | _, err := c.Read(buf)
57 | if err, ok := err.(net.Error); ok && err.Timeout() {
58 | continue
59 | }
60 | if err != nil {
61 | // logf("UDP Associate End.")
62 | return
63 | }
64 | }
65 | }
66 |
67 | logf("failed to get target address: %v", err)
68 | return
69 | }
70 |
71 | server = getClient(tgt.String())
72 | rc, err := net.Dial("tcp", server)
73 | if err != nil {
74 | logf("failed to connect to server %v: %v", server, err)
75 | return
76 | }
77 |
78 | defer rc.Close()
79 | tc := rc.(*net.TCPConn)
80 | if config.TCPCork {
81 | timedCork(tc, 10*time.Millisecond)
82 | }
83 | rc = shadow(rc)
84 |
85 | if _, err = rc.Write(tgt); err != nil {
86 | logf("failed to send target address: %v", err)
87 | return
88 | }
89 |
90 | logf("proxy %s <-> %s <-> %s", c.RemoteAddr(), server, tgt)
91 | err = relay(rc, c)
92 | if err != nil {
93 | if err, ok := err.(net.Error); ok && err.Timeout() {
94 | return // ignore i/o timeout
95 | }
96 | logf("relay error: %v", err)
97 | }
98 | }()
99 | }
100 | }
101 |
102 | // Listen on addr for incoming connections.
103 | func tcpRemote(addr string, shadow func(net.Conn) net.Conn) error {
104 | l, err := net.Listen("tcp", addr)
105 | if err != nil {
106 | return fmt.Errorf("failed to listen on %s: %v", addr, err)
107 | }
108 |
109 | logf("listening TCP on %s", addr)
110 | for {
111 | c, err := l.Accept()
112 | if err != nil {
113 | logf("failed to accept: %v", err)
114 | time.Sleep(time.Second)
115 | continue
116 | }
117 |
118 | go func() {
119 | defer c.Close()
120 | sc := shadow(c)
121 |
122 | tgt, err := socks.ReadAddr(sc)
123 | if err != nil {
124 | logf("failed to get target address: %v", err)
125 | // drain c to avoid leaking server behavioral features
126 | // see https://www.ndss-symposium.org/ndss-paper/detecting-probe-resistant-proxies/
127 | _, err = io.Copy(ioutil.Discard, c)
128 | if err != nil {
129 | logf("discard error: %v", err)
130 | }
131 | return
132 | }
133 |
134 | rc, err := net.Dial("tcp", tgt.String())
135 | if err != nil {
136 | logf("failed to connect to target: %v", err)
137 | return
138 | }
139 | defer rc.Close()
140 |
141 | logf("proxy %s <-> %s", c.RemoteAddr(), tgt)
142 | err = relay(sc, rc)
143 | if err != nil {
144 | if err, ok := err.(net.Error); ok && err.Timeout() {
145 | return // ignore i/o timeout
146 | }
147 | logf("relay error: %v", err)
148 | }
149 | }()
150 | }
151 | }
152 |
153 | // relay copies between left and right bidirectionally. Returns any error occurred.
154 | func relay(left, right net.Conn) error {
155 | var err, err1 error
156 | var wg sync.WaitGroup
157 |
158 | wg.Add(1)
159 | go func() {
160 | defer wg.Done()
161 | _, err1 = io.Copy(right, left)
162 | right.SetReadDeadline(time.Now()) // unblock read on right
163 | }()
164 |
165 | _, err = io.Copy(left, right)
166 | left.SetReadDeadline(time.Now()) // unblock read on left
167 | wg.Wait()
168 |
169 | if err1 != nil {
170 | err = err1
171 | }
172 | return err
173 | }
174 |
--------------------------------------------------------------------------------
/web/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "English",
3 | "mobile tab": "Connect from mobile device",
4 | "desktop tab": "Connect from desktop",
5 | "data plan tab": "Purchase Data Plan",
6 | "unlimited": "Unlimited",
7 | "need help tab": "I Need Help",
8 | "advance tab": "Advanced",
9 | "download nConnect part1": "Download nConnect",
10 | "download nConnect part2": "and create account",
11 | "add device from mobile": "Use your nConnect mobile App to scan the QR code from step 1, to establish a secure tunnel between your mobile and your NAS.",
12 | "connect from mobile": "Enable the connection to this device you can use its local IP address to visit this device in any app as if its on the same local network.",
13 | "mobile guide": "Read this guide if you need more detailed instructions.",
14 | "add device in mobile first": "1. Add this device in nConnect if you haven't done it yet.",
15 | "add server from desktop": "2. Download nConnect client for desktop, open add server screen and you will see a QR code.",
16 | "scan QR code to add server to desktop": "3. Open nConnect and select this device, choose add to nConnect desktop and scan the QR code shown in previous step to add this device.",
17 | "connect from desktop": "4. Enable the connection to this device and you can use its local IP address to visit this device in any app as if it's on the same local network.",
18 | "desktop guide": "Read Guide if you need more detailed instructions",
19 | "purchase method": "There are two ways to purchase data plan, you can choose either one of them:",
20 | "purchase from mobile": "1. Open nConnect, select this device and purchase data plan.",
21 | "purchase from web": "2. Or you can purchase data plan from web payment portal.",
22 | "need help method": "If you need help or have any suggestions, you can reach us using one of the following ways:",
23 | "create forum post": "1. Create a post under the nConnect category in our forum.",
24 | "Q&A": "2. Q&A",
25 | "send email": "3. Send an email to {email}.",
26 | "mobile customer service": "4. Open nConnect, select this device and choose customer service.",
27 | "local IP address": "Local IP address",
28 | "access key": "Access key (valid for 5 minutes)",
29 | "accept addresses": "Accept addresses",
30 | "admins": "Admins",
31 | "save": "Save",
32 | "save success": "Save success!",
33 | "export account": "Export account",
34 | "import account": "Import account",
35 | "export tip": "You can find your account information in “Advanced”. Please make sure to export and save your account information both before and after your purchase. For user privacy, NKN does not keep any user information.",
36 | "exportConfirm": "Do you want to export account? The account should be kept as secret. Anyone who has it can consume your data or decrypt the data you send. Do not continue if someone else asks you to do it.",
37 | "exportSuccess": "Export success! The secret seed of your account is: {seed}",
38 | "importConfirm": "Do you want to import account? The current account on this device will be lost permanently if not exported and backed up. All clients need to add this device again after import is finished.",
39 | "importPromptCurrent": "Please enter the secret seed of your CURRENT account and make sure you have your current account exported and backed up:",
40 | "importWrongCurrent": "The secret seed of your CURRENT account is incorrect! Please make sure you have your current account exported and backed up.",
41 | "importPromptNew": "Please enter the secret seed of the account you want to import:",
42 | "importSuccess": "Import success! Please restart nConnect on this device to use the new account.",
43 | "estimatedRemainingData": "Estimated Remaining High Speed Data",
44 | "currentServerRegion": "Current Server Region",
45 | "customized": "Customized",
46 | "tunaConfigChoiceTitle": "Please select server region",
47 | "cancel": "Cancel",
48 | "setTunaConfigSuccess": "Change server region success! New connections made to this device will use new configurations.",
49 | "serverRegion": "Server Region",
50 | "global": "Global",
51 | "help": "Help",
52 | "china": "China",
53 | "asia": "Asia",
54 | "premium": "Global",
55 | "chinaHighSpeed": "China",
56 | "chinaPlatinum": "China",
57 | "download log": "Download log",
58 | "no log available": "No log available",
59 | "notEnabled": "Not Enabled",
60 | "getStartedLink": "https://forum.nkn.org/t/nconnect-user-manual-video-nconnect/2457",
61 | "nMobileProLink": "https://www.nkn.org/nMobile-pro/",
62 | "nConnectLink": "https://nconnect.nkn.org/",
63 | "nConnectClientDesktopLink": "https://forum.nkn.org/t/nconnect-pc-download-nconnectpc/2456",
64 | "paymentLink": "https://nconnect-payment.nkncdn.com/payment/?addr={addr}&lng={lng}{additionalParams}",
65 | "forumLink": "https://forum.nkn.org/c/applications/nconnect/52",
66 | "QALink": "https://forum.nkn.org/t/nconnect/3836",
67 | "emailAddress": "nconnect@nkn.org"
68 | }
69 |
--------------------------------------------------------------------------------
/network/cliservice.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net"
8 | "time"
9 | )
10 |
11 | const (
12 | Cli_RPC = "127.0.0.1:10032"
13 | )
14 |
15 | const (
16 | Cli_Status = iota // Get my network status
17 | Cli_List // List all nodes I can access, and all nodes I accept
18 | Cli_Join // Join a network
19 | Cli_Leave // Leave a network
20 | )
21 |
22 | type CliMsgReq struct {
23 | MsgType int `json:"msgType"`
24 | }
25 |
26 | type CliMsgResp struct {
27 | MsgType int `json:"msgType"`
28 | Err string `json:"err"`
29 | NetworkInfo *networkInfo `json:"networkInfo"`
30 | NodeInfo *NodeInfo `json:"nodeInfo"`
31 | NodeICanAcces []*NodeInfo `json:"nodeICanAccess"`
32 | NodeIAccept []*NodeInfo `json:"nodeIAccept"`
33 | }
34 |
35 | func (m *Member) StartCliService() error {
36 | a, err := net.ResolveUDPAddr("udp", Cli_RPC)
37 | if err != nil {
38 | return err
39 | }
40 | udpServer, err := net.ListenUDP("udp", a)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | defer udpServer.Close()
46 |
47 | b := make([]byte, 1024)
48 | var req CliMsgReq
49 | var resp CliMsgResp
50 | for {
51 | n, addr, err := udpServer.ReadFromUDP(b)
52 | if err != nil {
53 | log.Printf("StartCliService.ReadFromUDP err: %v", err)
54 | time.Sleep(time.Second)
55 | continue
56 | }
57 |
58 | err = json.Unmarshal(b[:n], &req)
59 | if err != nil {
60 | log.Printf("StartCliService.Unmarshal err: %v\n", err)
61 | time.Sleep(time.Second)
62 | continue
63 | }
64 |
65 | resp.MsgType = req.MsgType
66 | switch req.MsgType {
67 | case Cli_Status:
68 | resp.NetworkInfo = m.networkData.NetworkInfo
69 | resp.NodeInfo = m.networkData.NodeInfo
70 | case Cli_List:
71 | m.GetNodeIAccept()
72 | m.GetNodeICanAccess()
73 | resp.NetworkInfo = m.networkData.NetworkInfo
74 | resp.NodeIAccept = m.networkData.NodesIAccept
75 | resp.NodeICanAcces = m.networkData.NodesICanAccess
76 | case Cli_Join:
77 | m.JoinNetwork(m.serverAddress)
78 | resp.NetworkInfo = m.networkData.NetworkInfo
79 | resp.NodeInfo = m.networkData.NodeInfo
80 | case Cli_Leave:
81 | m.LeaveNetwork()
82 | resp.NetworkInfo = m.networkData.NetworkInfo
83 |
84 | default:
85 | resp.Err = "Unknown msgType"
86 | }
87 |
88 | buf, err := json.Marshal(resp)
89 | if err != nil {
90 | log.Printf("StartCliService.Marshal err: %v\n", err)
91 | time.Sleep(time.Second)
92 | continue
93 | }
94 |
95 | _, _, err = udpServer.WriteMsgUDP(buf, nil, addr)
96 | if err != nil {
97 | log.Printf("StartCliService.WriteMsgUDP err: %v\n", err)
98 | time.Sleep(time.Second)
99 | continue
100 | }
101 | }
102 | }
103 |
104 | func CliStatus() {
105 | resp, err := CliRequest(Cli_Status)
106 | if err != nil {
107 | fmt.Println("CliStatus err: ", err)
108 | return
109 | }
110 | fmt.Println("\nNetwork Domain: ", resp.NetworkInfo.Domain)
111 | if resp.NodeInfo != nil && resp.NodeInfo.IP != "" {
112 | fmt.Println("Ip:", resp.NodeInfo.IP, "\tMask:", resp.NodeInfo.Netmask, "\tNode Name:", resp.NodeInfo.Name)
113 | } else {
114 | fmt.Println("You don't join the network yet")
115 | }
116 | }
117 |
118 | func CliList() {
119 | resp, err := CliRequest(Cli_List)
120 | if err != nil {
121 | fmt.Println("CliList err: ", err)
122 | return
123 | }
124 | fmt.Println("\nNodes I accept:")
125 | for _, node := range resp.NodeIAccept {
126 | fmt.Println("IP:", node.IP, "\tMask:", node.Netmask, "\tNode Name:", node.Name)
127 | }
128 | fmt.Println("\nNodes I can access:")
129 | for _, node := range resp.NodeICanAcces {
130 | fmt.Println("IP:", node.IP, "\tMask:", node.Netmask, "\tNode Name:", node.Name)
131 | }
132 | }
133 |
134 | func CliJoin() {
135 | resp, err := CliRequest(Cli_Join)
136 | if err != nil {
137 | fmt.Println("CliJoin err: ", err)
138 | return
139 | }
140 | fmt.Println("\nNetwork Domain: ", resp.NetworkInfo.Domain)
141 | if resp.NodeInfo != nil && resp.NodeInfo.IP != "" {
142 | fmt.Println("You have joined the network")
143 | fmt.Println("Ip:", resp.NodeInfo.IP, "\tMask:", resp.NodeInfo.Netmask, "\tNode Name:", resp.NodeInfo.Name)
144 | } else {
145 | fmt.Println("Join network request is sent, please wait for the manager to authorize it")
146 | }
147 | }
148 |
149 | func CliLeave() {
150 | resp, err := CliRequest(Cli_Leave)
151 | if err != nil {
152 | fmt.Println("CliLeave err: ", err)
153 | return
154 | }
155 | if resp.NetworkInfo == nil {
156 | fmt.Println("You have left the network")
157 | } else {
158 | fmt.Println("Leave network failed, please try again")
159 | }
160 | }
161 |
162 | func CliRequest(msgType int) (resp CliMsgResp, err error) {
163 | req := CliMsgReq{
164 | MsgType: msgType,
165 | }
166 |
167 | uc, err := net.Dial("udp", Cli_RPC)
168 | if err != nil {
169 | log.Println("CliRequest net.Dial err: ", err)
170 | return
171 | }
172 | defer uc.Close()
173 |
174 | send, _ := json.Marshal(req)
175 | if _, err = uc.Write(send); err != nil {
176 | log.Println("CliRequest.Write err ", err)
177 | return
178 | }
179 |
180 | b := make([]byte, 65535)
181 | n, err := uc.Read(b)
182 | if err != nil {
183 | log.Println("CliRequest.Read err ", err)
184 | return
185 | }
186 |
187 | err = json.Unmarshal(b[:n], &resp)
188 | if err != nil {
189 | log.Printf("CliRequest.Unmarshal err: %v\n", err)
190 | return
191 | }
192 |
193 | return
194 | }
195 |
--------------------------------------------------------------------------------
/ss/udp.go:
--------------------------------------------------------------------------------
1 | package ss
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 |
8 | "sync"
9 |
10 | "github.com/shadowsocks/go-shadowsocks2/socks"
11 | )
12 |
13 | type mode int
14 |
15 | const (
16 | remoteServer mode = iota
17 | relayClient
18 | socksClient
19 | )
20 |
21 | const udpBufSize = 64 * 1024
22 |
23 | // Listen on laddr for UDP packets, encrypt and send to server to reach target.
24 | func udpLocal(laddr, server, target string, shadow func(net.PacketConn) net.PacketConn) error {
25 | server = getClient(target)
26 | if server == "" {
27 | return nil // fmt.Errorf("UDP target address error: invalid target address: %q", target)
28 | }
29 | srvAddr, err := net.ResolveUDPAddr("udp", server)
30 | if err != nil {
31 | return fmt.Errorf("UDP server address error: %v", err)
32 | }
33 |
34 | tgt := socks.ParseAddr(target)
35 | if tgt == nil {
36 | return fmt.Errorf("UDP target address error: invalid target address: %q", target)
37 | }
38 |
39 | c, err := net.ListenPacket("udp", laddr)
40 | if err != nil {
41 | return fmt.Errorf("UDP local listen error: %v", err)
42 | }
43 | defer c.Close()
44 |
45 | nm := newNATmap(config.UDPTimeout)
46 | buf := make([]byte, udpBufSize)
47 | copy(buf, tgt)
48 |
49 | logf("UDP tunnel %s <-> %s <-> %s", laddr, server, target)
50 | for {
51 | n, raddr, err := c.ReadFrom(buf[len(tgt):])
52 | if err != nil {
53 | logf("UDP local read error: %v", err)
54 | continue
55 | }
56 |
57 | pc := nm.Get(raddr.String())
58 | if pc == nil {
59 | pc, err = net.ListenPacket("udp", "")
60 | if err != nil {
61 | logf("UDP local listen error: %v", err)
62 | continue
63 | }
64 |
65 | pc = shadow(pc)
66 | nm.Add(raddr, c, pc, relayClient)
67 | }
68 |
69 | _, err = pc.WriteTo(buf[:len(tgt)+n], srvAddr)
70 | if err != nil {
71 | logf("UDP local write error: %v", err)
72 | continue
73 | }
74 | }
75 | }
76 |
77 | // Listen on laddr for Socks5 UDP packets, encrypt and send to server to reach target.
78 | func udpSocksLocal(laddr, server string, shadow func(net.PacketConn) net.PacketConn) error {
79 | c, err := net.ListenPacket("udp", laddr)
80 | if err != nil {
81 | return fmt.Errorf("UDP Socks local listen error: %v", err)
82 | }
83 | defer c.Close()
84 |
85 | nm := newNATmap(config.UDPTimeout)
86 | buf := make([]byte, udpBufSize)
87 |
88 | for {
89 | n, raddr, err := c.ReadFrom(buf)
90 | if err != nil {
91 | logf("UDP local read error: %v", err)
92 | continue
93 | }
94 |
95 | pc := nm.Get(raddr.String())
96 | if pc == nil {
97 | pc, err = net.ListenPacket("udp", "")
98 | if err != nil {
99 | logf("UDP local listen error: %v", err)
100 | continue
101 | }
102 | // logf("UDP socks tunnel %s <-> %s <-> %s", laddr, server, socks.Addr(buf[3:]))
103 | pc = shadow(pc)
104 | nm.Add(raddr, c, pc, socksClient)
105 | }
106 |
107 | dest := socks.Addr(buf[3:])
108 | server = getClient(dest.String())
109 | if server == "" {
110 | // logf("UDP target address error: invalid target address: %q", dest)
111 | continue
112 | }
113 | srvAddr, err := net.ResolveUDPAddr("udp", server)
114 | if err != nil {
115 | return fmt.Errorf("UDP server address error: %v", err)
116 | }
117 |
118 | _, err = pc.WriteTo(buf[3:n], srvAddr)
119 | if err != nil {
120 | logf("UDP local write error: %v", err)
121 | continue
122 | }
123 | }
124 | }
125 |
126 | // Listen on addr for encrypted packets and basically do UDP NAT.
127 | func udpRemote(addr string, shadow func(net.PacketConn) net.PacketConn) error {
128 | c, err := net.ListenPacket("udp", addr)
129 | if err != nil {
130 | return fmt.Errorf("UDP remote listen error: %v", err)
131 | }
132 | defer c.Close()
133 | c = shadow(c)
134 |
135 | nm := newNATmap(config.UDPTimeout)
136 | buf := make([]byte, udpBufSize)
137 |
138 | logf("listening UDP on %s", addr)
139 | for {
140 | n, raddr, err := c.ReadFrom(buf)
141 | if err != nil {
142 | logf("UDP remote read error: %v", err)
143 | continue
144 | }
145 |
146 | tgtAddr := socks.SplitAddr(buf[:n])
147 | if tgtAddr == nil {
148 | logf("failed to split target address from packet: %q", buf[:n])
149 | continue
150 | }
151 |
152 | tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String())
153 | if err != nil {
154 | logf("failed to resolve target UDP address: %v", err)
155 | continue
156 | }
157 |
158 | payload := buf[len(tgtAddr):n]
159 |
160 | pc := nm.Get(raddr.String())
161 | if pc == nil {
162 | pc, err = net.ListenPacket("udp", "")
163 | if err != nil {
164 | logf("UDP remote listen error: %v", err)
165 | continue
166 | }
167 |
168 | nm.Add(raddr, c, pc, remoteServer)
169 | }
170 |
171 | _, err = pc.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature
172 | if err != nil {
173 | logf("UDP remote write error: %v", err)
174 | continue
175 | }
176 | }
177 | }
178 |
179 | // Packet NAT table
180 | type natmap struct {
181 | sync.RWMutex
182 | m map[string]net.PacketConn
183 | timeout time.Duration
184 | }
185 |
186 | func newNATmap(timeout time.Duration) *natmap {
187 | m := &natmap{}
188 | m.m = make(map[string]net.PacketConn)
189 | m.timeout = timeout
190 | return m
191 | }
192 |
193 | func (m *natmap) Get(key string) net.PacketConn {
194 | m.RLock()
195 | defer m.RUnlock()
196 | return m.m[key]
197 | }
198 |
199 | func (m *natmap) Set(key string, pc net.PacketConn) {
200 | m.Lock()
201 | defer m.Unlock()
202 |
203 | m.m[key] = pc
204 | }
205 |
206 | func (m *natmap) Del(key string) net.PacketConn {
207 | m.Lock()
208 | defer m.Unlock()
209 |
210 | pc, ok := m.m[key]
211 | if ok {
212 | delete(m.m, key)
213 | return pc
214 | }
215 | return nil
216 | }
217 |
218 | func (m *natmap) Add(peer net.Addr, dst, src net.PacketConn, role mode) {
219 | m.Set(peer.String(), src)
220 |
221 | go func() {
222 | timedCopy(dst, peer, src, m.timeout, role)
223 | if pc := m.Del(peer.String()); pc != nil {
224 | pc.Close()
225 | }
226 | }()
227 | }
228 |
229 | // copy from src to dst at target with read timeout
230 | func timedCopy(dst net.PacketConn, target net.Addr, src net.PacketConn, timeout time.Duration, role mode) error {
231 | buf := make([]byte, udpBufSize)
232 |
233 | for {
234 | src.SetReadDeadline(time.Now().Add(timeout))
235 | n, raddr, err := src.ReadFrom(buf)
236 | if err != nil {
237 | return err
238 | }
239 |
240 | switch role {
241 | case remoteServer: // server -> client: add original packet source
242 | srcAddr := socks.ParseAddr(raddr.String())
243 | copy(buf[len(srcAddr):], buf[:n])
244 | copy(buf, srcAddr)
245 | _, err = dst.WriteTo(buf[:len(srcAddr)+n], target)
246 | case relayClient: // client -> user: strip original packet source
247 | srcAddr := socks.SplitAddr(buf[:n])
248 | _, err = dst.WriteTo(buf[len(srcAddr):n], target)
249 | case socksClient: // client -> socks5 program: just set RSV and FRAG = 0
250 | _, err = dst.WriteTo(append([]byte{0, 0, 0}, buf[:n]...), target)
251 | }
252 |
253 | if err != nil {
254 | return err
255 | }
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/web/src/nuxt.config.js:
--------------------------------------------------------------------------------
1 | import colors from 'vuetify/es5/util/colors'
2 |
3 | export default {
4 | // Target: https://go.nuxtjs.dev/config-target
5 | target: 'static',
6 |
7 | // Global page headers: https://go.nuxtjs.dev/config-head
8 | head: {
9 | titleTemplate: '%s - nconnect-web',
10 | title: 'nconnect-web',
11 | meta: [
12 | {charset: 'utf-8'},
13 | {name: 'viewport', content: 'width=device-width, initial-scale=1'},
14 | {hid: 'description', name: 'description', content: ''},
15 | {name: 'format-detection', content: 'telephone=no'}
16 | ],
17 | link: [
18 | {rel: 'icon', type: 'image/x-icon', href: '/favicon.ico'}
19 | ],
20 | script: [
21 |
22 | ]
23 | },
24 |
25 | // Global CSS: https://go.nuxtjs.dev/config-css
26 | css: [],
27 |
28 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
29 | plugins: [
30 | '~/plugins/i18n'
31 | ],
32 |
33 | // Auto import components: https://go.nuxtjs.dev/config-components
34 | components: true,
35 |
36 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
37 | buildModules: [
38 | // https://go.nuxtjs.dev/vuetify
39 | '@nuxtjs/vuetify',
40 | ],
41 |
42 | // Modules: https://go.nuxtjs.dev/config-modules
43 | modules: [
44 | // https://go.nuxtjs.dev/axios
45 | '@nuxtjs/axios',
46 | // https://go.nuxtjs.dev/pwa
47 | '@nuxtjs/pwa',
48 | ],
49 | i18n: {
50 | // Options
51 | // vue-i18n configuration
52 | vueI18n: {
53 | fallbackLocale: 'en',
54 | },
55 | parsePages: false,
56 | // If true, vue-i18n-loader is added to Nuxt's Webpack config
57 | vueI18nLoader: false,
58 |
59 | // List of locales supported by your app
60 | // This can either be an array of codes: ['en', 'fr', 'es']
61 | // Or an array of objects for more complex configurations:
62 | // [
63 | // { code: 'en', iso: 'en-US', file: 'en.js' },
64 | // { code: 'fr', iso: 'fr-FR', file: 'fr.js' },
65 | // { code: 'es', iso: 'es-ES', file: 'es.js' }
66 | // ]
67 | locales: [
68 | {code: 'en', name: 'English', iso: 'en-US', file: 'en.json'},
69 | {code: 'zh', name: '简体中文', iso: 'zh', file: 'zh-CN.json'},
70 | {code: 'zh-TW', name: '繁体中文', iso: 'zh-Hant', file: 'zh-TW.json'},
71 | ],
72 |
73 | // The app's default locale, URLs for this locale won't have a prefix if
74 | // strategy is prefix_except_default
75 | defaultLocale: 'en',
76 |
77 | // Separator used to generated routes name for each locale, you shouldn't
78 | // need to change this
79 | // routesNameSeparator: '___',
80 |
81 | // Suffix added to generated routes name for default locale if strategy is prefix_and_default,
82 | // you shouldn't need to change this
83 | // defaultLocaleRouteNameSuffix: 'default',
84 |
85 | // Routes generation strategy, can be set to one of the following:
86 | // - 'prefix_except_default': add locale prefix for every locale except default
87 | // - 'prefix': add locale prefix for every locale
88 | // - 'prefix_and_default': add locale prefix for every locale and default
89 | // strategy: 'prefix_except_default',
90 |
91 | // Wether or not the translations should be lazy-loaded, if this is enabled,
92 | // you MUST configure langDir option, and locales must be an array of objects,
93 | // each containing a file key
94 | lazy: true,
95 |
96 | // Directory that contains translations files when lazy-loading messages,
97 | // this CAN NOT be empty if lazy-loading is enabled
98 | langDir: 'locales/',
99 |
100 | // Set this to a path to which you want to redirect users accessing root URL (/)
101 | // rootRedirect: null,
102 |
103 | // Enable browser language detection to automatically redirect user
104 | // to their preferred language as they visit your app for the first time
105 | // Set to false to disable
106 | detectBrowserLanguage: {
107 | // If enabled, a cookie is set once a user has been redirected to his
108 | // preferred language to prevent subsequent redirections
109 | // Set to false to redirect every time
110 | useCookie: true,
111 | // Cookie name
112 | cookieKey: 'i18n_redirected',
113 | // Set to always redirect to value stored in the cookie, not just once
114 | alwaysRedirect: true,
115 | // If no locale for the browsers locale is a match, use this one as a fallback
116 | fallbackLocale: 'en'
117 | },
118 |
119 | // Set this to true if you're using different domains for each language
120 | // If enabled, no prefix is added to your routes and you MUST configure locales
121 | // as an array of objects, each containing a domain key
122 | // differentDomains: false,
123 |
124 | // If using different domains, set this to true to get hostname from X-Forwared-Host
125 | // HTTP header instead of window.location
126 | // forwardedHost: false,
127 |
128 | // If true, SEO metadata is generated for routes that have i18n enabled
129 | // Set to false to disable app-wide
130 | // seo: true,
131 |
132 | // Base URL to use as prefix for alternate URLs in hreflang tags
133 | // baseUrl: '',
134 |
135 | // By default a store module is registered and kept in sync with the
136 | // app's i18n current state
137 | // Set to false to disable
138 | // vuex: {
139 | // Module namespace
140 | // moduleName: 'i18n',
141 |
142 | // Mutations config
143 | // mutations: {
144 | // Mutation to commit to store current locale, set to false to disable
145 | // setLocale: 'I18N_SET_LOCALE',
146 |
147 | // Mutation to commit to store current message, set to false to disable
148 | // setMessages: 'I18N_SET_MESSAGES'
149 | // },
150 |
151 | // PreserveState from server
152 | // preserveState: false
153 | // },
154 |
155 | // By default, custom routes are extracted from page files using acorn parsing,
156 | // set this to false to disable this
157 | // parsePages: true,
158 |
159 | // If parsePages option is disabled, the module will look for custom routes in
160 | // the pages option, refer to the "Routing" section for usage
161 | // pages: {
162 | // 'inspire':{
163 | // en:'/locales/en/inspire.js',
164 | // zh:'/locales/zh/inspire'
165 | // }
166 | // },
167 |
168 | // By default, custom paths will be encoded using encodeURI method.
169 | // This does not work with regexp: "/foo/:slug-:id(\\d+)". If you want to use
170 | // regexp in the path, then set this option to false, and make sure you process
171 | // path encoding yourself.
172 | encodePaths: true,
173 |
174 | // Called right before app's locale changes
175 | // beforeLanguageSwitch: () => null,
176 |
177 | // Called after app's locale has changed
178 | // onLanguageSwitched: () => null
179 | },
180 |
181 | // Axios module configuration: https://go.nuxtjs.dev/config-axios
182 | axios: {
183 | // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
184 | baseURL: '/',
185 | },
186 |
187 | // PWA module configuration: https://go.nuxtjs.dev/pwa
188 | pwa: {
189 | workbox: false,
190 | manifest: {
191 | lang: 'en'
192 | }
193 | },
194 |
195 | // Vuetify module configuration: https://go.nuxtjs.dev/config-vuetify
196 | vuetify: {
197 | customVariables: ['~/assets/variables.scss'],
198 | theme: {
199 | dark: true,
200 | themes: {
201 | dark: {
202 | primary: colors.blue.darken2,
203 | accent: colors.grey.darken3,
204 | secondary: colors.amber.darken3,
205 | info: colors.teal.lighten1,
206 | warning: colors.amber.base,
207 | error: colors.deepOrange.accent4,
208 | success: colors.green.accent3
209 | }
210 | }
211 | }
212 | },
213 |
214 | router: {
215 | base: process.env.BASE_URL || '',
216 | extendRoutes (routes, resolve) {
217 | routes.push({
218 | name: 'index',
219 | path: '/index.html',
220 | component: resolve(__dirname, 'pages/index.vue'),
221 | });
222 | },
223 | },
224 |
225 | generate: {
226 | subFolders: false
227 | },
228 |
229 | // Build Configuration: https://go.nuxtjs.dev/config-build
230 | build: {}
231 | }
232 |
--------------------------------------------------------------------------------
/web/dist/_nuxt/4c0b218.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{328:function(t,r,e){var n=e(20);t.exports=function(t){return n(Map.prototype.entries,t)}},335:function(t,r,e){"use strict";e.d(r,"a",(function(){return c}));var n=e(149);var o=e(198),f=e(123);function c(t){return function(t){if(Array.isArray(t))return Object(n.a)(t)}(t)||Object(o.a)(t)||Object(f.a)(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}},341:function(t,r,e){"use strict";var n=e(4),o=e(90).findIndex,f=e(104),c="findIndex",v=!0;c in[]&&Array(1).findIndex((function(){v=!1})),n({target:"Array",proto:!0,forced:v},{findIndex:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),f(c)},353:function(t,r,e){"use strict";var n=e(4),o=e(418),f=e(42),c=e(49),v=e(72),d=e(128);n({target:"Array",proto:!0},{flat:function(){var t=arguments.length?arguments[0]:void 0,r=f(this),e=c(r),n=d(r,0);return n.length=o(n,r,r,e,0,void 0===t?1:v(t)),n}})},354:function(t,r,e){e(104)("flat")},357:function(t,r,e){"use strict";e(435)("Map",(function(t){return function(){return t(this,arguments.length?arguments[0]:void 0)}}),e(436))},358:function(t,r,e){"use strict";e(4)({target:"Map",proto:!0,real:!0,forced:e(47)},{deleteAll:e(437)})},359:function(t,r,e){"use strict";var n=e(4),o=e(47),f=e(12),c=e(78),v=e(328),d=e(229);n({target:"Map",proto:!0,real:!0,forced:o},{every:function(t){var map=f(this),r=v(map),e=c(t,arguments.length>1?arguments[1]:void 0);return!d(r,(function(t,r,n){if(!e(r,t,map))return n()}),{AS_ENTRIES:!0,IS_ITERATOR:!0,INTERRUPTED:!0}).stopped}})},360:function(t,r,e){"use strict";var n=e(47),o=e(4),f=e(46),c=e(78),v=e(20),d=e(71),l=e(12),h=e(127),E=e(328),I=e(229);o({target:"Map",proto:!0,real:!0,forced:n},{filter:function(t){var map=l(this),r=E(map),e=c(t,arguments.length>1?arguments[1]:void 0),n=new(h(map,f("Map"))),o=d(n.set);return I(r,(function(t,r){e(r,t,map)&&v(o,n,t,r)}),{AS_ENTRIES:!0,IS_ITERATOR:!0}),n}})},361:function(t,r,e){"use strict";var n=e(4),o=e(47),f=e(12),c=e(78),v=e(328),d=e(229);n({target:"Map",proto:!0,real:!0,forced:o},{find:function(t){var map=f(this),r=v(map),e=c(t,arguments.length>1?arguments[1]:void 0);return d(r,(function(t,r,n){if(e(r,t,map))return n(r)}),{AS_ENTRIES:!0,IS_ITERATOR:!0,INTERRUPTED:!0}).result}})},362:function(t,r,e){"use strict";var n=e(4),o=e(47),f=e(12),c=e(78),v=e(328),d=e(229);n({target:"Map",proto:!0,real:!0,forced:o},{findKey:function(t){var map=f(this),r=v(map),e=c(t,arguments.length>1?arguments[1]:void 0);return d(r,(function(t,r,n){if(e(r,t,map))return n(t)}),{AS_ENTRIES:!0,IS_ITERATOR:!0,INTERRUPTED:!0}).result}})},363:function(t,r,e){"use strict";var n=e(47),o=e(4),f=e(12),c=e(328),v=e(438),d=e(229);o({target:"Map",proto:!0,real:!0,forced:n},{includes:function(t){return d(c(f(this)),(function(r,e,n){if(v(e,t))return n()}),{AS_ENTRIES:!0,IS_ITERATOR:!0,INTERRUPTED:!0}).stopped}})},364:function(t,r,e){"use strict";var n=e(4),o=e(47),f=e(12),c=e(328),v=e(229);n({target:"Map",proto:!0,real:!0,forced:o},{keyOf:function(t){return v(c(f(this)),(function(r,e,n){if(e===t)return n(r)}),{AS_ENTRIES:!0,IS_ITERATOR:!0,INTERRUPTED:!0}).result}})},365:function(t,r,e){"use strict";var n=e(47),o=e(4),f=e(46),c=e(78),v=e(20),d=e(71),l=e(12),h=e(127),E=e(328),I=e(229);o({target:"Map",proto:!0,real:!0,forced:n},{mapKeys:function(t){var map=l(this),r=E(map),e=c(t,arguments.length>1?arguments[1]:void 0),n=new(h(map,f("Map"))),o=d(n.set);return I(r,(function(t,r){v(o,n,e(r,t,map),r)}),{AS_ENTRIES:!0,IS_ITERATOR:!0}),n}})},366:function(t,r,e){"use strict";var n=e(47),o=e(4),f=e(46),c=e(78),v=e(20),d=e(71),l=e(12),h=e(127),E=e(328),I=e(229);o({target:"Map",proto:!0,real:!0,forced:n},{mapValues:function(t){var map=l(this),r=E(map),e=c(t,arguments.length>1?arguments[1]:void 0),n=new(h(map,f("Map"))),o=d(n.set);return I(r,(function(t,r){v(o,n,t,e(r,t,map))}),{AS_ENTRIES:!0,IS_ITERATOR:!0}),n}})},367:function(t,r,e){"use strict";var n=e(4),o=e(47),f=e(71),c=e(12),v=e(229);n({target:"Map",proto:!0,real:!0,forced:o},{merge:function(t){for(var map=c(this),r=f(map.set),e=arguments.length,i=0;i1?arguments[1]:void 0);return d(r,(function(t,r,n){if(e(r,t,map))return n()}),{AS_ENTRIES:!0,IS_ITERATOR:!0,INTERRUPTED:!0}).stopped}})},370:function(t,r,e){"use strict";var n=e(47),o=e(4),f=e(1),c=e(20),v=e(12),d=e(71),l=f.TypeError;o({target:"Map",proto:!0,real:!0,forced:n},{update:function(t,r){var map=v(this),e=d(map.get),n=d(map.has),o=d(map.set),f=arguments.length;d(r);var h=c(n,map,t);if(!h&&f<3)throw l("Updating absent value");var E=h?c(e,map,t):d(f>2?arguments[2]:void 0)(t,map);return c(o,map,t,r(E,t,map)),map}})},376:function(t,r,e){"use strict";var n=e(4),o=e(231);n({target:"String",proto:!0,forced:e(232)("fixed")},{fixed:function(){return o(this,"tt","","")}})},379:function(t,r,e){"use strict";var n=e(4),o=e(231);n({target:"String",proto:!0,forced:e(232)("small")},{small:function(){return o(this,"small","","")}})},413:function(t,r,e){"use strict";var n=e(4),o=e(231);n({target:"String",proto:!0,forced:e(232)("link")},{link:function(t){return o(this,"a","href",t)}})},418:function(t,r,e){"use strict";var n=e(1),o=e(106),f=e(49),c=e(78),v=n.TypeError,d=function(t,r,source,e,n,l,h,E){for(var element,I,T=n,R=0,S=!!h&&c(h,E);R0&&o(element))I=f(element),T=d(t,r,element,I,T,l-1)-1;else{if(T>=9007199254740991)throw v("Exceed the acceptable array length");t[T]=element}T++}R++}return T};t.exports=d},435:function(t,r,e){"use strict";var n=e(4),o=e(1),f=e(3),c=e(105),v=e(35),d=e(234),l=e(229),h=e(160),E=e(9),I=e(17),T=e(5),R=e(161),S=e(89),x=e(165);t.exports=function(t,r,e){var y=-1!==t.indexOf("Map"),A=-1!==t.indexOf("Weak"),_=y?"set":"add",m=o[t],w=m&&m.prototype,M=m,N={},O=function(t){var r=f(w[t]);v(w,t,"add"==t?function(t){return r(this,0===t?0:t),this}:"delete"==t?function(t){return!(A&&!I(t))&&r(this,0===t?0:t)}:"get"==t?function(t){return A&&!I(t)?void 0:r(this,0===t?0:t)}:"has"==t?function(t){return!(A&&!I(t))&&r(this,0===t?0:t)}:function(t,e){return r(this,0===t?0:t,e),this})};if(c(t,!E(m)||!(A||w.forEach&&!T((function(){(new m).entries().next()})))))M=e.getConstructor(r,t,y,_),d.enable();else if(c(t,!0)){var k=new M,z=k[_](A?{}:-0,1)!=k,U=T((function(){k.has(1)})),D=R((function(t){new m(t)})),P=!A&&T((function(){for(var t=new m,r=5;r--;)t[_](r,r);return!t.has(-0)}));D||((M=r((function(t,r){h(t,w);var e=x(new m,t,M);return null!=r&&l(r,e[_],{that:e,AS_ENTRIES:y}),e}))).prototype=w,w.constructor=M),(U||P)&&(O("delete"),O("has"),y&&O("get")),(P||z)&&O(_),A&&w.clear&&delete w.clear}return N[t]=M,n({global:!0,forced:M!=m},N),S(M,t),A||e.setStrong(M,t,y),M}},436:function(t,r,e){"use strict";var n=e(31).f,o=e(73),f=e(163),c=e(78),v=e(160),d=e(229),l=e(162),h=e(164),E=e(25),I=e(234).fastKey,T=e(59),R=T.set,S=T.getterFor;t.exports={getConstructor:function(t,r,e,l){var h=t((function(t,n){v(t,T),R(t,{type:r,index:o(null),first:void 0,last:void 0,size:0}),E||(t.size=0),null!=n&&d(n,t[l],{that:t,AS_ENTRIES:e})})),T=h.prototype,x=S(r),y=function(t,r,e){var n,o,f=x(t),c=A(t,r);return c?c.value=e:(f.last=c={index:o=I(r,!0),key:r,value:e,previous:n=f.last,next:void 0,removed:!1},f.first||(f.first=c),n&&(n.next=c),E?f.size++:t.size++,"F"!==o&&(f.index[o]=c)),t},A=function(t,r){var e,n=x(t),o=I(r);if("F"!==o)return n.index[o];for(e=n.first;e;e=e.next)if(e.key==r)return e};return f(T,{clear:function(){for(var t=x(this),data=t.index,r=t.first;r;)r.removed=!0,r.previous&&(r.previous=r.previous.next=void 0),delete data[r.index],r=r.next;t.first=t.last=void 0,E?t.size=0:this.size=0},delete:function(t){var r=this,e=x(r),n=A(r,t);if(n){var o=n.next,f=n.previous;delete e.index[n.index],n.removed=!0,f&&(f.next=o),o&&(o.previous=f),e.first==n&&(e.first=o),e.last==n&&(e.last=f),E?e.size--:r.size--}return!!n},forEach:function(t){for(var r,e=x(this),n=c(t,arguments.length>1?arguments[1]:void 0);r=r?r.next:e.first;)for(n(r.value,r.key,this);r&&r.removed;)r=r.previous},has:function(t){return!!A(this,t)}}),f(T,e?{get:function(t){var r=A(this,t);return r&&r.value},set:function(t,r){return y(this,0===t?0:t,r)}}:{add:function(t){return y(this,t=0===t?0:t,t)}}),E&&n(T,"size",{get:function(){return x(this).size}}),h},setStrong:function(t,r,e){var n=r+" Iterator",o=S(r),f=S(n);l(t,r,(function(t,r){R(this,{type:n,target:t,state:o(t),kind:r,last:void 0})}),(function(){for(var t=f(this),r=t.kind,e=t.last;e&&e.removed;)e=e.previous;return t.target&&(t.last=e=e?e.next:t.state.first)?"keys"==r?{value:e.key,done:!1}:"values"==r?{value:e.value,done:!1}:{value:[e.key,e.value],done:!1}:(t.target=void 0,{value:void 0,done:!0})}),e?"entries":"values",!e,!0),h(r)}}},437:function(t,r,e){"use strict";var n=e(20),o=e(71),f=e(12);t.exports=function(){for(var t,r=f(this),e=o(r.delete),c=!0,v=0,d=arguments.length;v 0 {
119 | m.networkData.NodeInfo = notification.NodeInfo[0]
120 | if m.serverAddress != "" && m.networkData.NodeInfo.ServerAddress != m.serverAddress {
121 | err := m.SetServerTunnel(m.serverTunnel)
122 | if err != nil {
123 | return err
124 | }
125 | m.networkData.NodeInfo.ServerAddress = m.serverAddress
126 | }
127 | if err := m.saveMemberData(); err != nil {
128 | return err
129 | }
130 |
131 | log.Printf("\n\nCongratulations!!! Your nConnect network member is authorized, IP: %v, mask: %v\n\n",
132 | m.networkData.NodeInfo.IP, m.networkData.NodeInfo.Netmask)
133 |
134 | m.OpenTunAndSetIp()
135 | m.GetNodeICanAccess()
136 | }
137 |
138 | case NOTI_NEW_MEMBER: // new member is authorized and joined the network
139 | if len(notification.NodeInfo) > 0 {
140 | m.networkData.NodesIAccept = append(m.networkData.NodesIAccept, notification.NodeInfo...)
141 | if err := m.saveMemberData(); err != nil {
142 | return err
143 | }
144 | m.UpdMyAccept(notification.NodeInfo)
145 | }
146 |
147 | case NOTI_UPD_I_ACCEPT:
148 | m.GetNodeIAccept()
149 |
150 | case NOTI_MEMBER_ONLINE:
151 | m.GetNodeICanAccess()
152 | if m.CbNodeICanAccessUpdated != nil {
153 | m.CbNodeICanAccessUpdated(m.networkData.NodesICanAccess)
154 | }
155 | m.UpdMyAccept(notification.NodeInfo)
156 |
157 | case NOTI_UPD_I_CAN_ACCESS:
158 | m.GetNodeICanAccess()
159 | if m.CbNodeICanAccessUpdated != nil {
160 | m.CbNodeICanAccessUpdated(m.networkData.NodesICanAccess)
161 | }
162 |
163 | case NKN_PING:
164 | log.Println("Network member, received ping from manager, send pong back")
165 |
166 | default:
167 | return fmt.Errorf("nConnect member got unknown notification type: %v", notification.MsgType)
168 | }
169 |
170 | return nil
171 | }
172 |
173 | func (m *Member) JoinNetwork(serverAddr string) error {
174 | if serverAddr == "" {
175 | serverAddr = m.networkData.NodeInfo.ServerAddress
176 | } else {
177 | if m.networkData.NodeInfo.ServerAddress != serverAddr {
178 | m.networkData.NodeInfo.ServerAddress = serverAddr
179 | m.saveMemberData()
180 | }
181 | }
182 |
183 | msg := memberToManager{MsgType: JOIN_NETWORK, Name: m.opts.NodeName, ServerAddress: serverAddr}
184 | resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true)
185 | if err != nil {
186 | return err
187 | }
188 |
189 | if resp.Err == errWaitForAuth {
190 | m.networkData.NetworkInfo = resp.NetworkInfo
191 | m.saveMemberData()
192 |
193 | log.Println("You sent a join the network request to the manager, wait for the manager to authorize.")
194 |
195 | return nil
196 | } else if resp.Err == errNameExist {
197 | log.Println("You network node name is used by other node, please config another name")
198 | return errors.New(errNameExist)
199 | }
200 |
201 | if resp.Err != "" {
202 | return errors.New(resp.Err)
203 | }
204 |
205 | if len(resp.NodeInfo) > 0 {
206 | m.networkData.NodeInfo = resp.NodeInfo[0]
207 | m.networkData.NetworkInfo = resp.NetworkInfo
208 | m.saveMemberData()
209 | if m.networkData.NodeInfo.IP != "" {
210 | m.joinedNetwork = true
211 | m.OpenTunAndSetIp()
212 |
213 | log.Printf("\n\nCongratulations!!! Your nConnect network member IP is: %v, mask is: %v\n\n",
214 | m.networkData.NodeInfo.IP, m.networkData.NodeInfo.Netmask)
215 | }
216 | } else {
217 | log.Println("You sent a join the network request to the manager, wait for the manager to authorize.")
218 | }
219 |
220 | return nil
221 | }
222 |
223 | func (m *Member) LeaveNetwork() error {
224 | msg := memberToManager{MsgType: LEAVE_NETWORK, Name: m.opts.NodeName}
225 | resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true)
226 | if err != nil {
227 | return err
228 | }
229 | if resp.Err != "" {
230 | return errors.New(resp.Err)
231 | }
232 |
233 | m.networkData = memberNetworkData{}
234 | return m.saveMemberData()
235 | }
236 |
237 | func (m *Member) SetServerTunnel(t *tunnel.Tunnel) error {
238 | m.serverTunnel = t
239 | serverAddress := t.FromAddr()
240 | m.serverAddress = serverAddress
241 | err := m.GetNodeIAccept()
242 | if err != nil {
243 | return err
244 | }
245 | if m.networkData.NodeInfo != nil && serverAddress == m.networkData.NodeInfo.ServerAddress {
246 | return nil
247 | }
248 |
249 | msg := memberToManager{MsgType: UPDATE_SERVER_ADDRESS, ServerAddress: serverAddress}
250 | _, err = SendMsg(m.c, m.opts.ManagerAddress, &msg, false)
251 | if err != nil {
252 | return err
253 | }
254 |
255 | if m.networkData.NodeInfo == nil {
256 | return nil
257 | }
258 |
259 | m.networkData.NodeInfo.ServerAddress = serverAddress
260 | return m.saveMemberData()
261 | }
262 |
263 | func (m *Member) GetNodeIAccept() error {
264 | msg := memberToManager{MsgType: GET_NODES_I_ACCEPT}
265 | resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true)
266 | if err != nil {
267 | return err
268 | }
269 | if resp.Err != "" {
270 | return errors.New(resp.Err)
271 | }
272 |
273 | if len(resp.NodeInfo) == 0 {
274 | return nil
275 | }
276 |
277 | m.networkData.NodesIAccept = resp.NodeInfo
278 | if err = m.saveMemberData(); err != nil {
279 | return err
280 | }
281 |
282 | m.UpdMyAccept(m.networkData.NodesIAccept)
283 |
284 | return nil
285 | }
286 |
287 | func (m *Member) UpdMyAccept(nodes []*NodeInfo) {
288 | if m.opts.Verbose {
289 | log.Printf("Network member, nodes I accept: %+v\n", nodes)
290 | }
291 |
292 | var addrs []string
293 | for _, node := range nodes {
294 | arr := strings.Split(node.Address, ".")
295 | addrs = append(addrs, arr[len(arr)-1]+"$")
296 | }
297 |
298 | if len(addrs) > 0 {
299 | err := m.opts.Config.AddAcceptAddrs(addrs)
300 | if err != nil {
301 | log.Println("Network member, opts.Config.AddAcceptAddrs error: ", err)
302 | }
303 | if m.serverTunnel != nil {
304 | err = m.serverTunnel.SetAcceptAddrs(nkn.NewStringArray(m.opts.Config.GetAcceptAddrs()...))
305 | if err != nil {
306 | log.Println("Network member, serverTunnel.SetAcceptAddrs error: ", err)
307 | }
308 | }
309 | }
310 | }
311 |
312 | func (m *Member) GetNodeICanAccess() error {
313 | msg := memberToManager{MsgType: GET_NODES_I_CAN_ACCESS, Name: m.opts.NodeName}
314 | resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true)
315 | if err != nil {
316 | return err
317 | }
318 | if resp.Err != "" {
319 | return errors.New(resp.Err)
320 | }
321 |
322 | if len(resp.NodeInfo) > 0 {
323 | m.networkData.NodesICanAccess = resp.NodeInfo
324 | if err = m.saveMemberData(); err != nil {
325 | return err
326 | }
327 |
328 | if m.CbNodeICanAccessUpdated != nil {
329 | m.CbNodeICanAccessUpdated(m.networkData.NodesICanAccess)
330 | }
331 | }
332 |
333 | return nil
334 | }
335 |
336 | func (m *Member) loadMemberData() error {
337 | jsonFile, err := os.OpenFile(memberFile, os.O_CREATE|os.O_RDONLY, 0666)
338 | if err != nil {
339 | return err
340 | }
341 |
342 | defer jsonFile.Close()
343 |
344 | b, err := io.ReadAll(jsonFile)
345 | if err != nil {
346 | return err
347 | }
348 |
349 | data := memberNetworkData{NetworkInfo: &networkInfo{}, NodeInfo: &NodeInfo{}}
350 | if len(b) == 0 {
351 | return errors.New(errNoDataInFile)
352 | }
353 |
354 | if err = json.Unmarshal(b, &data); err != nil {
355 | return err
356 | }
357 | if data.NetworkInfo != nil {
358 | m.networkData.NetworkInfo = data.NetworkInfo
359 | }
360 | if data.NodeInfo != nil {
361 | m.networkData.NodeInfo = data.NodeInfo
362 | }
363 |
364 | return nil
365 | }
366 |
367 | func (m *Member) saveMemberData() error {
368 | b, err := json.MarshalIndent(m.networkData, "", " ")
369 | if err != nil {
370 | return err
371 | }
372 |
373 | return os.WriteFile(memberFile, b, os.ModePerm)
374 | }
375 |
376 | func (m *Member) SetRoutes() error {
377 | routes := make([]string, 0, len(m.networkData.NodesIAccept))
378 | for _, n := range m.networkData.NodesIAccept {
379 | routes = append(routes, fmt.Sprintf("%s/32", n.IP))
380 | }
381 |
382 | ipNets := make([]*net.IPNet, len(routes))
383 | if len(routes) > 0 {
384 | for i, cidr := range routes {
385 | _, cidr, err := net.ParseCIDR(cidr)
386 | if err != nil {
387 | return fmt.Errorf("parse CIDR %s error: %v", cidr, err)
388 | }
389 | ipNets[i] = cidr
390 | }
391 | }
392 | arch.SetVPNRoutes(m.opts.TunName, m.networkData.NetworkInfo.Gateway, ipNets)
393 |
394 | return nil
395 | }
396 |
397 | func (m *Member) DeleteRoutes() error {
398 | routes := make([]string, 0, len(m.networkData.NodesIAccept))
399 | for _, n := range m.networkData.NodesIAccept {
400 | routes = append(routes, fmt.Sprintf("%s/32", n.IP))
401 | }
402 |
403 | ipNets := make([]*net.IPNet, len(routes))
404 | if len(routes) > 0 {
405 | for i, cidr := range routes {
406 | _, cidr, err := net.ParseCIDR(cidr)
407 | if err != nil {
408 | return fmt.Errorf("parse CIDR %s error: %v", cidr, err)
409 | }
410 | ipNets[i] = cidr
411 | }
412 | }
413 |
414 | arch.RemoveVPNRoutes(m.opts.TunName, m.networkData.NetworkInfo.Gateway, ipNets)
415 |
416 | return nil
417 | }
418 |
419 | func (m *Member) GetNodeInfo() *NodeInfo {
420 | return m.networkData.NodeInfo
421 | }
422 |
423 | func (m *Member) GetNetworkInfo() *networkInfo {
424 | return m.networkData.NetworkInfo
425 | }
426 |
427 | func (m *Member) OpenTunAndSetIp() {
428 | m.openTunOnce.Do(func() {
429 | err := arch.OpenTun(m.opts.TunName, m.networkData.NodeInfo.IP, m.networkData.NetworkInfo.Gateway, m.networkData.NodeInfo.Netmask, m.opts.TunDNS[0], m.opts.LocalSocksAddr)
430 | if err != nil {
431 | log.Printf("OpenTun error: %v", err)
432 | } else {
433 | log.Println("Started tun2socks, interface:", m.opts.TunName, "address:", m.networkData.NodeInfo.IP)
434 | }
435 | })
436 | }
437 |
--------------------------------------------------------------------------------
/admin/common.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "net"
7 |
8 | "github.com/nknorg/nconnect/config"
9 | "github.com/nknorg/nconnect/util"
10 | "github.com/nknorg/nkn-sdk-go"
11 | ts "github.com/nknorg/nkn-tuna-session"
12 | tunnel "github.com/nknorg/nkn-tunnel"
13 | "github.com/nknorg/tuna/filter"
14 | "github.com/nknorg/tuna/geo"
15 | )
16 |
17 | type permission uint8
18 |
19 | const (
20 | rpcPermissionAcceptClient permission = 1 << iota
21 | rpcPermissionAdminClient
22 | rpcPermissionWeb
23 | )
24 |
25 | var (
26 | errUnknownMethod = errors.New("unknown method")
27 | errPermissionDenied = errors.New("permission denied")
28 | resultSuccess = "success"
29 | )
30 |
31 | var (
32 | rpcPermissions = map[string]permission{
33 | "getAdminToken": rpcPermissionAdminClient | rpcPermissionWeb,
34 | "getAddrs": rpcPermissionAdminClient | rpcPermissionWeb,
35 | "setAddrs": rpcPermissionAdminClient | rpcPermissionWeb,
36 | "addAddrs": rpcPermissionAdminClient | rpcPermissionWeb,
37 | "removeAddrs": rpcPermissionAdminClient | rpcPermissionWeb,
38 | "getLocalIP": rpcPermissionAcceptClient | rpcPermissionAdminClient | rpcPermissionWeb,
39 | "getInfo": rpcPermissionAcceptClient | rpcPermissionAdminClient | rpcPermissionWeb,
40 | "getBalance": rpcPermissionAcceptClient | rpcPermissionAdminClient | rpcPermissionWeb,
41 | "setAdminHttpApi": rpcPermissionAdminClient | rpcPermissionWeb,
42 | "getSeed": rpcPermissionAdminClient | rpcPermissionWeb,
43 | "setSeed": rpcPermissionAdminClient | rpcPermissionWeb,
44 | "setTunaConfig": rpcPermissionAdminClient | rpcPermissionWeb,
45 | "getLog": rpcPermissionAdminClient | rpcPermissionWeb,
46 | }
47 | )
48 |
49 | type RpcReq struct {
50 | ID string `json:"id"`
51 | JSONRPC string `json:"jsonrpc"`
52 | Method string `json:"method"`
53 | Params map[string]interface{} `json:"params"`
54 | Token string `json:"token"`
55 | }
56 |
57 | type RpcResp struct {
58 | Result interface{} `json:"result,omitempty"`
59 | Error string `json:"error,omitempty"`
60 | }
61 |
62 | type addrsJSON struct {
63 | AcceptAddrs []string `json:"acceptAddrs"`
64 | AdminAddrs []string `json:"adminAddrs"`
65 | }
66 |
67 | type adminTokenJSON struct {
68 | Addr string `json:"addr"`
69 | Token *Token `json:"token"`
70 | }
71 |
72 | type localIPJSON struct {
73 | Ipv4 []string `json:"ipv4"`
74 | }
75 |
76 | type GetInfoJSON struct {
77 | Addr string `json:"addr"`
78 | LocalIP *localIPJSON `json:"localIP"`
79 | AdminHTTPAPIDisabled bool `json:"adminHttpApiDisabled"`
80 | Version string `json:"version"`
81 | Tuna bool `json:"tuna"`
82 | TunaServiceName string `json:"tunaServiceName,omitempty"`
83 | TunaCountry []string `json:"tunaCountry,omitempty"`
84 | InPrice []string `json:"inPrice,omitempty"`
85 | OutPrice []string `json:"outPrice,omitempty"`
86 | Tags []string `json:"tags,omitempty"`
87 | }
88 |
89 | type setSeedJSON struct {
90 | Seed string `json:"seed"`
91 | }
92 |
93 | type adminHTTPAPIJSON struct {
94 | Disable bool `json:"disable"`
95 | }
96 |
97 | type tunaConfigJSON struct {
98 | ServiceName string `json:"serviceName"`
99 | Country []string `json:"country"`
100 | AllowNknAddr []string `json:"allowNknAddr"`
101 | DisallowNknAddr []string `json:"disallowNknAddr"`
102 | AllowIp []string `json:"allowIp"`
103 | DisallowIp []string `json:"disallowIp"`
104 | }
105 |
106 | type getLogJSON struct {
107 | MaxSize int `json:"maxSize"`
108 | }
109 |
110 | func handleRequest(req *RpcReq, persistConf, mergedConf *config.Config, tun *tunnel.Tunnel, rpcPerm permission) *RpcResp {
111 | resp := &RpcResp{}
112 |
113 | if rpcPermissions[req.Method]&rpcPerm == 0 {
114 | resp.Error = errPermissionDenied.Error()
115 | return resp
116 | }
117 |
118 | switch req.Method {
119 | case "getAdminToken":
120 | resp.Result = getAdminToken()
121 | case "getAddrs":
122 | resp.Result = getAddrs(persistConf)
123 | case "setAddrs":
124 | addrs := &addrsJSON{}
125 | err := util.JSONConvert(req.Params, addrs)
126 | if err != nil {
127 | resp.Error = err.Error()
128 | break
129 | }
130 | err = setAddrs(persistConf, addrs, tun)
131 | if err != nil {
132 | resp.Error = err.Error()
133 | break
134 | }
135 | resp.Result = getAddrs(persistConf)
136 | case "addAddrs":
137 | addrs := &addrsJSON{}
138 | err := util.JSONConvert(req.Params, addrs)
139 | if err != nil {
140 | resp.Error = err.Error()
141 | break
142 | }
143 | err = addAddrs(persistConf, addrs, tun)
144 | if err != nil {
145 | resp.Error = err.Error()
146 | break
147 | }
148 | resp.Result = getAddrs(persistConf)
149 | case "removeAddrs":
150 | addrs := &addrsJSON{}
151 | err := util.JSONConvert(req.Params, addrs)
152 | if err != nil {
153 | resp.Error = err.Error()
154 | break
155 | }
156 | err = removeAddrs(persistConf, addrs, tun)
157 | if err != nil {
158 | resp.Error = err.Error()
159 | break
160 | }
161 | resp.Result = getAddrs(persistConf)
162 | case "getLocalIP":
163 | localIP, err := getLocalIP()
164 | if err != nil {
165 | resp.Error = err.Error()
166 | break
167 | }
168 | resp.Result = localIP
169 | case "getInfo":
170 | info, err := getInfo(mergedConf, tun)
171 | if err != nil {
172 | resp.Error = err.Error()
173 | break
174 | }
175 | resp.Result = info
176 | case "getBalance":
177 | balance, err := getBalance(tun)
178 | if err != nil {
179 | resp.Error = err.Error()
180 | break
181 | }
182 | resp.Result = balance
183 | case "setAdminHttpApi":
184 | params := &adminHTTPAPIJSON{}
185 | err := util.JSONConvert(req.Params, params)
186 | if err != nil {
187 | resp.Error = err.Error()
188 | break
189 | }
190 | err = setAdminHTTPAPI(persistConf, mergedConf, params)
191 | if err != nil {
192 | resp.Error = err.Error()
193 | break
194 | }
195 | resp.Result = resultSuccess
196 | case "getSeed":
197 | resp.Result = mergedConf.Seed
198 | case "setSeed":
199 | params := &setSeedJSON{}
200 | err := util.JSONConvert(req.Params, params)
201 | if err != nil {
202 | resp.Error = err.Error()
203 | break
204 | }
205 | err = persistConf.SetSeed(params.Seed)
206 | if err != nil {
207 | resp.Error = err.Error()
208 | break
209 | }
210 | resp.Result = resultSuccess
211 | case "setTunaConfig":
212 | params := &tunaConfigJSON{}
213 | err := util.JSONConvert(req.Params, params)
214 | if err != nil {
215 | resp.Error = err.Error()
216 | break
217 | }
218 | err = setTunaConfig(tun, persistConf, mergedConf, params)
219 | if err != nil {
220 | resp.Error = err.Error()
221 | break
222 | }
223 | resp.Result = resultSuccess
224 | case "getLog":
225 | params := &getLogJSON{}
226 | err := util.JSONConvert(req.Params, params)
227 | if err != nil {
228 | resp.Error = err.Error()
229 | break
230 | }
231 | logContent, err := getLog(mergedConf, params)
232 | if err != nil {
233 | resp.Error = err.Error()
234 | break
235 | }
236 | resp.Result = logContent
237 | default:
238 | resp.Error = errUnknownMethod.Error()
239 | }
240 | return resp
241 | }
242 |
243 | func getAdminToken() *adminTokenJSON {
244 | if len(serverAdminAddr) == 0 {
245 | return nil
246 | }
247 | return &adminTokenJSON{
248 | Addr: serverAdminAddr,
249 | Token: tokenStore.GetCurrentToken(),
250 | }
251 | }
252 |
253 | func getAddrs(conf *config.Config) *addrsJSON {
254 | return &addrsJSON{
255 | AcceptAddrs: conf.GetAcceptAddrs(),
256 | AdminAddrs: conf.GetAdminAddrs(),
257 | }
258 | }
259 |
260 | func setAddrs(conf *config.Config, addrs *addrsJSON, tun *tunnel.Tunnel) error {
261 | if addrs.AcceptAddrs != nil {
262 | conf.SetAcceptAddrs(addrs.AcceptAddrs)
263 | }
264 | if addrs.AdminAddrs != nil {
265 | conf.SetAdminAddrs(addrs.AdminAddrs)
266 | }
267 | return tun.SetAcceptAddrs(nkn.NewStringArray(conf.GetAcceptAddrs()...))
268 | }
269 |
270 | func addAddrs(conf *config.Config, addrs *addrsJSON, tun *tunnel.Tunnel) error {
271 | if addrs.AcceptAddrs != nil {
272 | conf.AddAcceptAddrs(addrs.AcceptAddrs)
273 | }
274 | if addrs.AdminAddrs != nil {
275 | conf.AddAdminAddrs(addrs.AdminAddrs)
276 | }
277 | return tun.SetAcceptAddrs(nkn.NewStringArray(conf.GetAcceptAddrs()...))
278 | }
279 |
280 | func removeAddrs(conf *config.Config, addrs *addrsJSON, tun *tunnel.Tunnel) error {
281 | if addrs.AcceptAddrs != nil {
282 | conf.RemoveAcceptAddrs(addrs.AcceptAddrs)
283 | }
284 | if addrs.AdminAddrs != nil {
285 | conf.RemoveAdminAddrs(addrs.AdminAddrs)
286 | }
287 | return tun.SetAcceptAddrs(nkn.NewStringArray(conf.GetAcceptAddrs()...))
288 | }
289 |
290 | func getLocalIP() (*localIPJSON, error) {
291 | ifaces, err := net.Interfaces()
292 | if err != nil {
293 | return nil, err
294 | }
295 | ipv4 := make([]string, 0, len(ifaces))
296 | for _, iface := range ifaces {
297 | if iface.Flags&net.FlagUp == 0 {
298 | continue
299 | }
300 | if iface.Flags&net.FlagLoopback != 0 {
301 | continue
302 | }
303 | addrs, err := iface.Addrs()
304 | if err != nil {
305 | return nil, err
306 | }
307 | for _, addr := range addrs {
308 | var ip net.IP
309 | switch v := addr.(type) {
310 | case *net.IPNet:
311 | ip = v.IP
312 | case *net.IPAddr:
313 | ip = v.IP
314 | }
315 | if ip == nil || ip.IsLoopback() {
316 | continue
317 | }
318 | ip = ip.To4()
319 | if ip == nil {
320 | continue
321 | }
322 | ipv4 = append(ipv4, ip.String())
323 | }
324 | }
325 | return &localIPJSON{Ipv4: ipv4}, nil
326 | }
327 |
328 | func getInfo(conf *config.Config, tun *tunnel.Tunnel) (*GetInfoJSON, error) {
329 | localIP, err := getLocalIP()
330 | if err != nil {
331 | return nil, err
332 | }
333 | info := &GetInfoJSON{
334 | Addr: tun.FromAddr(),
335 | LocalIP: localIP,
336 | AdminHTTPAPIDisabled: conf.DisableAdminHTTPAPI,
337 | Tuna: conf.Tuna,
338 | TunaServiceName: conf.TunaServiceName,
339 | TunaCountry: conf.TunaCountry,
340 | Version: config.Version,
341 | }
342 | tunaPubAddrs := tun.TunaPubAddrs()
343 | if tunaPubAddrs != nil {
344 | info.InPrice = make([]string, 0, len(tunaPubAddrs.Addrs))
345 | info.OutPrice = make([]string, 0, len(tunaPubAddrs.Addrs))
346 | for _, addr := range tunaPubAddrs.Addrs {
347 | if len(addr.IP) > 0 {
348 | info.InPrice = append(info.InPrice, addr.InPrice)
349 | info.OutPrice = append(info.OutPrice, addr.OutPrice)
350 | }
351 | }
352 | }
353 | if len(conf.Tags) > 0 {
354 | info.Tags = conf.Tags
355 | }
356 | return info, nil
357 | }
358 |
359 | func getBalance(tun *tunnel.Tunnel) (string, error) {
360 | balance, err := tun.MultiClient().Balance()
361 | if err != nil {
362 | return "", err
363 | }
364 | return balance.String(), nil
365 | }
366 |
367 | func setAdminHTTPAPI(persistConf, mergedConf *config.Config, params *adminHTTPAPIJSON) error {
368 | err := persistConf.SetAdminHTTPAPI(params.Disable)
369 | if err != nil {
370 | return err
371 | }
372 | return mergedConf.SetAdminHTTPAPI(params.Disable)
373 | }
374 |
375 | func setTunaConfig(tun *tunnel.Tunnel, persistConf, mergedConf *config.Config, params *tunaConfigJSON) error {
376 | err := persistConf.SetTunaConfig(params.ServiceName, params.Country, params.AllowNknAddr, params.DisallowNknAddr, params.AllowIp, params.DisallowIp)
377 | if err != nil {
378 | return err
379 | }
380 | err = mergedConf.SetTunaConfig(params.ServiceName, params.Country, params.AllowNknAddr, params.DisallowNknAddr, params.AllowIp, params.DisallowIp)
381 | if err != nil {
382 | return err
383 | }
384 | tsClient := tun.TunaSessionClient()
385 | if tsClient != nil {
386 | locations := make([]geo.Location, len(params.Country))
387 | for i := range params.Country {
388 | locations[i].CountryCode = params.Country[i]
389 | }
390 | allowIps := make([]geo.Location, len(params.AllowIp))
391 | for i := range params.AllowIp {
392 | allowIps[i].IP = params.AllowIp[i]
393 | }
394 | var allowed = append(locations, allowIps...)
395 |
396 | disallowed := make([]geo.Location, len(params.DisallowIp))
397 | for i := range params.DisallowIp {
398 | disallowed[i].IP = params.DisallowIp[i]
399 | }
400 |
401 | allowNknAddrs := make([]filter.NknClient, len(params.AllowNknAddr))
402 | for i := range params.AllowNknAddr {
403 | allowNknAddrs[i].Address = params.AllowNknAddr[i]
404 | }
405 |
406 | disallowNknAddrs := make([]filter.NknClient, len(params.DisallowNknAddr))
407 | for i := range params.DisallowNknAddr {
408 | disallowNknAddrs[i].Address = params.DisallowNknAddr[i]
409 | }
410 |
411 | err = tsClient.SetConfig(&ts.Config{
412 | TunaIPFilter: &geo.IPFilter{Allow: allowed, Disallow: disallowed},
413 | TunaNknFilter: &filter.NknFilter{Allow: allowNknAddrs, Disallow: disallowNknAddrs},
414 | TunaServiceName: params.ServiceName,
415 | })
416 | if err != nil {
417 | return err
418 | }
419 | go tsClient.RotateAll()
420 | }
421 | return nil
422 | }
423 |
424 | func getLog(conf *config.Config, params *getLogJSON) (string, error) {
425 | if len(conf.LogFileName) == 0 {
426 | return "", nil
427 | }
428 | b, err := ioutil.ReadFile(conf.LogFileName)
429 | if err != nil {
430 | return "", err
431 | }
432 | if conf.LogAPIResponseSize > 0 && len(b) > conf.LogAPIResponseSize {
433 | b = b[len(b)-conf.LogAPIResponseSize:]
434 | }
435 | if params.MaxSize > 0 && len(b) > params.MaxSize {
436 | b = b[len(b)-params.MaxSize:]
437 | }
438 | return string(b), nil
439 | }
440 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "crypto/ed25519"
5 | "encoding/hex"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "math/rand"
10 | "os"
11 | "runtime"
12 | "sync"
13 | "time"
14 |
15 | "github.com/nknorg/nconnect/util"
16 | "github.com/nknorg/nkn/v2/common"
17 | )
18 |
19 | const (
20 | RandomIdentifierChars = "abcdefghijklmnopqrstuvwxyz0123456789"
21 | RandomIdentifierLength = 6
22 | DefaultTunNameLinux = "nConnect-tun0"
23 | DefaultTunNameNonLinux = "nConnect-tap0"
24 | FallbackTunaMaxPrice = "0.01"
25 | DefaultUDPTimeout = time.Hour * 720
26 | )
27 |
28 | var (
29 | Version string
30 | )
31 |
32 | func init() {
33 | rand.NewSource(time.Now().UnixNano())
34 | }
35 |
36 | type Opts struct {
37 | Client bool `short:"c" long:"client" description:"Client mode"`
38 | Server bool `short:"s" long:"server" description:"Server mode"`
39 | NetworkManager bool `short:"m" long:"network-manager" description:"Network manager mode"`
40 | NetworkMember bool `short:"n" long:"network-member" description:"Join nConnect network as a member node"`
41 |
42 | Config
43 | ConfigFile string `short:"f" long:"config-file" default:"config.json" description:"Config file path"`
44 |
45 | Address bool `long:"address" description:"Print client address (client mode) or admin address (server mode)"`
46 | WalletAddress bool `long:"wallet-address" description:"Print wallet address (server only)"`
47 | Version bool `long:"version" description:"Print version"`
48 | Info string `short:"i" long:"info" description:"nConnect information"`
49 | }
50 |
51 | type Config struct {
52 | path string
53 |
54 | // Account config
55 | Identifier string `json:"identifier" long:"identifier" description:"NKN client identifier. A random one will be generated and saved to config.json if not provided."`
56 | Seed string `json:"seed" long:"seed" description:"NKN client secret seed. A random one will be generated and saved to config.json if not provided."`
57 |
58 | // NKN Client config
59 | SeedRPCServerAddr []string `json:"seedRPCServerAddr,omitempty" long:"rpc" description:"Seed RPC server address"`
60 | ConnectRetries int32 `json:"connectRetries,omitempty" long:"connect-retries" description:"client connect retries, a negative value means unlimited retries."`
61 |
62 | // Cipher config
63 | Cipher string `json:"cipher,omitempty" long:"cipher" description:"Socks proxy cipher. Dummy (no cipher) will not reduce security because NKN tunnel already has end to end encryption." choice:"dummy" choice:"chacha20-ietf-poly1305" choice:"aes-128-gcm" choice:"aes-256-gcm" default:"chacha20-ietf-poly1305"`
64 | Password string `json:"password,omitempty" long:"password" description:"Socks proxy password"`
65 |
66 | // Session config
67 | DialTimeout int32 `json:"dialTimeout,omitempty" long:"dial-timeout" description:"dial timeout in milliseconds"`
68 | SessionWindowSize int32 `json:"sessionWindowSize,omitempty" long:"session-window-size" description:"tuna session window size (byte)."`
69 |
70 | // Log config
71 | LogFileName string `json:"log,omitempty" long:"log" description:"Log file path. Will write log to stdout if not provided."`
72 | LogMaxSize int `json:"logMaxSize,omitempty" long:"log-max-size" description:"Maximum size in megabytes of the log file before it gets rotated." default:"1"`
73 | LogMaxBackups int `json:"logMaxBackups,omitempty" long:"log-max-backups" description:"Maximum number of old log files to retain." default:"3"`
74 | LogAPIResponseSize int `json:"logAPIResponseSize,omitempty" long:"log-api-response-size" description:"(server only) Maximum size in bytes of get log api response. If log size is greater than this value, only the lastest part of the log will be returned."`
75 |
76 | // Remote address
77 | RemoteAdminAddr []string `json:"remoteAdminAddr,omitempty" short:"a" long:"remote-admin-addr" description:"(client only) Remote server admin address"`
78 | RemoteTunnelAddr []string `json:"remoteTunnelAddr,omitempty" short:"r" long:"remote-tunnel-addr" description:"(client only) Remote server tunnel address, not needed if remote server admin address is given"`
79 |
80 | // Socks proxy config
81 | LocalSocksAddr string `json:"localSocksAddr,omitempty" short:"l" long:"local-socks-addr" description:"(client only) Local socks proxy listen address" default:"127.0.0.1:1080"`
82 |
83 | // TUN/TAP device config
84 | Tun bool `json:"tun,omitempty" long:"tun" description:"(client only) Enable TUN device, might require root privilege"`
85 | TunAddr string `json:"tunAddr,omitempty" long:"tun-addr" description:"(client only) TUN device IP address" default:"10.0.86.2"`
86 | TunGateway string `json:"tunGateway,omitempty" long:"tun-gateway" description:"(client only) TUN device gateway" default:"10.0.86.1"`
87 | TunMask string `json:"tunMask,omitempty" long:"tun-mask" description:"(client only) TUN device network mask, should be a prefixlen (a number) for IPv6 address" default:"255.255.255.0"`
88 | TunDNS []string `json:"tunDNS,omitempty" long:"tun-dns" description:"(client only) DNS resolvers for the TUN device (Windows only)" default:"1.1.1.1" default:"8.8.8.8"`
89 | TunName string `json:"tunName,omitempty" long:"tun-name" description:"(client only) TUN device name, will be ignored on MacOS. Default is nConnect-tun0 on Linux and nConnect-tap0 on Windows."`
90 |
91 | // VPN mode config
92 | VPN bool `json:"vpn,omitempty" long:"vpn" description:"(client only) Enable VPN mode, might require root privilege. TUN device will be enabled when VPN mode is enabled."`
93 | VPNRoute []string `json:"vpnRoute,omitempty" long:"vpn-route" description:"(client only) VPN routing table destinations, each item should be a valid CIDR. If not given, remote server's local IP addresses will be used."`
94 |
95 | // Tuna config
96 | Tuna bool `json:"tuna,omitempty" short:"t" long:"tuna" description:"Enable tuna sessions"`
97 | TunaMinBalance string `json:"tunaMinBalance,omitempty" long:"tuna-min-balance" description:"(server only) Minimal balance to enable tuna sessions" default:"0.01"`
98 | TunaMaxPrice string `json:"tunaMaxPrice,omitempty" long:"tuna-max-price" description:"(server only) Tuna max price in unit of NKN/MB. Can also be a url where the price will be get dynamically at launch." default:"0.01"`
99 | TunaMinFee string `json:"tunaMinFee,omitempty" long:"tuna-min-fee" description:"(server only) Tuna nanopay minimal txn fee" default:"0.00001"`
100 | TunaFeeRatio float64 `json:"tunaFeeRatio,omitempty" long:"tuna-fee-ratio" description:"(server only) Tuna nanopay txn fee ratio" default:"0.1"`
101 | TunaCountry []string `json:"tunaCountry,omitempty" long:"tuna-country" description:"(server only) Tuna service node allowed country code, e.g. US. All countries will be allowed if not provided"`
102 | TunaServiceName string `json:"tunaServiceName,omitempty" long:"tuna-service-name" description:"(server only) Tuna reverse service name"`
103 | TunaAllowNknAddr []string `json:"tunaAllowNknAddr,omitempty" long:"tuna-allow-nkn-addr" description:"(server only) Tuna service node allowed NKN address. All NKN address will be allowed if not provided"`
104 | TunaDisallowNknAddr []string `json:"tunaDisallowNknAddr,omitempty" long:"tuna-disallow-nkn-addr" description:"(server only) Tuna service node disallowed NKN address. All NKN address will be allowed if not provided"`
105 | TunaAllowIp []string `json:"tunaAllowIp,omitempty" long:"tuna-allow-ip" description:"(server only) Tuna service node allowed IP. All IP will be allowed if not provided"`
106 | TunaDisallowIp []string `json:"tunaDisallowIp,omitempty" long:"tuna-disallow-ip" description:"(server only) Tuna service node disallowed IP. All IP will be allowed if not provided"`
107 | TunaDisableDownloadGeoDB bool `json:"tunaDisableDownloadGeoDB,omitempty" long:"tuna-disable-download-geo-db" description:"(server only) Disable Tuna download geo db to disk"`
108 | TunaGeoDBPath string `json:"tunaGeoDBPath,omitempty" long:"tuna-geo-db-path" description:"(server only) Path to store Tuna geo db" default:"."`
109 | TunaDisableMeasureBandwidth bool `json:"tunaDisableMeasureBandwidth,omitempty" long:"tuna-disable-measure-bandwidth" description:"(server only) Disable Tuna measure bandwidth when selecting service nodes"`
110 | TunaMeasureStoragePath string `json:"tunaMeasureStoragePath,omitempty" long:"tuna-measure-storage-path" description:"(server only) Path to store Tuna measurement results" default:"."`
111 | TunaMeasureBandwidthBytes int32 `json:"tunaMeasureBandwidthBytes,omitempty" long:"tuna-measure-bandwidth-bytes" description:"(server only) Tuna measure bandwidth bytes to transmit when selecting service nodes" default:"1"`
112 |
113 | // UDP config
114 | UDP bool `json:"udp,omitempty" long:"udp" description:"Support udp proxy"`
115 | UDPIdleTime int32 `json:"udpIdleTime,omitempty" long:"udp-idle-time" description:"UDP connections will be purged after idle time (in seconds). 0 is for no purge" default:"0"`
116 |
117 | // Admin config
118 | AdminIdentifier string `json:"adminIdentifier,omitempty" long:"admin-identifier" description:"(server only) Admin NKN client identifier prefix" default:"nConnect"`
119 | AdminHTTPAddr string `json:"adminHttpAddr,omitempty" long:"admin-http" description:"(server only) Admin web GUI listen address (e.g. 127.0.0.1:8000)"`
120 | DisableAdminHTTPAPI bool `json:"disableAdminHttpApi,omitempty" long:"disable-admin-http-api" description:"(server only) Disable admin http api so admin web GUI only show static assets"`
121 | WebRootPath string `json:"webRootPath,omitempty" long:"web-root-path" description:"(server only) Web root path" default:"web/dist"`
122 |
123 | Tags []string `json:"tags,omitempty" long:"tags" description:"(server only) Tags that will be included in get info api"`
124 | Verbose bool `json:"verbose,omitempty" short:"v" long:"verbose" description:"Verbose mode, show logs on dialing/accepting connections"`
125 |
126 | lock sync.RWMutex
127 | AcceptAddrs []string `json:"acceptAddrs"`
128 | AdminAddrs []string `json:"adminAddrs"`
129 |
130 | // nconnect network
131 | NodeName string `json:"nodeName,omitempty" long:"node-name" description:"(network member only) Node name that will be used as to join a network"`
132 | ManagerAddress string `json:"managerAddress,omitempty" long:"manager-address" description:"(network member only) Manager address to connect to when joining a network"`
133 | }
134 |
135 | func NewConfig() *Config {
136 | return &Config{
137 | AcceptAddrs: make([]string, 0),
138 | AdminAddrs: make([]string, 0),
139 | }
140 | }
141 |
142 | func LoadOrNewConfig(path string) (*Config, error) {
143 | b, err := os.ReadFile(path)
144 | if err != nil {
145 | if os.IsNotExist(err) {
146 | c := NewConfig()
147 | c.path = path
148 | err := c.save()
149 | if err != nil {
150 | return nil, err
151 | }
152 | return c, nil
153 | }
154 | return nil, err
155 | }
156 |
157 | c := &Config{
158 | path: path,
159 | }
160 |
161 | err = json.Unmarshal(b, c)
162 | if err != nil {
163 | return nil, err
164 | }
165 |
166 | return c, nil
167 | }
168 |
169 | func (c *Config) SetPlatformSpecificDefaultValues() error {
170 | if len(c.TunName) == 0 {
171 | switch runtime.GOOS {
172 | case "linux":
173 | c.TunName = DefaultTunNameLinux
174 | default:
175 | c.TunName = DefaultTunNameNonLinux
176 | }
177 | }
178 | return nil
179 | }
180 |
181 | func (c *Config) VerifyClient() error {
182 | if len(c.RemoteAdminAddr) == 0 && len(c.RemoteTunnelAddr) == 0 {
183 | return errors.New("remoteAdminAddr and remoteTunnelAddr are both empty")
184 | }
185 | return nil
186 | }
187 |
188 | func (c *Config) VerifyServer() error {
189 | _, err := common.StringToFixed64(c.TunaMinBalance)
190 | if err != nil {
191 | return fmt.Errorf("parse TunaMinBalance error: %v", err)
192 | }
193 | _, err = common.StringToFixed64(c.TunaMaxPrice)
194 | if err != nil {
195 | return fmt.Errorf("parse TunaMaxPrice error: %v", err)
196 | }
197 | _, err = common.StringToFixed64(c.TunaMinFee)
198 | if err != nil {
199 | return fmt.Errorf("parse TunaMinFee error: %v", err)
200 | }
201 | return nil
202 | }
203 |
204 | func (c *Config) GetAcceptAddrs() []string {
205 | c.lock.RLock()
206 | defer c.lock.RUnlock()
207 | return c.AcceptAddrs
208 | }
209 |
210 | func (c *Config) SetAcceptAddrs(acceptAddrs []string) error {
211 | c.lock.Lock()
212 | defer c.lock.Unlock()
213 | c.AcceptAddrs = acceptAddrs
214 | return c.save()
215 | }
216 |
217 | func (c *Config) AddAcceptAddrs(acceptAddrs []string) error {
218 | c.lock.Lock()
219 | defer c.lock.Unlock()
220 | c.AcceptAddrs = util.MergeStrings(c.AcceptAddrs, acceptAddrs)
221 | return c.save()
222 | }
223 |
224 | func (c *Config) RemoveAcceptAddrs(acceptAddrs []string) error {
225 | c.lock.Lock()
226 | defer c.lock.Unlock()
227 | c.AcceptAddrs = util.RemoveStrings(c.AcceptAddrs, acceptAddrs)
228 | return c.save()
229 | }
230 |
231 | func (c *Config) GetAdminAddrs() []string {
232 | c.lock.RLock()
233 | defer c.lock.RUnlock()
234 | return c.AdminAddrs
235 | }
236 |
237 | func (c *Config) SetAdminAddrs(adminAddrs []string) error {
238 | c.lock.Lock()
239 | defer c.lock.Unlock()
240 | c.AdminAddrs = adminAddrs
241 | return c.save()
242 | }
243 |
244 | func (c *Config) AddAdminAddrs(adminAddrs []string) error {
245 | c.lock.Lock()
246 | defer c.lock.Unlock()
247 | c.AdminAddrs = util.MergeStrings(c.AdminAddrs, adminAddrs)
248 | return c.save()
249 | }
250 |
251 | func (c *Config) RemoveAdminAddrs(adminAddrs []string) error {
252 | c.lock.Lock()
253 | defer c.lock.Unlock()
254 | c.AdminAddrs = util.RemoveStrings(c.AdminAddrs, adminAddrs)
255 | return c.save()
256 | }
257 |
258 | func (c *Config) SetAdminHTTPAPI(disable bool) error {
259 | c.lock.Lock()
260 | defer c.lock.Unlock()
261 | c.DisableAdminHTTPAPI = disable
262 | return c.save()
263 | }
264 |
265 | func (c *Config) SetSeed(s string) error {
266 | seed, err := hex.DecodeString(s)
267 | if err != nil {
268 | return errors.New("invalid seed string, should be a hex string")
269 | }
270 |
271 | if len(seed) != ed25519.SeedSize {
272 | return fmt.Errorf("invalid seed string length %d, should be %d", len(s), 2*ed25519.SeedSize)
273 | }
274 |
275 | c.lock.Lock()
276 | defer c.lock.Unlock()
277 | c.Seed = s
278 | return c.save()
279 | }
280 |
281 | func (c *Config) SetTunaConfig(serviceName string, country []string, allowNknAddr []string, disallowNknAddr []string, allowIp []string, disallowIp []string) error {
282 | c.lock.Lock()
283 | defer c.lock.Unlock()
284 | c.TunaServiceName = serviceName
285 | c.TunaCountry = country
286 | c.TunaAllowNknAddr = allowNknAddr
287 | c.TunaDisallowNknAddr = disallowNknAddr
288 | c.TunaAllowIp = allowIp
289 | c.TunaDisallowIp = disallowIp
290 | return c.save()
291 | }
292 |
293 | func (c *Config) Save() error {
294 | c.lock.Lock()
295 | defer c.lock.Unlock()
296 | return c.save()
297 | }
298 |
299 | func (c *Config) save() error {
300 | if len(c.path) == 0 {
301 | return nil
302 | }
303 |
304 | b, err := json.MarshalIndent(c, "", " ")
305 | if err != nil {
306 | return err
307 | }
308 |
309 | err = os.WriteFile(c.path, b, 0666)
310 | if err != nil {
311 | return err
312 | }
313 |
314 | return nil
315 | }
316 |
317 | func RandomIdentifier() string {
318 | b := make([]byte, RandomIdentifierLength)
319 | for i := range b {
320 | b[i] = RandomIdentifierChars[rand.Intn(len(RandomIdentifierChars))]
321 | }
322 | return string(b)
323 | }
324 |
--------------------------------------------------------------------------------