diff --git a/README.md b/README.md index 31c4ce5..3c8457d 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ tunneling, thus benefits from all the advantages of [nkn-tunnel](https://github.com/nknorg/nkn-tunnel): - Network agnostic: Neither sender nor receiver needs to have public IP address - or port forwarding. NKN tunnel only establish outbound (websocket) - connections, so Internet access is all they need on both side. + or port forwarding. NKN tunnel only establishes outbound (websocket) + connections, so Internet access is all they need on both sides. - Top level security: All data are end to end authenticated and encrypted. No - one else in the world except sender and receiver can see or modify the content + one else in the world except the sender and receiver can see or modify the content of the data. The same public key is used for both routing and encryption, eliminating the possibility of man in the middle attack. @@ -288,10 +288,129 @@ $ nConnect -c -a server-address1 -a server-address2 -a server-address3 ``` - ## Use `config.json` to Simplify Command Arguments -You can use `config.json` to simpliy command arguments. Move config.client.json or config.server.json as `config.json` and edit it before starting your nConnect client or server. After saving `config.json`, you can start nConnect simply. +You can use `config.json` to simplify command arguments. Copy config.client.json or config.server.json as `config.json` and edit it before starting your nConnect client or server. After saving `config.json`, you can start nConnect simply. + +## Set up a Virtual Private Network by nConnect +Yes, nConnect supports setting up a virtual private network. It means many computers can join a nConnect virtual network, and access each other just like all nodes are in a local network no matter where they are. + +In nConnect private virtual network, there are two types of nodes: one manager node, and network member nodes. The manager node is an administrative node that configures network parameters and authorizes members. + +To set up a nConnect network, you need firstly to start a network manager node. + +### Start a network manager +To start the network manager, we copy `config.network.json` to `config.manager.json`: + +``` + +$ cp config.network.josn config.manager.json + +``` + +Then edit config.json, enable `NetworkManager`, and give a value to "identifier", just like below: + +``` + +{ + "identifier": "manager", + "AdminHTTPAddr": "127.0.0.1:8001", +} + +``` + +Then start nConnect as a network manager with parameter `-m -f config.manager.json` : + +``` + +$ ./nConnect -m -f config.manager.json + +``` + +After nConnect network manager starts, you can see a console printed message: + +``` + +nConnect network manager is listening at: manager.0ec192083.... +Network manager web serve at: http://127.0.0.1:8001/network + +``` + +Copy this listening address: `manager.0ec192083....`, it is the manager's address. Other member nodes need this address to join this network. +After the manager starts, you can visit the web service `http://127.0.0.1:8001/network` (default), to config, to manage the network. + +### Start some network members and join the network +On another computer, you can start a network member, and let it join the nConnect which you start above. +First, you copy `config.network.json` to `config.member.json` + +``` + +$ cp config.network.json config.member.json + +``` + +Then edit `config.member.json` to edit `identifier`, `managerAddress` and `nodeName`. + +``` +{ + "identifier": "alice", + "managerAddress": "manager.0ec192083....", + "nodeName": "alice", + "seed": "...", + "AdminHTTPAddr": "127.0.0.1:8000", +} + +``` + +Set `managerAddress` as your network manager's listening address, and identify your node name `nodeName`. Each network member should have different `nodeName`. +The field `seed` is the seed of the wallet which you use to pay for `tuna` fee. Please keep it secured. If your wallet has zero balance, then nConnect Server could not start at `tuna` mode. + +Then you can start this node to join the network: + +``` + +$ ./nConnect -n -s -c -f config.member.json --tuna --vpn --udp + +``` + +`-n` means this is a network member `node` +For a network member, you may start both `-c` client, and `-s` server, which means you can access other nodes, and other nodes can access you too. +Or you can only set `-c`, which means you can access other nodes, but you don't want other nodes to access you. +Or you can only set `-s`, which means you can only be accessed, and you don't want to access other nodes. + +### Manage the network + +When a network member starts, it first will send `JoinNetwork` message to the network manager. +After the network administrator should open the manager's web administrate page `http://127.0.0.1:8001/network` (default), to config the network name, IP range, netmask, and gateway. + +There are two lists on the manager's web page: +* Waiting for Authorization + This lists all the nodes which are waiting for authorization to join this network. The administrator can accept it or reject it. + Only authorized nodes can become network members and will get a network-specific IP address. + When authorizing a node, it will pop up a dialog to set this node's permission to other nodes which decide if all members or only some of them can access this node. + +* Network Members + In the network members list, the administrator can reset nodes' access permission and remove a node from the network (authorization). + +If you don't see your node information in `Waiting for Authorization`, please click `Refresh` button to fetch updated data from the manager. + +### Test your network +From the manager web page, you can see all member's IP. To test your network, you can run a TCP server on a member node: + +``` + +nconnect$ go run tests/tools/tcpmain.go -server + +``` + +Then you run a TCP client on another member node, for example, the TCP server node's IP is: 10.20.30.2 + +``` + +nconnect$ go run tests/tools/tcpmain.go -serverAddr 10.20.30.2 + +``` +You will see messages transmit on both the server and the client. ## Contributing diff --git a/admin/client.go b/admin/client.go index 71810c3..2aea904 100644 --- a/admin/client.go +++ b/admin/client.go @@ -3,6 +3,7 @@ package admin import ( "encoding/json" "errors" + "log" "time" "github.com/nknorg/nconnect/config" @@ -14,7 +15,7 @@ const ( ) var ( - errReplyTimeout = errors.New("wait for reply timeout") + ErrReplyTimeout = errors.New("wait for reply timeout") ) var ( @@ -23,18 +24,21 @@ var ( type Client struct { *nkn.MultiClient - replyTimeout time.Duration + ReplyTimeout time.Duration } -func NewClient(account *nkn.Account, clientConfig *nkn.ClientConfig) (*Client, error) { - m, err := nkn.NewMultiClient(account, config.RandomIdentifier(), 4, false, clientConfig) +func NewClient(account *nkn.Account, clientConfig *nkn.ClientConfig, identifier string) (*Client, error) { + if identifier == "" { + identifier = config.RandomIdentifier() + } + m, err := nkn.NewMultiClient(account, identifier, 4, false, clientConfig) if err != nil { return nil, err } c := &Client{ MultiClient: m, - replyTimeout: replyTimeout, + ReplyTimeout: replyTimeout, } <-m.OnConnect.C @@ -43,36 +47,18 @@ func NewClient(account *nkn.Account, clientConfig *nkn.ClientConfig) (*Client, e } func (c *Client) RPCCall(addr, method string, params interface{}, result interface{}) error { - req, err := json.Marshal(map[string]interface{}{ + req := map[string]interface{}{ "id": "nConnect", "method": method, "params": params, - }) - if err != nil { - return err } - var onReply *nkn.OnMessage - var reply *nkn.Message -Loop: - for i := 0; i < 3; i++ { // retry 3 times if timeout - onReply, err = c.Send(nkn.NewStringArray(addr), req, nil) - if err != nil { - return err - } - - select { - case reply = <-onReply.C: - break Loop - case <-time.After(c.replyTimeout): - err = errReplyTimeout - } - } + reply, err := c.SendMsg(addr, req, true) if err != nil { return err } - resp := &rpcResp{ + resp := &RpcResp{ Result: result, } err = json.Unmarshal(reply.Data, resp) @@ -95,3 +81,40 @@ func (c *Client) GetInfo(addr string) (*GetInfoJSON, error) { } return res, nil } + +func (c *Client) SendMsg(address string, msg interface{}, waitResponse bool) (reply *nkn.Message, err error) { + if c.ReplyTimeout == 0 { + c.ReplyTimeout = replyTimeout + } + + reqBytes, err := json.Marshal(msg) + if err != nil { + return nil, err + } + + var onReply *nkn.OnMessage + for i := 0; i < 3; i++ { + onReply, err = c.Send(nkn.NewStringArray(address), reqBytes, nil) + if err != nil { + return nil, err + } + + if !waitResponse { + return nil, nil + } + + select { + case reply = <-onReply.C: + return reply, nil + + case <-time.After(c.ReplyTimeout): + err = ErrReplyTimeout + } + } + + if err == ErrReplyTimeout { + log.Printf("Wait for repsone timeout, please make sure the server is running and reachable") + } + + return nil, err +} diff --git a/admin/common.go b/admin/common.go index 95f49fa..ba78cef 100644 --- a/admin/common.go +++ b/admin/common.go @@ -46,7 +46,7 @@ var ( } ) -type rpcReq struct { +type RpcReq struct { ID string `json:"id"` JSONRPC string `json:"jsonrpc"` Method string `json:"method"` @@ -54,7 +54,7 @@ type rpcReq struct { Token string `json:"token"` } -type rpcResp struct { +type RpcResp struct { Result interface{} `json:"result,omitempty"` Error string `json:"error,omitempty"` } @@ -107,8 +107,8 @@ type getLogJSON struct { MaxSize int `json:"maxSize"` } -func handleRequest(req *rpcReq, persistConf, mergedConf *config.Config, tun *tunnel.Tunnel, rpcPerm permission) *rpcResp { - resp := &rpcResp{} +func handleRequest(req *RpcReq, persistConf, mergedConf *config.Config, tun *tunnel.Tunnel, rpcPerm permission) *RpcResp { + resp := &RpcResp{} if rpcPermissions[req.Method]&rpcPerm == 0 { resp.Error = errPermissionDenied.Error() diff --git a/admin/server.go b/admin/server.go index b77e6e6..4750de9 100644 --- a/admin/server.go +++ b/admin/server.go @@ -23,7 +23,7 @@ func StartNKNServer(account *nkn.Account, identifier string, clientConfig *nkn.C for { msg := <-m.OnMessage.C - req := &rpcReq{} + req := &RpcReq{} err := json.Unmarshal(msg.Data, req) if err != nil { log.Println("Unmarshal client request error:", err) diff --git a/admin/web.go b/admin/web.go index 97dcb8c..dc88171 100644 --- a/admin/web.go +++ b/admin/web.go @@ -23,13 +23,13 @@ func StartWebServer(listenAddr string, tun *tunnel.Tunnel, persistConf, mergedCo r.Use(gzip.Gzip(gzip.DefaultCompression)) r.POST("/rpc/admin", func(c *gin.Context) { - req := &rpcReq{} + req := &RpcReq{} if err := c.ShouldBindJSON(req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if mergedConf.DisableAdminHTTPAPI { - c.JSON(http.StatusOK, &rpcResp{Error: errAdminHTTPAPIDisabled.Error()}) + c.JSON(http.StatusOK, &RpcResp{Error: errAdminHTTPAPIDisabled.Error()}) return } resp := handleRequest(req, persistConf, mergedConf, tun, rpcPermissionWeb) diff --git a/arch/network.go b/arch/network.go new file mode 100644 index 0000000..ddd9027 --- /dev/null +++ b/arch/network.go @@ -0,0 +1,78 @@ +package arch + +import ( + "fmt" + "io" + "log" + "net" + "os" + "time" + + "github.com/eycorsican/go-tun2socks/core" + "github.com/eycorsican/go-tun2socks/proxy/socks" + "github.com/nknorg/nconnect/util" +) + +const ( + mtu = 1500 +) + +func OpenTun(tunName, ip, gateway, mask, dns, socksAddr string) error { + tunDevice, err := OpenTunDevice(tunName, ip, gateway, mask, []string{dns}, false) + if err != nil { + return fmt.Errorf("failed to open TUN device: %v", err) + } + + core.RegisterOutputFn(tunDevice.Write) + + proxyAddr, err := net.ResolveTCPAddr("tcp", socksAddr) + if err != nil { + return fmt.Errorf("invalid proxy server address %v err: %v", socksAddr, err) + } + proxyHost := proxyAddr.IP.String() + proxyPort := uint16(proxyAddr.Port) + + core.RegisterTCPConnHandler(socks.NewTCPHandler(proxyHost, proxyPort)) + core.RegisterUDPConnHandler(socks.NewUDPHandler(proxyHost, proxyPort, 30*time.Second)) + + lwipWriter := core.NewLWIPStack() + + go func() { + _, err := io.CopyBuffer(lwipWriter, tunDevice, make([]byte, mtu)) + if err != nil { + log.Fatalf("Failed to write data to network stack: %v", err) + } + }() + + return nil +} + +func SetVPNRoutes(tunName, gateway string, cidrs []*net.IPNet) ([]*net.IPNet, error) { + for _, dest := range cidrs { + log.Printf("Adding route %s", dest) + out, err := AddRouteCmd(dest, gateway, tunName) + if len(out) > 0 { + os.Stdout.Write(out) + } + if err != nil { + os.Stdout.Write([]byte(util.ParseExecError(err))) + os.Exit(1) + } + } + + return cidrs, nil +} + +func RemoveVPNRoutes(tunName, gateway string, cidrs []*net.IPNet) error { + for _, dest := range cidrs { + log.Printf("Deleting route %s", dest) + out, err := DeleteRouteCmd(dest, gateway, tunName) + if len(out) > 0 { + os.Stdout.Write(out) + } + if err != nil { + os.Stdout.Write([]byte(util.ParseExecError(err))) + } + } + return nil +} diff --git a/arch/tun_darwin.go b/arch/tun_darwin.go index f20de8a..8e45cac 100644 --- a/arch/tun_darwin.go +++ b/arch/tun_darwin.go @@ -9,3 +9,8 @@ import ( func OpenTunDevice(name, addr, gw, mask string, dnsServers []string, persist bool) (io.ReadWriteCloser, error) { return tun.OpenTunDevice(name, addr, gw, mask, dnsServers, persist) } + +func SetTunIp(tapName, ip, mask string) error { + + return nil +} diff --git a/arch/tun_linux.go b/arch/tun_linux.go index 24caeb4..e5b3d6c 100644 --- a/arch/tun_linux.go +++ b/arch/tun_linux.go @@ -20,12 +20,18 @@ func OpenTunDevice(name, addr, gw, mask string, dnsServers []string, persist boo return nil, err } + err = SetTunIp(name, addr, mask) + + return tunDev, err +} + +func SetTunIp(tapName, ip, mask string) error { out, err := func() ([]byte, error) { - out, err := exec.Command("ip", "addr", "replace", addr+"/"+mask, "dev", name).Output() + out, err := exec.Command("ip", "addr", "replace", ip+"/"+mask, "dev", tapName).Output() if err != nil { return out, err } - return exec.Command("ip", "link", "set", "dev", name, "up").Output() + return exec.Command("ip", "link", "set", "dev", tapName, "up").Output() }() if err != nil { if len(out) > 0 { @@ -33,20 +39,20 @@ func OpenTunDevice(name, addr, gw, mask string, dnsServers []string, persist boo } log.Println(util.ParseExecError(err)) - ip := net.ParseIP(addr) + ip := net.ParseIP(ip) if ip == nil { - return nil, errors.New("invalid IP address") + return errors.New("invalid IP address") } var params string if ip.To4() != nil { - params = fmt.Sprintf("%s inet %s netmask %s up", name, addr, mask) + params = fmt.Sprintf("%s inet %s netmask %s up", tapName, ip, mask) } else { prefixlen, err := strconv.Atoi(mask) if err != nil { - return nil, fmt.Errorf("parse IPv6 prefixlen failed: %v", err) + return fmt.Errorf("parse IPv6 prefixlen failed: %v", err) } - params = fmt.Sprintf("%s inet6 %s/%d up", name, addr, prefixlen) + params = fmt.Sprintf("%s inet6 %s/%d up", tapName, ip, prefixlen) } out, err := exec.Command("ifconfig", strings.Split(params, " ")...).Output() @@ -54,9 +60,8 @@ func OpenTunDevice(name, addr, gw, mask string, dnsServers []string, persist boo if len(out) > 0 { log.Print(string(out)) } - return nil, errors.New(util.ParseExecError(err)) + return errors.New(util.ParseExecError(err)) } } - - return tunDev, nil + return nil } diff --git a/arch/tun_test.go b/arch/tun_test.go new file mode 100644 index 0000000..68bcb06 --- /dev/null +++ b/arch/tun_test.go @@ -0,0 +1,14 @@ +package arch + +import "testing" + +func TestOpenTunDevice(t *testing.T) { + name := "tap0901" + addr := "192.168.0.2" + gw := "192.168.0.1" + mask := "255.255.255.0" + dnsServers := []string{"192.168.0.1"} + persist := false + + OpenTunDevice(name, addr, gw, mask, dnsServers, persist) +} diff --git a/arch/tun_windows.go b/arch/tun_windows.go index 5cd62e3..9c919e4 100644 --- a/arch/tun_windows.go +++ b/arch/tun_windows.go @@ -2,6 +2,8 @@ package arch import ( "io" + "log" + "os/exec" "github.com/eycorsican/go-tun2socks/tun" ) @@ -13,3 +15,9 @@ const ( func OpenTunDevice(name, addr, gw, mask string, dnsServers []string, persist bool) (io.ReadWriteCloser, error) { return tun.OpenTunDevice(name, addr, gw, mask, dnsServers, persist) } + +func SetTunIp(tapName, ip, mask string) error { + out, err := exec.Command("netsh", "interface", "ip", "set", "address", tapName, "static", ip, mask).Output() + log.Printf("SetTunIp: ip %s, mask %v, result: %s\n", ip, mask, string(out)) + return err +} diff --git a/bin/main.go b/bin/main.go index 6aef4bf..e8ca64e 100644 --- a/bin/main.go +++ b/bin/main.go @@ -36,12 +36,29 @@ func main() { log.Fatal(err) } + if opts.NetworkManager { + err = nc.StartNetworkManager() + if err != nil { + log.Fatal(err) + } + return + } + + if opts.NetworkMember { + err = nc.StartNetworkMember() + if err != nil { + log.Fatal(err) + } + return + } + if opts.Client { err = nc.StartClient() if err != nil { log.Fatal(err) } } + if opts.Server { err = nc.StartServer() if err != nil { diff --git a/config.client.json b/config.client.json index c9cab7e..2bdc929 100644 --- a/config.client.json +++ b/config.client.json @@ -1,13 +1,13 @@ -{ - "Client": true, - "Server": false, - "identifier": "", - "seed": "", - "remoteAdminAddr": [], - "localSocksAddr": "127.0.0.1:1080", - "tuna": true, - "udp": true, - "acceptAddrs": null, - "adminAddrs": null -} +{ + "Client": true, + "Server": false, + "identifier": "", + "seed": "", + "remoteAdminAddr": [], + "localSocksAddr": "127.0.0.1:1080", + "tuna": true, + "udp": true, + "acceptAddrs": null, + "adminAddrs": null +} \ No newline at end of file diff --git a/config.network.json b/config.network.json new file mode 100644 index 0000000..9b9ef62 --- /dev/null +++ b/config.network.json @@ -0,0 +1,27 @@ +{ + "identifier": "", + "seed": "", + + "managerAddress": "", + "nodeName": "", + + "cipher": "dummy", + "adminIdentifier": "nConnect", + "adminHTTPAddr": "127.0.0.1:8000", + + "remoteAdminAddr": [], + "localSocksAddr": "127.0.0.1:1080", + "tuna": true, + "udp": true, + "acceptAddrs": [], + "adminAddrs": [], + + "tunAddr": "10.0.86.2", + "tunGateway": "10.0.86.1", + "tunMask": "255.255.255.0", + "tunDNS": ["1.1.1.1", "8.8.8.8"], + + "tunaDisableMeasureBandwidth": false, + "verbose": false +} + \ No newline at end of file diff --git a/config.server.json b/config.server.json index e2434e0..d58dbc1 100644 --- a/config.server.json +++ b/config.server.json @@ -1,14 +1,14 @@ -{ - "Client": false, - "Server": true, - "identifier": "", - "seed": "", - "remoteAdminAddr": [], - "localSocksAddr": "", - "tuna": true, - "udp": true, - "adminIdentifier": "nConnect", - "webRootPath": "web/dist", - "acceptAddrs": [], - "adminAddrs": [] -} +{ + "Client": false, + "Server": true, + "identifier": "", + "seed": "", + "remoteAdminAddr": [], + "localSocksAddr": "", + "tuna": true, + "udp": true, + "adminIdentifier": "nConnect", + "webRootPath": "web/dist", + "acceptAddrs": [], + "adminAddrs": [] +} diff --git a/config/config.go b/config/config.go index 52123f1..524b788 100644 --- a/config/config.go +++ b/config/config.go @@ -34,8 +34,10 @@ func init() { } type Opts struct { - Client bool `short:"c" long:"client" description:"Client mode"` - Server bool `short:"s" long:"server" description:"Server mode"` + Client bool `short:"c" long:"client" description:"Client mode"` + Server bool `short:"s" long:"server" description:"Server mode"` + NetworkManager bool `short:"m" long:"network-manager" description:"Network manager mode"` + NetworkMember bool `short:"n" long:"network-member" description:"Join nConnect network as a member node"` Config ConfigFile string `short:"f" long:"config-file" default:"config.json" description:"Config file path"` @@ -123,6 +125,10 @@ type Config struct { lock sync.RWMutex AcceptAddrs []string `json:"acceptAddrs"` AdminAddrs []string `json:"adminAddrs"` + + // nconnect network + NodeName string `json:"nodeName,omitempty" long:"node-name" description:"(network member only) Node name that will be used as to join a network"` + ManagerAddress string `json:"managerAddress,omitempty" long:"manager-address" description:"(network member only) Manager address to connect to when joining a network"` } func NewConfig() *Config { diff --git a/go.mod b/go.mod index 704dfff..989603f 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.20 require ( github.com/eycorsican/go-tun2socks v1.16.11 + github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/gzip v0.0.3 github.com/gin-gonic/gin v1.9.0 github.com/imdario/mergo v0.3.15 github.com/jessevdk/go-flags v1.5.0 github.com/nknorg/ncp-go v1.0.6-0.20230228002512-f4cd1740bebd github.com/nknorg/nkn-sdk-go v1.4.6-0.20230404044330-ad192f36d07e - github.com/nknorg/nkn-tuna-session v0.2.6-0.20230720223014-911b930bb501 - github.com/nknorg/nkn-tunnel v0.3.5-0.20230720223134-353fece879da + github.com/nknorg/nkn-tuna-session v0.2.6-0.20230803011026-70aae317eec6 + github.com/nknorg/nkn-tunnel v0.3.5-0.20230804232435-382c0785fad3 github.com/nknorg/nkn/v2 v2.2.0 github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9 github.com/nknorg/tuna v0.0.0-20230720072916-745c518d52a8 @@ -20,6 +21,7 @@ require ( github.com/txthinking/brook v0.0.0-20230418095906-76ced63f1803 github.com/txthinking/socks5 v0.0.0-20230307062227-0e1677eca4ba golang.org/x/net v0.8.0 + google.golang.org/protobuf v1.29.1 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -91,6 +93,5 @@ require ( golang.org/x/sys v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/tools v0.6.0 // indirect - google.golang.org/protobuf v1.29.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5a1822f..b470ec0 100644 --- a/go.sum +++ b/go.sum @@ -19,26 +19,33 @@ github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/I github.com/eycorsican/go-tun2socks v1.16.11/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ= github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k= github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -87,8 +94,13 @@ github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7y github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk= @@ -97,6 +109,7 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= @@ -114,10 +127,10 @@ github.com/nknorg/ncp-go v1.0.6-0.20230228002512-f4cd1740bebd h1:ZAXKeWKjkbS9QQh github.com/nknorg/ncp-go v1.0.6-0.20230228002512-f4cd1740bebd/go.mod h1:T7ThlxmBjVIv3Ll3gJOHbQTuAFN3ZCYWvbux6JOX5wQ= github.com/nknorg/nkn-sdk-go v1.4.6-0.20230404044330-ad192f36d07e h1:lWKUEfqOJ9NImCX60Bden+Y6VgRDhlx4gc6B09S32RQ= github.com/nknorg/nkn-sdk-go v1.4.6-0.20230404044330-ad192f36d07e/go.mod h1:mnI1+17p2cI+5wv+3CWRyCjSALqUg5k1jTaWC2h0f/M= -github.com/nknorg/nkn-tuna-session v0.2.6-0.20230720223014-911b930bb501 h1:9fTysrDKfUhpab2orbEe+vdH2GG+fDP3jP6zB75amos= -github.com/nknorg/nkn-tuna-session v0.2.6-0.20230720223014-911b930bb501/go.mod h1:VMoe4p9aGs9jTWtwUCBGQO7cyXzykbPn4OQpZbqdpUo= -github.com/nknorg/nkn-tunnel v0.3.5-0.20230720223134-353fece879da h1:BrIEOCNsjHoAT0/zV0YXXLnPXGhlS3JwvrwMi9ek14g= -github.com/nknorg/nkn-tunnel v0.3.5-0.20230720223134-353fece879da/go.mod h1:AWUMC+vfWBi1JMBee3VUMn09pPTgc7Tgc/0nf9C2taU= +github.com/nknorg/nkn-tuna-session v0.2.6-0.20230803011026-70aae317eec6 h1:ZvBX/sZJH7EsZcO+3f477yl7Ds8sOOgb0SatF2URa1o= +github.com/nknorg/nkn-tuna-session v0.2.6-0.20230803011026-70aae317eec6/go.mod h1:VMoe4p9aGs9jTWtwUCBGQO7cyXzykbPn4OQpZbqdpUo= +github.com/nknorg/nkn-tunnel v0.3.5-0.20230804232435-382c0785fad3 h1:/AxzH0Ij5D5GrFmGTy0jwDvEhHA6fWONr9lrtjaUkTg= +github.com/nknorg/nkn-tunnel v0.3.5-0.20230804232435-382c0785fad3/go.mod h1:tK38iEeJhhJdyciyy/qM/O7FvsSDpq66jFBR67V3Rsg= github.com/nknorg/nkn/v2 v2.2.0 h1:sXOawvVF/T3bBTuWbzBCyrGuxldA3be+f+BDjoWcOEA= github.com/nknorg/nkn/v2 v2.2.0/go.mod h1:yv3jkg0aOtN9BDHS4yerNSZJtJNBfGvlaD5K6wL6U3E= github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9 h1:Gr37j7Ttvcn8g7TdC5fs6Y6IJKdmfqCvj03UbsrS77o= @@ -135,6 +148,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/phuslu/iploc v1.0.20230201 h1:AMhy7j8z0N5iI0jaqh514KTDEB7wVdQJ4Y4DJPCvKBU= @@ -158,6 +172,8 @@ github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KP github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= github.com/riobard/go-bloom v0.0.0-20200213042214-218e1707c495 h1:p7xbxYTzzfXghR1kpsJDeoVVRRWAotKc8u7FP/N48rU= github.com/riobard/go-bloom v0.0.0-20200213042214-218e1707c495/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -196,7 +212,9 @@ github.com/txthinking/socks5 v0.0.0-20230307062227-0e1677eca4ba/go.mod h1:ntmMHL github.com/txthinking/x v0.0.0-20220929041811-1b4d914e9133 h1:fUw8+3ruX0uv2gAko4D0v6IpLmSI2soOkGl6YYmiBrM= github.com/txthinking/x v0.0.0-20220929041811-1b4d914e9133/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= @@ -212,6 +230,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= @@ -249,6 +268,8 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -286,16 +307,22 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/nConnect.exe b/nConnect.exe new file mode 100644 index 0000000..11b2f2e Binary files /dev/null and b/nConnect.exe differ diff --git a/nconnect.go b/nconnect.go index 95fa227..2986218 100644 --- a/nconnect.go +++ b/nconnect.go @@ -2,23 +2,23 @@ package nconnect import ( "encoding/hex" + "errors" "fmt" - "io" "log" "net" "os" "os/signal" "strconv" "strings" + "sync" "syscall" "time" - "github.com/eycorsican/go-tun2socks/core" - "github.com/eycorsican/go-tun2socks/proxy/socks" "github.com/imdario/mergo" "github.com/nknorg/nconnect/admin" "github.com/nknorg/nconnect/arch" "github.com/nknorg/nconnect/config" + "github.com/nknorg/nconnect/network" "github.com/nknorg/nconnect/ss" "github.com/nknorg/nconnect/util" "github.com/nknorg/ncp-go" @@ -34,26 +34,32 @@ import ( "gopkg.in/natefinch/lumberjack.v2" ) -const ( - mtu = 1500 -) - type nconnect struct { opts *config.Opts account *nkn.Account - walletConfig *nkn.WalletConfig - clientConfig *nkn.ClientConfig - tunnelConfig *tunnel.Config - ssConfig *ss.Config - persistConf *config.Config + walletConfig *nkn.WalletConfig + clientConfig *nkn.ClientConfig + tunnelConfig *tunnel.Config + ssClientConfig *ss.Config + ssServerConfig *ss.Config + persistConf *config.Config adminClientCache *admin.Client + sync.RWMutex // lock for maps remoteInfoCache map[string]*admin.GetInfoJSON // map remote admin address to remote info remoteInfoByTunnel map[string]*admin.GetInfoJSON // map tunnel address to remote info - tunnels []*tunnel.Tunnel + clientTunnels []*tunnel.Tunnel // tunnels for client mode + serverTunnel *tunnel.Tunnel // tunnel for server mode + serverReady chan struct{} // channel to notify server is ready + tunaNode *types.Node // It is used to connect specified tuna node, mainly is for testing. + + networkMember *network.Member + networkTunnels map[string]*tunnel.Tunnel // tunnels for network nodes + routeCIDRs []*net.IPNet // CIDRs for routing traffic through network nodes + openTunOnce sync.Once // only open tun device once } func NewNconnect(opts *config.Opts) (*nconnect, error) { @@ -62,8 +68,10 @@ func NewNconnect(opts *config.Opts) (*nconnect, error) { return nil, err } - if opts.Client == opts.Server { - log.Fatal("Exactly one mode (client or server) should be selected.") + if !(opts.NetworkMember || opts.NetworkManager) { + if opts.Client == opts.Server { + log.Fatal("Exactly one mode (client or server) should be selected if not join a network.") + } } persistConf, err := config.LoadOrNewConfig(opts.ConfigFile) @@ -221,6 +229,10 @@ func NewNconnect(opts *config.Opts) (*nconnect, error) { TunaMeasureStoragePath: opts.TunaMeasureStoragePath, TunaMeasurementBytesDownLink: opts.TunaMeasureBandwidthBytes, TunaMinBalance: opts.TunaMinBalance, + Verbose: opts.Verbose, + } + if opts.Verbose { + tsConfig.NumTunaListeners = 1 } if opts.SessionWindowSize > 0 { @@ -239,7 +251,7 @@ func NewNconnect(opts *config.Opts) (*nconnect, error) { UDPIdleTime: opts.UDPIdleTime, } - ssConfig := &ss.Config{ + ssClientConfig := ss.Config{ TCP: true, Cipher: opts.Cipher, Password: opts.Password, @@ -252,20 +264,24 @@ func NewNconnect(opts *config.Opts) (*nconnect, error) { } if opts.UDP && opts.Client { - ssConfig.UDPSocks = true + ssClientConfig.UDPSocks = true } + ssServerConfig := ssClientConfig nc := &nconnect{ - opts: opts, - account: account, - clientConfig: clientConfig, - tunnelConfig: tunnelConfig, - ssConfig: ssConfig, - walletConfig: walletConfig, - persistConf: persistConf, + opts: opts, + account: account, + clientConfig: clientConfig, + tunnelConfig: tunnelConfig, + ssClientConfig: &ssClientConfig, + ssServerConfig: &ssServerConfig, + walletConfig: walletConfig, + persistConf: persistConf, remoteInfoCache: make(map[string]*admin.GetInfoJSON), remoteInfoByTunnel: make(map[string]*admin.GetInfoJSON), + networkTunnels: make(map[string]*tunnel.Tunnel), + serverReady: make(chan struct{}, 1), } return nc, nil @@ -276,7 +292,16 @@ func (nc *nconnect) getAdminClient() (*admin.Client, error) { if nc.adminClientCache != nil { return nc.adminClientCache, nil } - c, err := admin.NewClient(nc.account, nc.clientConfig) + + identifier := "" + if nc.opts.NetworkMember { + if nc.opts.Identifier == "" { + identifier = "client" + } else { + identifier = "client." + nc.opts.Identifier + } + } + c, err := admin.NewClient(nc.account, nc.clientConfig, identifier) if err != nil { return nil, err } @@ -300,7 +325,7 @@ func (nc *nconnect) getRemoteInfo(remoteAdminAddr string) (*admin.GetInfoJSON, e remoteInfoCache, err := c.GetInfo(remoteAdminAddr) if err != nil { - return nil, fmt.Errorf("get remote server info error: %v. make sure server is online and accepting connections from this client address", err) + return nil, fmt.Errorf("get remote server info error: %v. make sure server is online and accepting this client address", err) } nc.remoteInfoCache[remoteAdminAddr] = remoteInfoCache @@ -310,9 +335,11 @@ func (nc *nconnect) getRemoteInfo(remoteAdminAddr string) (*admin.GetInfoJSON, e } func (nc *nconnect) StartClient() error { - err := nc.opts.VerifyClient() - if err != nil { - return err + if !nc.opts.NetworkMember { + err := nc.opts.VerifyClient() + if err != nil { + return err + } } remoteTunnelAddr := nc.opts.RemoteTunnelAddr @@ -326,128 +353,65 @@ func (nc *nconnect) StartClient() error { remoteTunnelAddr = append(remoteTunnelAddr, remoteInfo.Addr) } } - if len(remoteTunnelAddr) == 0 { + if !nc.opts.NetworkMember && len(remoteTunnelAddr) == 0 { return fmt.Errorf("no remote tunnel address, start client fail") } - var vpnCIDR []*net.IPNet - if nc.opts.VPN { - vpnRoutes := nc.opts.VPNRoute - if len(vpnRoutes) == 0 { - for _, remoteAdminAddr := range nc.opts.RemoteAdminAddr { - remoteInfo, err := nc.getRemoteInfo(remoteAdminAddr) - if err != nil { - log.Printf("getRemoteInfo %v err: %v", remoteAdminAddr, err) - continue - } - if len(remoteInfo.LocalIP.Ipv4) > 0 { - vpnRoutes = make([]string, 0, len(remoteInfo.LocalIP.Ipv4)) - for _, ip := range remoteInfo.LocalIP.Ipv4 { - if ip == nc.opts.TunAddr || ip == nc.opts.TunGateway { - log.Printf("Skipping server's local IP %s in routes", ip) - continue - } - vpnRoutes = append(vpnRoutes, fmt.Sprintf("%s/32", ip)) - } - } - } - } - if len(vpnRoutes) > 0 { - vpnCIDR = make([]*net.IPNet, len(vpnRoutes)) - for i, cidr := range vpnRoutes { - _, cidr, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("parse CIDR %s error: %v", cidr, err) - } - vpnCIDR[i] = cidr - } - } - } - - proxyAddr, err := net.ResolveTCPAddr("tcp", nc.opts.LocalSocksAddr) + vpnRoutes, err := nc.getRemoteRoutes() if err != nil { - return fmt.Errorf("invalid proxy server address: %v", err) + return err } - proxyHost := proxyAddr.IP.String() - proxyPort := uint16(proxyAddr.Port) - var from, to []string - for _, remote := range remoteTunnelAddr { - port, err := util.GetFreePort() - if err != nil { - return err - } + if len(remoteTunnelAddr) > 0 { + var from, to []string + for _, remote := range remoteTunnelAddr { + port, err := util.GetFreePort(0) + if err != nil { + return err + } - ssAddr := "127.0.0.1:" + strconv.Itoa(port) - from = append(from, ssAddr) - to = append(to, remote) + ssAddr := "127.0.0.1:" + strconv.Itoa(port) + from = append(from, ssAddr) + to = append(to, remote) - if remoteInfo, ok := nc.remoteInfoByTunnel[remote]; ok { - for _, addr := range remoteInfo.LocalIP.Ipv4 { - nc.ssConfig.TargetToClient[addr] = ssAddr + if remoteInfo, ok := nc.remoteInfoByTunnel[remote]; ok { + for _, addr := range remoteInfo.LocalIP.Ipv4 { + nc.ssClientConfig.TargetToClient[addr] = ssAddr + } } } - } - tunnels, err := tunnel.NewTunnels(nc.account, nc.opts.Identifier, from, to, nc.opts.Tuna, nc.tunnelConfig) - if err != nil { - return err - } - nc.tunnels = tunnels - - nc.ssConfig.Socks = nc.opts.LocalSocksAddr - nc.ssConfig.Client = from[0] - nc.ssConfig.DefaultClient = from[0] // the first config is the default client - - log.Println("Client socks proxy listen address:", nc.opts.LocalSocksAddr) - if nc.opts.Tun || nc.opts.VPN { - tunDevice, err := arch.OpenTunDevice(nc.opts.TunName, nc.opts.TunAddr, nc.opts.TunGateway, nc.opts.TunMask, nc.opts.TunDNS, true) + identifier := config.RandomIdentifier() + tunnels, err := tunnel.NewTunnels(nc.account, identifier, from, to, nc.opts.Tuna, nc.tunnelConfig, nil) if err != nil { - return fmt.Errorf("failed to open TUN device: %v", err) + return err } + nc.clientTunnels = tunnels - core.RegisterOutputFn(tunDevice.Write) - - core.RegisterTCPConnHandler(socks.NewTCPHandler(proxyHost, proxyPort)) - core.RegisterUDPConnHandler(socks.NewUDPHandler(proxyHost, proxyPort, 30*time.Second)) - - lwipWriter := core.NewLWIPStack() + nc.ssClientConfig.Client = from[0] + nc.ssClientConfig.DefaultClient = from[0] // the first config is the default client + } else { + nc.ssClientConfig.Client = "127.0.0.1" + nc.ssClientConfig.DefaultClient = "" + } + nc.ssClientConfig.Socks = nc.opts.LocalSocksAddr - go func() { - _, err := io.CopyBuffer(lwipWriter, tunDevice, make([]byte, mtu)) - if err != nil { - log.Fatalf("Failed to write data to network stack: %v", err) - } - }() + log.Println("nConnect client socks proxy listen address:", nc.opts.LocalSocksAddr) - log.Println("Started tun2socks") + if nc.opts.Tun || nc.opts.VPN { + nc.openTun() if nc.opts.VPN { - for _, dest := range vpnCIDR { - log.Printf("Adding route %s", dest) - out, err := arch.AddRouteCmd(dest, nc.opts.TunGateway, nc.opts.TunName) - if len(out) > 0 { - os.Stdout.Write(out) - } - if err != nil { - os.Stdout.Write([]byte(util.ParseExecError(err))) - os.Exit(1) - } - defer func(dest *net.IPNet) { - log.Printf("Deleting route %s", dest) - out, err := arch.DeleteRouteCmd(dest, nc.opts.TunGateway, nc.opts.TunName) - if len(out) > 0 { - os.Stdout.Write(out) - } - if err != nil { - os.Stdout.Write([]byte(util.ParseExecError(err))) - } - }(dest) + vpnCIDR, err := arch.SetVPNRoutes(nc.opts.TunName, nc.opts.TunGateway, vpnRoutes) + if err != nil { + return err } + nc.routeCIDRs = vpnCIDR + defer arch.RemoveVPNRoutes(nc.opts.TunName, nc.opts.TunGateway, nc.routeCIDRs) } } - nc.startSSAndTunnel() + nc.startSSAndTunnel(true) nc.waitForSignal() return nil @@ -459,12 +423,13 @@ func (nc *nconnect) StartServer() error { return err } - port, err := util.GetFreePort() + port, err := util.GetFreePort(0) if err != nil { return err } ssAddr := "127.0.0.1:" + strconv.Itoa(port) - nc.ssConfig.Server = ssAddr + nc.ssServerConfig.Server = ssAddr + nc.ssServerConfig.Client = "" if nc.opts.Tuna { minBalance, err := common.StringToFixed64(nc.opts.TunaMinBalance) @@ -492,12 +457,16 @@ func (nc *nconnect) StartServer() error { if nc.tunaNode != nil { nc.tunnelConfig.TunaNode = nc.tunaNode } - t, err := tunnel.NewTunnel(nc.account, nc.opts.Identifier, "", ssAddr, nc.opts.Tuna, nc.tunnelConfig) + + t, err := tunnel.NewTunnel(nc.account, nc.opts.Identifier, "", ssAddr, nc.opts.Tuna, nc.tunnelConfig, nil) if err != nil { return err } - nc.tunnels = append(nc.tunnels, t) - log.Println("Tunnel listen address:", t.FromAddr()) + nc.serverTunnel = t + log.Println("nConnect server tunnel listen address:", t.FromAddr()) + if nc.networkMember != nil { + nc.networkMember.SetServerTunnel(t) + } if len(nc.opts.AdminIdentifier) > 0 { go func() { @@ -511,7 +480,7 @@ func (nc *nconnect) StartServer() error { } os.Exit(0) }() - log.Println("Admin listening address:", nc.opts.AdminIdentifier+"."+t.FromAddr()) + log.Println("nConnect admin listening address:", nc.opts.AdminIdentifier+"."+t.FromAddr()) } if len(nc.opts.AdminHTTPAddr) > 0 { @@ -522,38 +491,58 @@ func (nc *nconnect) StartServer() error { } os.Exit(0) }() - log.Println("Admin web dashboard listening address:", nc.opts.AdminHTTPAddr) + log.Println("nConnect admin web dashboard serve at:", nc.opts.AdminHTTPAddr) } - nc.startSSAndTunnel() + nc.serverReady <- struct{}{} + + nc.startSSAndTunnel(false) nc.waitForSignal() return nil } -func (nc *nconnect) startSSAndTunnel() { +func (nc *nconnect) startSSAndTunnel(client bool) { + var ssConfig *ss.Config + if client { + ssConfig = nc.ssClientConfig + } else { + ssConfig = nc.ssServerConfig + } go func() { - err := ss.Start(nc.ssConfig) + err := ss.Start(ssConfig) if err != nil { log.Fatal(err) } os.Exit(0) }() - for _, t := range nc.tunnels { - go func(t *tunnel.Tunnel) { - err := t.Start() + if client { + for _, t := range nc.clientTunnels { + go func(t *tunnel.Tunnel) { + err := t.Start() + if err != nil { + log.Fatal(err) + } + os.Exit(0) + }(t) + } + } else { + go func() { + fmt.Println("nc.serverTunnel to start") + err := nc.serverTunnel.Start() if err != nil { log.Fatal(err) } os.Exit(0) - }(t) + }() } } func (nc *nconnect) waitForSignal() { + return sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-sigs } @@ -561,6 +550,225 @@ func (nc *nconnect) SetTunaNode(node *types.Node) { nc.tunaNode = node } -func (nc *nconnect) GetTunnels() []*tunnel.Tunnel { - return nc.tunnels +func (nc *nconnect) GetClientTunnels() []*tunnel.Tunnel { + nc.RLock() + defer nc.RUnlock() + iLen := len(nc.clientTunnels) + len(nc.networkTunnels) + if iLen == 0 { + return nil + } + tunnels := make([]*tunnel.Tunnel, 0, iLen) + if len(nc.clientTunnels) > 0 { + tunnels = append(tunnels, nc.clientTunnels...) + } + + for _, t := range nc.networkTunnels { + tunnels = append(tunnels, t) + } + return tunnels +} + +func (nc *nconnect) StartNetworkManager() error { + m, err := network.NewManager(nc.account, nc.clientConfig, nc.opts) + if err != nil { + return err + } + + go m.StartManager() + + err = m.StartWebServer() + if err != nil { + return err + } + + nc.waitForSignal() + + return nil +} + +func (nc *nconnect) StartNetworkMember() error { + if nc.opts.ManagerAddress == "" { + return errors.New("network manager address is not specified") + } + + mc, err := nc.getAdminClient() + if err != nil { + return err + } + nc.networkMember = network.NewMember(nc.opts, mc) + + if nc.opts.Server { + go func() { + err = nc.StartServer() + if err != nil { + log.Fatalf("StartServer err: %v", err) + } + }() + } + + if nc.opts.Client { + go func() { + err = nc.StartClient() + if err != nil { + log.Fatalf("StartClient err: %v", err) + } + }() + } + + nc.openTun() + + <-nc.serverReady // wait server tunnel is ready + serverAddr := "" + if nc.serverTunnel != nil { + serverAddr = nc.serverTunnel.FromAddr() + } + nc.networkMember.CbNodeICanAccessUpdated = nc.setupNetworkTunnel + if err = nc.networkMember.StartMember(serverAddr); err != nil { + return err + } + + return nil +} + +func (nc *nconnect) setupNetworkTunnel(nodes []*network.NodeInfo) error { + if len(nodes) == 0 || !nc.opts.Client { + return nil + } + + oldTunnels := make(map[string]struct{}) + for addr := range nc.networkTunnels { + oldTunnels[addr] = struct{}{} + } + + var cidrs []*net.IPNet + var from, to []string + for _, node := range nodes { + if node.ServerAddress == "" { + continue + } + if _, ok := oldTunnels[node.ServerAddress]; ok { + delete(oldTunnels, node.ServerAddress) + continue + } + + if nc.opts.Verbose { + log.Printf("nconnect setupNetworkTunnels to node: %+v\n", node) + } + + port, err := util.GetFreePort(0) + if err != nil { + return err + } + ssAddr := "127.0.0.1:" + strconv.Itoa(port) + + toAddr := node.ServerAddress + nc.ssClientConfig.TargetToClient[node.IP] = ssAddr + + from = append(from, ssAddr) + to = append(to, toAddr) + delete(oldTunnels, toAddr) + + r := fmt.Sprintf("%s/32", node.IP) + _, cidr, err := net.ParseCIDR(r) + if err != nil { + continue + } + cidrs = append(cidrs, cidr) + } + + var mc *nkn.MultiClient + if len(nc.clientTunnels) > 0 { + mc = nc.clientTunnels[0].MultiClient() + } + + if len(from) > 0 { + identifier := config.RandomIdentifier() + + tunnels, err := tunnel.NewTunnels(nc.account, identifier, from, to, nc.opts.Tuna, nc.tunnelConfig, mc) + if err != nil { + return err + } + + if nc.ssClientConfig.DefaultClient == "" { + nc.ssClientConfig.DefaultClient = from[0] + } + + arch.SetVPNRoutes(nc.opts.TunName, nc.opts.TunGateway, cidrs) + ss.UpdateTargetToClient(nc.ssClientConfig.TargetToClient) + + for _, tunel := range tunnels { + go func(t *tunnel.Tunnel) { + err := t.Start() + if err != nil { + log.Printf("nconnect tunnel to %v start error: %v\n", t.ToAddr(), err) + } else { + if nc.opts.Verbose { + log.Printf("nconnect tunnel to %v started\n", t.ToAddr()) + } + } + }(tunel) + + nc.Lock() + nc.networkTunnels[tunel.ToAddr()] = tunel + nc.Unlock() + } + } + + for addr := range oldTunnels { + t := nc.networkTunnels[addr] + t.Close() + delete(nc.networkTunnels, addr) + } + + return nil +} + +func (nc *nconnect) getRemoteRoutes() ([]*net.IPNet, error) { + vpnRoutes := nc.opts.VPNRoute + if nc.opts.VPN { + if len(vpnRoutes) == 0 { + for _, remoteAdminAddr := range nc.opts.RemoteAdminAddr { + remoteInfo, err := nc.getRemoteInfo(remoteAdminAddr) + if err != nil { + log.Printf("getRemoteInfo %v err: %v", remoteAdminAddr, err) + continue + } + if len(remoteInfo.LocalIP.Ipv4) > 0 { + vpnRoutes = make([]string, 0, len(remoteInfo.LocalIP.Ipv4)) + for _, ip := range remoteInfo.LocalIP.Ipv4 { + if ip == nc.opts.TunAddr || ip == nc.opts.TunGateway { + log.Printf("Skipping server's local IP %s in routes", ip) + continue + } + vpnRoutes = append(vpnRoutes, fmt.Sprintf("%s/32", ip)) + } + } + } + } + } + + var routeCIDRs []*net.IPNet + if len(vpnRoutes) > 0 { + routeCIDRs = make([]*net.IPNet, len(vpnRoutes)) + for i, r := range vpnRoutes { + _, cidr, err := net.ParseCIDR(r) + if err != nil { + return nil, fmt.Errorf("parse CIDR %s error: %v", r, err) + } + routeCIDRs[i] = cidr + } + } + + return routeCIDRs, nil +} + +func (nc *nconnect) openTun() { + nc.openTunOnce.Do(func() { + err := arch.OpenTun(nc.opts.TunName, nc.opts.TunAddr, nc.opts.TunGateway, nc.opts.TunMask, nc.opts.TunDNS[0], nc.opts.LocalSocksAddr) + if err != nil { + log.Printf("OpenTun error: %v", err) + } else { + log.Println("Started tun2socks") + } + }) } diff --git a/network/manager.go b/network/manager.go new file mode 100644 index 0000000..a282531 --- /dev/null +++ b/network/manager.go @@ -0,0 +1,602 @@ +package network + +import ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net" + "os" + "sync" + "time" + + "github.com/nknorg/nconnect/admin" + "github.com/nknorg/nconnect/config" + "github.com/nknorg/nkn-sdk-go" +) + +const ( + defaultDomain = "nconnect.nkn" + defaultIpStart = "10.20.30.1" + defaultIpEnd = "10.20.30.254" + defaultNetmask = "255.255.255.0" + defaultGateway = "10.20.30.1" + defaultDNS = "1.1.1.1" + + networkDataFile = "network.json" + + AllMembers = "allMembers" +) + +type networkData struct { + NetworkInfo *networkInfo `json:"networkInfo"` + IpStart string `json:"ipStart"` // start ip of the network + IpEnd string `json:"ipEnd"` // end ip of the network + Netmask string `json:"netmask"` // mask of the network + NextIp string `json:"nextIp"` // next available ip + + Waiting map[string]*NodeInfo `json:"waiting"` // nodes waiting for authorization, map address to node info + Member map[string]*NodeInfo `json:"member"` // authorized member list, map address to node info + AcceptAddress map[string][]string `json:"acceptAddress"` // accept address data for each member. map address to list of accepted address + NameToAddress map[string]string `json:"nameToAddress"` // map name to address +} + +type Manager struct { + opts *config.Opts + c *admin.Client + + sync.RWMutex + networkData *networkData // persisted data that will be saved to disk +} + +var manager *Manager + +func NewManager(account *nkn.Account, clientConfig *nkn.ClientConfig, opts *config.Opts) (*Manager, error) { + if manager != nil { + return manager, nil + } + + manager = &Manager{opts: opts} + err := manager.loadNetworkData() + if err != nil { + return nil, err + } + + if len(opts.Identifier) == 0 { + return nil, errors.New("network manager's identifier should not be empty") + } + + manager.c, err = admin.NewClient(account, clientConfig, opts.Identifier) + if err != nil { + return nil, err + } + + return manager, nil +} + +func (m *Manager) StartManager() error { + log.Println("nConnect network manager is listening at:", m.c.MultiClient.Address()) + + for { + msg := <-m.c.MultiClient.OnMessage.C + resp, err := m.handleRequest(msg) + if err != nil { + log.Println("nConnect manager handle request error", err) + continue + } + + b, err := json.Marshal(resp) + if err != nil { + log.Println("nConnect manager json.Marshal resp error", err) + continue + } + + err = msg.Reply(b) + if err != nil { + log.Println("nConnect manager msg.Reply error", err) + continue + } + } +} + +func (m *Manager) handleRequest(msg *nkn.Message) (*managerToMember, error) { + req := &memberToManager{} + err := json.Unmarshal(msg.Data, req) + if err != nil { + return nil, err + } + + var node *NodeInfo + resp := &managerToMember{} + resp.MsgType = req.MsgType + + switch req.MsgType { + case JOIN_NETWORK: + resp.NetworkInfo = m.networkData.NetworkInfo + node, err = m.JoinNetwork(msg.Src, req.Name, req.ServerAddress) + if node != nil { + resp.NodeInfo = append(resp.NodeInfo, node) + } + + case LEAVE_NETWORK: + err = m.LeaveNetwork(msg.Src, req.Name) + + case GET_MY_INFO: + resp.NetworkInfo = m.networkData.NetworkInfo + if n := m.GetNodeInfo(msg.Src); n != nil { + resp.NodeInfo = append(resp.NodeInfo, n) + } else { + err = errors.New(errNodeNotFound) + } + + case GET_NODES_I_ACCEPT: + list := m.GetAcceptNodes(msg.Src) + resp.NodeInfo = list + + case GET_NODES_I_CAN_ACCESS: + list := m.GetNodesICanAccess(msg.Src) + resp.NodeInfo = list + + case UPDATE_SERVER_ADDRESS: + err = m.SetNodeServerAddress(msg.Src, req.ServerAddress) + + default: + return nil, fmt.Errorf("nConnect manager got unknown message type: %v", req.MsgType) + } + + if err != nil { + resp.Err = err.Error() + } + + return resp, nil +} + +func (m *Manager) JoinNetwork(address, name, serverAddr string) (*NodeInfo, error) { + if node, ok := m.networkData.Member[address]; ok { + node.LastSeen = time.Now() + if name != "" { + node.Name = name + } + if serverAddr != "" { + node.ServerAddress = serverAddr + } + m.networkData.Member[address] = node + if err := m.saveNetworkData(); err != nil { + return nil, err + } + + notification := &managerToMember{ + MsgType: NOTI_MEMBER_ONLINE, + NodeInfo: []*NodeInfo{node}, + } + _, err := SendMsg(m.c, address, notification, false) // response to requestor + if err != nil { + return nil, err + } + // broadcast member online event to related members + m.NotifyIAccept(address, notification) + + return node, nil + } + + m.Lock() + defer m.Unlock() + if node, ok := m.networkData.Waiting[address]; ok { + changed := false + if name != "" && name != node.Name { + node.Name = name + changed = true + } + if serverAddr != "" && serverAddr != node.ServerAddress { + node.ServerAddress = serverAddr + changed = true + } + if changed { + m.networkData.Waiting[address] = node + if err := m.saveNetworkData(); err != nil { + return nil, err + } + } + + return nil, errors.New(errWaitForAuth) + } + + if _, nameExists := m.networkData.NameToAddress[name]; nameExists { + return nil, errors.New(errNameExist) + } + + if len(name) > 0 { + m.networkData.NameToAddress[name] = address + } + m.networkData.Waiting[address] = &NodeInfo{Name: name, Address: address, ServerAddress: address, LastSeen: time.Now()} + if err := m.saveNetworkData(); err != nil { + return nil, err + } + + return nil, errors.New(errWaitForAuth) +} + +func (m *Manager) LeaveNetwork(address, name string) error { + m.Lock() + defer m.Unlock() + delete(m.networkData.NameToAddress, name) + if _, ok := m.networkData.Member[address]; ok { + delete(m.networkData.Member, address) + + for _, n := range m.networkData.Member { + acceptAddrs := m.networkData.AcceptAddress[n.Address] + for i, addr := range acceptAddrs { + if addr == address { + acceptAddrs = append(acceptAddrs[:i], acceptAddrs[i+1:]...) + m.networkData.AcceptAddress[n.Address] = acceptAddrs + break + } + } + } + + notification := &managerToMember{ + MsgType: NOTI_LEAVE_NETWORK, + NodeInfo: []*NodeInfo{{Name: name, Address: address}}, + } + m.NotifyIAccept(address, notification) + m.NotifyICanAccess(address, notification) + + } else if _, ok := m.networkData.Waiting[address]; ok { + delete(m.networkData.Waiting, address) + } + + return m.saveNetworkData() +} + +func (m *Manager) AuthorizeMemeber(n *NodeInfo) error { + m.RLock() + nw, ok := m.networkData.Waiting[n.Address] + m.RUnlock() + + if !ok { + return errors.New(errNodeNotFound) + } + + ip, err := m.GetAvailableIp() + if err != nil { + return err + } + nw.IP = ip + nw.Netmask = m.networkData.Netmask + + m.Lock() + m.networkData.Member[n.Address] = nw + delete(m.networkData.Waiting, n.Address) + m.Unlock() + + if err = m.saveNetworkData(); err != nil { + return err + } + + notification := &managerToMember{ + MsgType: NOTI_AUTHORIZED, + NetworkInfo: m.networkData.NetworkInfo, + NodeInfo: []*NodeInfo{nw}, + } + + if _, err = SendMsg(m.c, n.Address, notification, false); err != nil { + return err + } + + m.NotifyIAccept(n.Address, &managerToMember{MsgType: NOTI_SET_ACCEPT}) + + return nil +} + +func (m *Manager) DeleteWaiting(n *NodeInfo) error { + m.Lock() + delete(m.networkData.Waiting, n.Address) + m.Unlock() + + return m.saveNetworkData() +} + +func (m *Manager) UnauthorizeMemeber(n *NodeInfo) error { + m.Lock() + defer m.Unlock() + + if nw, ok := m.networkData.Member[n.Address]; ok { + m.networkData.Waiting[n.Address] = nw + delete(m.networkData.Member, n.Address) + + err := m.saveNetworkData() + if err != nil { + return err + } + + for _, node := range m.networkData.Member { + changed := false + acceptAddresses := m.networkData.AcceptAddress[node.Address] + if len(acceptAddresses) > 0 && acceptAddresses[0] == AllMembers { + changed = true + } else { + for index, addr := range acceptAddresses { + if addr == n.Address { + acceptAddresses = append(acceptAddresses[:index], acceptAddresses[index+1:]...) + m.networkData.AcceptAddress[node.Address] = acceptAddresses + changed = true + break + } + } + } + if changed { + notification := &managerToMember{ + MsgType: NOTI_SET_ACCEPT, + } + if _, err = SendMsg(m.c, node.Address, notification, false); err != nil { + return err + } + } + } + } + + return nil +} + +func (m *Manager) SetNodeServerAddress(address, serverAddress string) error { + m.Lock() + defer m.Unlock() + + if serverAddress == "" { + return nil + } + + if node, ok := m.networkData.Member[address]; ok && node.ServerAddress != serverAddress { + node.ServerAddress = serverAddress + m.networkData.Member[address] = node + return m.saveNetworkData() + } + + if node, ok := m.networkData.Waiting[address]; ok && node.ServerAddress != serverAddress { + node.ServerAddress = serverAddress + m.networkData.Waiting[address] = node + return m.saveNetworkData() + } + + return nil +} + +func (m *Manager) GetAcceptAddress(address string) []string { + m.RLock() + defer m.RUnlock() + list := m.networkData.AcceptAddress[address] + return list +} + +func (m *Manager) GetAcceptNodes(address string) []*NodeInfo { + m.RLock() + defer m.RUnlock() + + addressList := m.networkData.AcceptAddress[address] + var list []*NodeInfo + if len(addressList) > 0 && addressList[0] == AllMembers { + for _, n := range m.networkData.Member { + if n.Address != address { + list = append(list, n) + } + } + } else { + for _, a := range addressList { + if n, ok := m.networkData.Member[a]; ok { + list = append(list, n) + } + } + } + + return list +} + +func (m *Manager) GetNodesICanAccess(address string) []*NodeInfo { + m.RLock() + defer m.RUnlock() + + if _, ok := m.networkData.Member[address]; !ok { // not a member + return nil + } + + var list []*NodeInfo + for addr, acceptAddress := range m.networkData.AcceptAddress { + if addr == address { + continue + } + + if len(acceptAddress) > 0 && acceptAddress[0] == AllMembers { + if n, ok := m.networkData.Member[addr]; ok { + list = append(list, n) + } + continue + } + + for _, a := range acceptAddress { + if a == address { + if n, ok := m.networkData.Member[addr]; ok { + list = append(list, n) + } + } + } + } + + return list +} + +func (m *Manager) SetAcceptAddress(address string, acceptAddress []string) error { + m.Lock() + m.networkData.AcceptAddress[address] = acceptAddress + m.Unlock() + + if err := m.saveNetworkData(); err != nil { + return err + } + notification := &managerToMember{MsgType: NOTI_SET_ACCEPT} + if _, err := SendMsg(m.c, address, notification, false); err != nil { + return err + } + + m.RLock() + defer m.RUnlock() + + n := m.networkData.Member[address] + notification = &managerToMember{MsgType: NOTI_NEW_MEMBER, NodeInfo: []*NodeInfo{n}} + m.NotifyIAccept(address, notification) + + return nil +} + +func (m *Manager) NotifyIAccept(initiatorAddr string, notification *managerToMember) error { + m.RLock() + acceptAddress := m.networkData.AcceptAddress[initiatorAddr] + m.RUnlock() + + if len(acceptAddress) > 0 && acceptAddress[0] == AllMembers { + for _, n := range m.networkData.Member { + if n.Address != initiatorAddr { + if _, err := SendMsg(m.c, n.Address, notification, false); err != nil { + log.Printf("Send msg type %v to %v error %v\n", notification.MsgType, n.Address, err) + } + } + } + } else { + for _, addr := range acceptAddress { + if _, err := SendMsg(m.c, addr, notification, false); err != nil { + log.Printf("Send msg type %v to %v error %v\n", notification.MsgType, addr, err) + } + } + } + + return nil +} + +func (m *Manager) NotifyICanAccess(initiatorAddr string, notification *managerToMember) error { + for _, n := range m.networkData.Member { + if n.Address == initiatorAddr { + continue + } + + acceptAddr := m.networkData.AcceptAddress[n.Address] + if len(acceptAddr) > 0 && acceptAddr[0] == AllMembers { + if _, err := SendMsg(m.c, n.Address, notification, false); err != nil { + log.Printf("Send msg type %v to %v error %v\n", notification.MsgType, n.Address, err) + } + } else { + for _, addr := range acceptAddr { // broadcast accept info to nodes + if addr == initiatorAddr { + if _, err := SendMsg(m.c, n.Address, notification, false); err != nil { + log.Printf("Send msg type %v to %v error %v\n", notification.MsgType, addr, err) + } + } + } + } + } + + return nil +} + +func (m *Manager) GetNodeInfo(address string) *NodeInfo { + m.RLock() + defer m.RUnlock() + if n, ok := m.networkData.Member[address]; ok { + return n + } + return nil +} + +func (m *Manager) GetNetworkConfig() *networkData { + m.RLock() + defer m.RUnlock() + return m.networkData +} + +func (m *Manager) SetNetworkConfig(conf *networkData) error { + m.Lock() + defer m.Unlock() + m.networkData.NetworkInfo = conf.NetworkInfo + m.networkData.IpStart = conf.IpStart + m.networkData.IpEnd = conf.IpEnd + m.networkData.Netmask = conf.Netmask + + return m.saveNetworkData() +} + +func (m *Manager) GetAvailableIp() (string, error) { + m.Lock() + defer m.Unlock() + if m.networkData.NextIp == "" { + return "", errors.New("nConnect manager has no available ip") + } + + ip := m.networkData.NextIp + m.networkData.NextIp = int2ip(ip2int(m.networkData.NextIp) + 1) + if m.networkData.NextIp == m.networkData.IpEnd { + m.networkData.NextIp = "" + } + + return ip, nil +} + +func (m *Manager) loadNetworkData() error { + m.Lock() + defer m.Unlock() + + nwData := &networkData{ + Waiting: make(map[string]*NodeInfo), + Member: make(map[string]*NodeInfo), + AcceptAddress: make(map[string][]string), + NameToAddress: make(map[string]string), + } + m.networkData = nwData + + jsonFile, err := os.OpenFile(networkDataFile, os.O_CREATE|os.O_RDONLY, os.ModePerm) + if err != nil { + return err + } + + defer jsonFile.Close() + + b, err := io.ReadAll(jsonFile) + if err != nil { + return err + } + + if len(b) > 0 { + return json.Unmarshal(b, nwData) + } else { // set default value + nwData.IpStart = defaultIpStart // IpStart is reserved for manager + nwData.IpEnd = defaultIpEnd + nwData.Netmask = defaultNetmask + ipNext := int2ip(ip2int(defaultIpStart) + 1) + nwData.NextIp = ipNext + nwData.NetworkInfo = &networkInfo{Domain: defaultDomain, Gateway: defaultGateway, DNS: defaultDNS} + return m.saveNetworkData() + } +} + +func (m *Manager) saveNetworkData() error { + if m.networkData == nil { + return errors.New("networkData is nil") + } + + b, err := json.Marshal(m.networkData) + if err != nil { + return err + } + + return os.WriteFile(networkDataFile, b, os.ModePerm) +} + +func ip2int(ip string) uint32 { + s := net.ParseIP(ip).To4() + return binary.BigEndian.Uint32(s) +} + +func int2ip(n uint32) string { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, n) + return ip.String() +} diff --git a/network/member.go b/network/member.go new file mode 100644 index 0000000..78f4e2f --- /dev/null +++ b/network/member.go @@ -0,0 +1,408 @@ +package network + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net" + "os" + "strings" + + "github.com/nknorg/nconnect/admin" + "github.com/nknorg/nconnect/arch" + "github.com/nknorg/nconnect/config" + "github.com/nknorg/nconnect/util" + "github.com/nknorg/nkn-sdk-go" + tunnel "github.com/nknorg/nkn-tunnel" +) + +const ( + memberFile = "member.json" +) + +var ( + errNoDataInFile = "no data in file" + errWaitForAuth = "wait for authorization" + errNameExist = "name already exists" + errNodeNotFound = "node not found" +) + +type memberNetworkData struct { + NetworkInfo *networkInfo `json:"networkInfo"` + NodeInfo *NodeInfo `json:"NodeInfo"` + NodesIAccept []*NodeInfo `json:"nodesIAccept"` // node info of nodes that this node accepts + NodesICanAccess []*NodeInfo `json:"nodesICanAccess"` // node info of nodes that this node can access +} + +type callbackNodeICanAccessUpdated func(nodes []*NodeInfo) error + +type Member struct { + opts *config.Opts + c *admin.Client + networkData memberNetworkData // node info of this node + serverAddress string // server address + serverTunnel *tunnel.Tunnel + joinedNetwork bool + CbNodeICanAccessUpdated callbackNodeICanAccessUpdated +} + +func NewMember(opts *config.Opts, c *admin.Client) *Member { + return &Member{opts: opts, c: c, networkData: memberNetworkData{NetworkInfo: &networkInfo{}, NodeInfo: &NodeInfo{}}} +} + +func (m *Member) StartMember(serverAddress string) error { + m.serverAddress = serverAddress + + err := m.loadMemberData() + if err != nil && err.Error() != errNoDataInFile { + return err + } + + if err = m.JoinNetwork(serverAddress); err != nil { + return err + } + + if m.joinedNetwork { + if _, err = m.GetNodeICanAccess(); err != nil { + return err + } + } + + log.Println("nConnect Network member is listening at:", m.c.Address()) + for { + msg := <-m.c.OnMessage.C + + resp := &managerToMember{} + err := json.Unmarshal(msg.Data, resp) + if err != nil { + log.Println("Network member, received multiclient msg, unmarshal msg.Data error: ", err) + continue + } + + err = m.handleNotification(resp) + if err != nil { + log.Println(err) + continue + } + } +} + +// handle notification from manager +func (m *Member) handleNotification(notification *managerToMember) error { + switch notification.MsgType { + case NOTI_AUTHORIZED: + if len(notification.NodeInfo) > 0 { + m.networkData.NodeInfo = notification.NodeInfo[0] + if m.serverAddress != "" && m.networkData.NodeInfo.ServerAddress != m.serverAddress { + err := m.SetServerTunnel(m.serverTunnel) + if err != nil { + return err + } + m.networkData.NodeInfo.ServerAddress = m.serverAddress + } + if err := m.saveMemberData(); err != nil { + return err + } + if err := arch.SetTunIp(m.opts.TunName, m.networkData.NodeInfo.IP, m.networkData.NodeInfo.Netmask); err != nil { + log.Printf("Failed to set network ip: %v", err) + } + m.GetNodeICanAccess() + } + + case NOTI_NEW_MEMBER: + m.GetNodeICanAccess() + case NOTI_SET_ACCEPT: + m.GetNodeIAccept() + case NOTI_MEMBER_ONLINE: + if m.CbNodeICanAccessUpdated != nil { + m.GetNodeICanAccess() + m.CbNodeICanAccessUpdated(m.networkData.NodesICanAccess) + } + + default: + return fmt.Errorf("nConnect member got unknown notification type: %v", notification.MsgType) + } + + return nil +} + +func (m *Member) JoinNetwork(serverAddr string) error { + if serverAddr == "" { + serverAddr = m.networkData.NodeInfo.ServerAddress + } else { + if m.networkData.NodeInfo.ServerAddress != serverAddr { + m.networkData.NodeInfo.ServerAddress = serverAddr + m.saveMemberData() + } + } + + msg := memberToManager{MsgType: JOIN_NETWORK, Name: m.opts.NodeName, ServerAddress: serverAddr} + resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true) + if err != nil { + return err + } + + if resp.Err == errWaitForAuth { + m.networkData.NetworkInfo = resp.NetworkInfo + m.saveMemberData() + return nil + } else if resp.Err == errNameExist { + return errors.New(errNameExist) + } + + if resp.Err != "" { + return errors.New(resp.Err) + } + + if len(resp.NodeInfo) > 0 { + m.networkData.NodeInfo = resp.NodeInfo[0] + m.networkData.NetworkInfo = resp.NetworkInfo + m.saveMemberData() + if m.networkData.NodeInfo.IP != "" { + m.joinedNetwork = true + if err = arch.SetTunIp(m.opts.TunName, m.networkData.NodeInfo.IP, m.networkData.NodeInfo.Netmask); err != nil { + return err + } else { + log.Printf("Joined network ip: %v, mask: %v", m.networkData.NodeInfo.IP, m.networkData.NodeInfo.Netmask) + } + } + } + + return nil +} + +func (m *Member) LeaveNetwork() error { + msg := memberToManager{MsgType: LEAVE_NETWORK, Name: m.opts.NodeName} + resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true) + if err != nil { + return err + } + if resp.Err != "" { + return errors.New(resp.Err) + } + + m.networkData = memberNetworkData{} + return m.saveMemberData() +} + +func (m *Member) GetMyInfo() (*NodeInfo, error) { + msg := memberToManager{MsgType: GET_MY_INFO} + + resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true) + if err != nil { + return nil, err + } + if resp.Err != "" { + return nil, errors.New(resp.Err) + } + + if len(resp.NodeInfo) > 0 { + m.networkData.NodeInfo = resp.NodeInfo[0] + if err = m.saveMemberData(); err != nil { + return nil, err + } + if err = arch.SetTunIp(m.opts.TunName, m.networkData.NodeInfo.IP, m.networkData.NodeInfo.Netmask); err != nil { + return nil, err + } + } + + return m.networkData.NodeInfo, err +} + +func (m *Member) SetServerTunnel(t *tunnel.Tunnel) error { + m.serverTunnel = t + serverAddress := t.FromAddr() + m.serverAddress = serverAddress + err := m.GetNodeIAccept() + if err != nil { + return err + } + if m.networkData.NodeInfo != nil && serverAddress == m.networkData.NodeInfo.ServerAddress { + return nil + } + + msg := memberToManager{MsgType: UPDATE_SERVER_ADDRESS, ServerAddress: serverAddress} + _, err = SendMsg(m.c, m.opts.ManagerAddress, &msg, false) + if err != nil { + return err + } + + if m.networkData.NodeInfo == nil { + return nil + } + + m.networkData.NodeInfo.ServerAddress = serverAddress + return m.saveMemberData() +} + +func (m *Member) GetNodeIAccept() error { + msg := memberToManager{MsgType: GET_NODES_I_ACCEPT} + resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true) + if err != nil { + return err + } + if resp.Err != "" { + return errors.New(resp.Err) + } + + if len(resp.NodeInfo) == 0 { + return nil + } + + m.networkData.NodesIAccept = resp.NodeInfo + if err = m.saveMemberData(); err != nil { + return err + } + + var addrs []string + for _, node := range m.networkData.NodesIAccept { + arr := strings.Split(node.Address, ".") + addrs = append(addrs, arr[len(arr)-1]+"$") + } + + if len(addrs) > 0 { + m.opts.Config.AddAcceptAddrs(addrs) + if m.serverTunnel != nil { + m.serverTunnel.SetAcceptAddrs(nkn.NewStringArray(m.opts.Config.GetAcceptAddrs()...)) + } + } + + return nil +} + +func (m *Member) GetNodeICanAccess() ([]*NodeInfo, error) { + msg := memberToManager{MsgType: GET_NODES_I_CAN_ACCESS, Name: m.opts.NodeName} + resp, err := SendMsg(m.c, m.opts.ManagerAddress, &msg, true) + if err != nil { + return nil, err + } + if resp.Err != "" { + return nil, errors.New(resp.Err) + } + + if len(resp.NodeInfo) > 0 { + m.networkData.NodesICanAccess = resp.NodeInfo + if err = m.saveMemberData(); err != nil { + return nil, err + } + + if m.CbNodeICanAccessUpdated != nil { + m.CbNodeICanAccessUpdated(m.networkData.NodesICanAccess) + } + } + + return m.networkData.NodesICanAccess, nil +} + +func (m *Member) GetAddressByIp(ip string) (string, error) { + for _, node := range m.networkData.NodesICanAccess { + if node.IP == ip { + return node.Address, nil + } + } + return "", errors.New(errNodeNotFound) +} + +func (m *Member) loadMemberData() error { + jsonFile, err := os.OpenFile(memberFile, os.O_CREATE|os.O_RDONLY, os.ModePerm) + if err != nil { + return err + } + + defer jsonFile.Close() + + b, err := io.ReadAll(jsonFile) + if err != nil { + return err + } + + data := memberNetworkData{NetworkInfo: &networkInfo{}, NodeInfo: &NodeInfo{}} + if len(b) == 0 { + return errors.New(errNoDataInFile) + } + + if err = json.Unmarshal(b, &data); err != nil { + return err + } + if data.NetworkInfo != nil { + m.networkData.NetworkInfo = data.NetworkInfo + } + if data.NodeInfo != nil { + m.networkData.NodeInfo = data.NodeInfo + } + + return nil +} + +func (m *Member) saveMemberData() error { + b, err := json.Marshal(m.networkData) + if err != nil { + return err + } + + return os.WriteFile(memberFile, b, os.ModePerm) +} + +func (m *Member) SetRoutes() error { + routes := make([]string, 0, len(m.networkData.NodesIAccept)) + for _, n := range m.networkData.NodesIAccept { + routes = append(routes, fmt.Sprintf("%s/32", n.IP)) + } + + ipNets := make([]*net.IPNet, len(routes)) + if len(routes) > 0 { + for i, cidr := range routes { + _, cidr, err := net.ParseCIDR(cidr) + if err != nil { + return fmt.Errorf("parse CIDR %s error: %v", cidr, err) + } + ipNets[i] = cidr + } + } + for _, dest := range ipNets { + log.Printf("Adding route %s", dest) + out, err := arch.AddRouteCmd(dest, m.networkData.NetworkInfo.Gateway, m.opts.TunName) + if len(out) > 0 { + os.Stdout.Write(out) + } + if err != nil { + os.Stdout.Write([]byte(util.ParseExecError(err))) + os.Exit(1) + } + } + + return nil +} + +func (m *Member) DeleteRoutes() error { + routes := make([]string, 0, len(m.networkData.NodesIAccept)) + for _, n := range m.networkData.NodesIAccept { + routes = append(routes, fmt.Sprintf("%s/32", n.IP)) + } + + ipNets := make([]*net.IPNet, len(routes)) + if len(routes) > 0 { + for i, cidr := range routes { + _, cidr, err := net.ParseCIDR(cidr) + if err != nil { + return fmt.Errorf("parse CIDR %s error: %v", cidr, err) + } + ipNets[i] = cidr + } + } + + for _, dest := range ipNets { + log.Printf("Deleting route %s", dest) + out, err := arch.DeleteRouteCmd(dest, m.networkData.NetworkInfo.Gateway, m.opts.TunName) + if len(out) > 0 { + os.Stdout.Write(out) + } + if err != nil { + os.Stdout.Write([]byte(util.ParseExecError(err))) + } + } + + return nil +} diff --git a/network/message.go b/network/message.go new file mode 100644 index 0000000..58b2946 --- /dev/null +++ b/network/message.go @@ -0,0 +1,67 @@ +package network + +import ( + "encoding/json" + "time" + + "github.com/nknorg/nconnect/admin" +) + +// msgType constants +const ( + MT_NONE = iota + JOIN_NETWORK + UPDATE_MY_INFO + GET_MY_INFO + UPDATE_SERVER_ADDRESS + GET_NODES_I_ACCEPT + GET_NODES_I_CAN_ACCESS + LEAVE_NETWORK + + NOTI_AUTHORIZED + NOTI_NEW_MEMBER + NOTI_SET_ACCEPT + NOTI_MEMBER_ONLINE + NOTI_LEAVE_NETWORK +) + +type NodeInfo struct { + IP string `json:"ip"` + Netmask string `json:"netmask"` + Name string `json:"name"` + Address string `json:"address"` // client address + ServerAddress string `json:"serverAddress"` // nconnect server listen address + LastSeen time.Time `json:"lastSeen"` +} + +type networkInfo struct { + Domain string `json:"domain"` + Gateway string `json:"gateway"` + DNS string `json:"dns"` +} + +type memberToManager struct { + MsgType int `json:"msgType"` + Name string `json:"name"` + ServerAddress string `json:"serverAddress"` +} + +type managerToMember struct { + MsgType int `json:"msgType"` + Err string `json:"err"` + NetworkInfo *networkInfo `json:"networkInfo"` + NodeInfo []*NodeInfo `json:"nodeInfo"` +} + +func SendMsg(mc *admin.Client, address string, msg interface{}, waitResponse bool) (*managerToMember, error) { + reply, err := mc.SendMsg(address, msg, waitResponse) + if err != nil || !waitResponse { + return nil, err + } + + var respMsg managerToMember + if err = json.Unmarshal(reply.Data, &respMsg); err != nil { + return nil, err + } + return &respMsg, nil +} diff --git a/network/webservice.go b/network/webservice.go new file mode 100644 index 0000000..689e82f --- /dev/null +++ b/network/webservice.go @@ -0,0 +1,124 @@ +package network + +import ( + "log" + "net/http" + "path" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/nknorg/nconnect/admin" + "github.com/nknorg/nconnect/util" +) + +const ( + success = "success" +) + +type addresses struct { + Address string `json:"address"` + AcceptAddresses []string `json:"acceptAddresses"` +} + +func (m *Manager) StartWebServer() error { + gin.SetMode(gin.ReleaseMode) + + r := gin.Default() + + // This is for development, when start web page with "yarn dev" at ../web/src + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"http://localhost:3000"}, + AllowMethods: []string{"POST", "OPTIONS"}, + AllowHeaders: []string{"Content-Type,access-control-allow-origin, access-control-allow-headers"}, + })) + + r.POST("/rpc/network", func(c *gin.Context) { + req := &admin.RpcReq{} + if err := c.ShouldBindJSON(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp := m.handleWebRequest(req) + if m.opts.Verbose { + log.Printf("Web request %v, response %+v\n", req.Method, resp) + } + + c.JSON(http.StatusOK, resp) + }) + + r.StaticFile("/network", path.Join(m.opts.WebRootPath, "network.html")) + r.StaticFile("/favicon.ico", path.Join(m.opts.WebRootPath, "favicon.ico")) + r.StaticFile("/sw.js", path.Join(m.opts.WebRootPath, "sw.js")) + r.Static("/static", path.Join(m.opts.WebRootPath, "static")) + r.Static("/_nuxt", path.Join(m.opts.WebRootPath, "_nuxt")) + r.Static("/img", path.Join(m.opts.WebRootPath, "img")) + r.Static("/zh", path.Join(m.opts.WebRootPath, "zh")) + r.Static("/zh-TW", path.Join(m.opts.WebRootPath, "zh-TW")) + + log.Println("Network manager web serve at ", "http://"+m.opts.AdminHTTPAddr+"/network") + return r.Run(m.opts.AdminHTTPAddr) +} + +func (m *Manager) handleWebRequest(req *admin.RpcReq) *admin.RpcResp { + resp := &admin.RpcResp{} + var err error + + switch req.Method { + case "getNetworkConfig": + resp.Result = m.GetNetworkConfig() + + case "setNetworkConfig": + params := &networkData{} + if err = util.JSONConvert(req.Params, params); err != nil { + break + } + err = m.SetNetworkConfig(params) + resp.Result = success + + case "authorizeMember": + params := &NodeInfo{} + if err = util.JSONConvert(req.Params, params); err != nil { + break + } + m.AuthorizeMemeber(params) + + resp.Result = success + + case "unauthorizeMember": + params := &NodeInfo{} + if err = util.JSONConvert(req.Params, params); err != nil { + break + } + m.UnauthorizeMemeber(params) + + resp.Result = success + + case "deleteWaiting": + params := &NodeInfo{} + if err = util.JSONConvert(req.Params, params); err != nil { + break + } + m.UnauthorizeMemeber(params) + + resp.Result = success + + case "setAcceptAddress": + params := &addresses{} + if err = util.JSONConvert(req.Params, params); err != nil { + break + } + + m.SetAcceptAddress(params.Address, params.AcceptAddresses) + resp.Result = success + + default: + resp.Error = "nConnect network webservice got unknown method" + } + + if err != nil { + resp.Error = err.Error() + } + + return resp +} diff --git a/ss/multiconn.go b/ss/multiconn.go index 4486742..7b55b82 100644 --- a/ss/multiconn.go +++ b/ss/multiconn.go @@ -16,10 +16,14 @@ func getClient(target string) string { routes.RLock() defer routes.RUnlock() - server, ok := routes.TargetToClient[tgtIp[0]] - - if ok { + if server, ok := routes.TargetToClient[tgtIp[0]]; ok { return server } return routes.DefaultClient } + +func UpdateTargetToClient(targetToClient map[string]string) { + routes.Lock() + defer routes.Unlock() + routes.TargetToClient = targetToClient +} diff --git a/ss/tcp.go b/ss/tcp.go index 22af00b..02ee878 100644 --- a/ss/tcp.go +++ b/ss/tcp.go @@ -57,7 +57,7 @@ func tcpLocal(addr, server string, shadow func(net.Conn) net.Conn, getAddr func( continue } if err != nil { - logf("UDP Associate End.") + // logf("UDP Associate End.") return } } diff --git a/ss/udp.go b/ss/udp.go index 356be15..67e43c7 100644 --- a/ss/udp.go +++ b/ss/udp.go @@ -23,6 +23,9 @@ const udpBufSize = 64 * 1024 // Listen on laddr for UDP packets, encrypt and send to server to reach target. func udpLocal(laddr, server, target string, shadow func(net.PacketConn) net.PacketConn) error { server = getClient(target) + if server == "" { + return nil // fmt.Errorf("UDP target address error: invalid target address: %q", target) + } srvAddr, err := net.ResolveUDPAddr("udp", server) if err != nil { return fmt.Errorf("UDP server address error: %v", err) @@ -75,7 +78,7 @@ func udpLocal(laddr, server, target string, shadow func(net.PacketConn) net.Pack func udpSocksLocal(laddr, server string, shadow func(net.PacketConn) net.PacketConn) error { c, err := net.ListenPacket("udp", laddr) if err != nil { - return fmt.Errorf("UDP local listen error: %v", err) + return fmt.Errorf("UDP Socks local listen error: %v", err) } defer c.Close() @@ -96,13 +99,17 @@ func udpSocksLocal(laddr, server string, shadow func(net.PacketConn) net.PacketC logf("UDP local listen error: %v", err) continue } - logf("UDP socks tunnel %s <-> %s <-> %s", laddr, server, socks.Addr(buf[3:])) + // logf("UDP socks tunnel %s <-> %s <-> %s", laddr, server, socks.Addr(buf[3:])) pc = shadow(pc) nm.Add(raddr, c, pc, socksClient) } dest := socks.Addr(buf[3:]) server = getClient(dest.String()) + if server == "" { + // logf("UDP target address error: invalid target address: %q", dest) + continue + } srvAddr, err := net.ResolveUDPAddr("udp", server) if err != nil { return fmt.Errorf("UDP server address error: %v", err) diff --git a/tests/config.manager.json b/tests/config.manager.json new file mode 100644 index 0000000..f83356e --- /dev/null +++ b/tests/config.manager.json @@ -0,0 +1,20 @@ +{ + "Client": false, + "Server": false, + "NetworkManager": true, + "NetworkMember": false, + "seed": "cb53701c451d0344943e0d15e1c84025742c993fd3e2c4768c0e3a211d381087", + "identifier": "manager", + "remoteAdminAddr": [], + "localSocksAddr": "", + "tuna": true, + "udp": true, + "adminIdentifier": "nConnect", + "webRootPath": "../web/dist", + "acceptAddrs": [], + "adminAddrs": [], + + "AdminHTTPAddr": "127.0.0.1:8001", + "nodeName": "bill", + "managerAddress": "manager.0ec192083aaf67d1bf44ea862858a457c9b864b4d4416b647552e2ebcad2facb" +} \ No newline at end of file diff --git a/tests/main_test.go b/tests/main_test.go index 68a9edc..f042786 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -11,6 +11,10 @@ import ( "github.com/nknorg/tuna/types" ) +const ( + tunaIp = "127.0.0.1" // "147.182.210.189" // DO No.9 test server +) + var remoteTuna = flag.Bool("remoteTuna", false, "use remote tuna nodes") var tun = flag.Bool("tun", false, "use tun device") @@ -23,7 +27,7 @@ func TestMain(m *testing.M) { } go func() { - err := StartTcpServer() + err := StartTcpServer(tcpPort) if err != nil { log.Fatalf("StartTcpServer err %v", err) return @@ -47,7 +51,7 @@ func TestMain(m *testing.M) { var tunaNode *types.Node var err error if !(*remoteTuna) { - tunaNode, err = getTunaNode() + tunaNode, err = getTunaNode(tunaIp) if err != nil { log.Fatalf("getTunaNode err %v", err) return diff --git a/tests/pub.go b/tests/pub.go index faf48d7..2831f33 100644 --- a/tests/pub.go +++ b/tests/pub.go @@ -1,6 +1,7 @@ package tests import ( + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -17,6 +18,7 @@ import ( "github.com/nknorg/tuna/pb" "github.com/nknorg/tuna/types" "github.com/nknorg/tuna/util" + "google.golang.org/protobuf/proto" ) var ch chan string = make(chan string, 4) @@ -48,7 +50,6 @@ func startNconnect(configFile string, tuna, udp, tun bool, n *types.Node) error } opts.LocalSocksAddr = fmt.Sprintf("127.0.0.1:%v", port) } - fmt.Printf("opts.RemoteAdminAddr: %+v\n", opts.RemoteAdminAddr) nc, _ := nconnect.NewNconnect(opts) go func() { @@ -68,37 +69,48 @@ func startNconnect(configFile string, tuna, udp, tun bool, n *types.Node) error time.Sleep(5 * time.Second) // wait for nconnect to create tunnels - tunnels := nc.GetTunnels() - for _, tunnel := range tunnels { - <-tunnel.TunaSessionClient().OnConnect() + tunnels := nc.GetClientTunnels() + for _, t := range tunnels { + if ts := t.TunaSessionClient(); ts != nil { + <-ts.OnConnect() + } } return err } -func getTunaNode() (*types.Node, error) { +func getTunaNode(ip string) (*types.Node, error) { tunaSeed, _ := hex.DecodeString(seedHex) acc, err := nkn.NewAccount(tunaSeed) if err != nil { return nil, err } - go runReverseEntry(tunaSeed) + if ip == "127.0.0.1" { + go runReverseEntry(tunaSeed) + } md := &pb.ServiceMetadata{ - Ip: "127.0.0.1", + Ip: ip, // "127.0.0.1", TcpPort: 30020, UdpPort: 30021, ServiceId: 0, Price: "0.0", BeneficiaryAddr: "", } + + metadataRaw, err := proto.Marshal(md) + if err != nil { + log.Fatalln(err) + } + metadata := base64.StdEncoding.EncodeToString(metadataRaw) + n := &types.Node{ Delay: 0, Bandwidth: 0, Metadata: md, Address: hex.EncodeToString(acc.PublicKey), - MetadataRaw: "CgkxMjcuMC4wLjEQxOoBGMXqAToFMC4wMDE=", + MetadataRaw: metadata, // "CgkxMjcuMC4wLjEQxOoBGMXqAToFMC4wMDE=", } return n, nil diff --git a/tests/tcp.go b/tests/tcp.go index 00783da..6520c75 100644 --- a/tests/tcp.go +++ b/tests/tcp.go @@ -4,12 +4,13 @@ import ( "encoding/json" "fmt" "net" + "strings" "time" "golang.org/x/net/proxy" ) -func StartTcpServer() error { +func StartTcpServer(tcpPort string) error { tcpServer, err := net.Listen("tcp", tcpPort) if err != nil { return err @@ -21,15 +22,21 @@ func StartTcpServer() error { if err != nil { return err } + fmt.Printf("TCP Server got a connection from %v\n", c.RemoteAddr()) go func(conn net.Conn) { defer conn.Close() b := make([]byte, 1024) for { n, err := conn.Read(b) if err != nil { - fmt.Printf("StartTcpServer, Read err %v\n", err) + if strings.Contains(err.Error(), "closed") { + fmt.Printf("client connection closed\n") + } else { + fmt.Printf("StartTcpServer, Read err %v\n", err) + } break } + fmt.Printf("TCP Server got: %v\n", string(b[:n])) _, err = conn.Write(b[:n]) if err != nil { @@ -71,6 +78,7 @@ func StartTCPClient(serverAddr string) error { fmt.Printf("StartTCPClient, conn.Write err: %v\n", err) return err } + fmt.Printf("StartTCPClient, conn.Write %+v\n", user) b2 := make([]byte, 1024) n, err := conn.Read(b2) @@ -123,6 +131,7 @@ func StartTCPTunClient(serverAddr string) error { fmt.Printf("StartTCPClient, json.Unmarshal err: %v\n", err) return err } + fmt.Printf("Receive echo from server: %+v\n", respUser) if respUser.Age != user.Age { return fmt.Errorf("StartTCPClient, got wrong response, sent %+v, recv %+v", user, respUser) diff --git a/tests/tools/tcpmain.go b/tests/tools/tcpmain.go new file mode 100644 index 0000000..7026763 --- /dev/null +++ b/tests/tools/tcpmain.go @@ -0,0 +1,22 @@ +package main + +import ( + "flag" + + "github.com/nknorg/nconnect/tests" +) + +const ( + tcpPort = ":12345" +) + +func main() { + var server = flag.Bool("server", false, "run as server") + var serverAddr = flag.String("serverAddr", "127.0.0.1", "server's ip") + flag.Parse() + if *server { + tests.StartTcpServer(tcpPort) + } else { + tests.StartTCPTunClient(*serverAddr + tcpPort) + } +} diff --git a/tests/tun_test.go b/tests/tun_test.go index 9fa0f06..f865eb6 100644 --- a/tests/tun_test.go +++ b/tests/tun_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/require" ) +// var tun = flag.Bool("tun", false, "use tun device") + // go test -v -run=TestTun -tun func TestTun(t *testing.T) { fmt.Println("Make sure run this case at root or administrator shell") diff --git a/util/util.go b/util/util.go index 2ec97ba..de3237a 100644 --- a/util/util.go +++ b/util/util.go @@ -2,7 +2,8 @@ package util import ( "encoding/json" - "github.com/nknorg/tuna" + "errors" + "fmt" "io/ioutil" "log" "net" @@ -11,23 +12,58 @@ import ( "os/exec" "regexp" "strings" + "sync" "time" + + "github.com/nknorg/tuna" ) -func GetFreePort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") - if err != nil { - return 0, err - } +// Get free port start from parameter port +// If paramenter port is 0, start from system returned free port +// The returned port is free in TCP and UDP +var lock sync.Mutex +var ports map[int]struct{} = make(map[int]struct{}) - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, err - } +func GetFreePort(port int) (int, error) { + for i := 0; i < 100; i++ { + addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() - defer l.Close() + port = l.Addr().(*net.TCPAddr).Port + + udpAddr, err := net.ResolveUDPAddr("udp", addr.String()) + if err != nil { + return 0, err + } + u, err := net.ListenUDP("udp", udpAddr) + if err != nil { + l.Close() + port++ + continue + } + defer u.Close() + + lock.Lock() + if _, ok := ports[port]; ok { + lock.Unlock() + continue + } else { + ports[port] = struct{}{} + } + lock.Unlock() + + return port, nil + } - return l.Addr().(*net.TCPAddr).Port, nil + return 0, errors.New("failed to find free port after 100 tries") } func MergeStrings(src, target []string) []string { diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 0000000..2005a56 --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,21 @@ +package util + +import ( + "log" + "testing" +) + +// go test -v -run=TestGetFreePort +func TestGetFreePort(t *testing.T) { + port, err := GetFreePort(0) + if err != nil { + log.Println(err) + } + log.Println(port) + + port, err = GetFreePort(1080) + if err != nil { + log.Println(err) + } + log.Println(port) +} diff --git a/web/dist/200.html b/web/dist/200.html index cee360f..5e34da9 100644 --- a/web/dist/200.html +++ b/web/dist/200.html @@ -1,9 +1,9 @@
-