├── 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 | 14 | 15 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /web/src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 |
Loading...
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 | --------------------------------------------------------------------------------