diff --git a/README.md b/README.md index 31c4ce5..9205718 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. @@ -293,6 +293,85 @@ $ nConnect -c -a server-address1 -a server-address2 -a server-address3 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. +## Set up a virtual private network by nConnect +Yes, nConnect supports to set up a virtual private network. It means many computers can join a nConnect network, and access each others just like all nodes are in a private network no matter where they are. + +To set up a nConnect network, you need first starts a node run `network manager`. + +### Start a network manager +To start network manager, first, we copy `config.network.json` to config.json: + +``` +$ cp config.network.josn config.json +``` + +Then modify config.json, enable `NetworkManager`, and disable others' options, just like below: + +``` + +{ + "Client": false, + "Server": false, + "NetworkManager": true, + "NetworkMember": false, + "ManagerAddress": "", + + "identifier": "manager", + +} + +``` + +Then you can start nConnect as a network manager: + +``` +$ ./nConnect +``` + +After nConnect network Manager starts, You can see a printed message: + +``` +nConnect network manager is listening at: manager.0ec192083.... +Network manager web serve at http://127.0.0.1:8000/network +``` + +Copy this listening address, it is the manager's address. +After the manager starts, you can visit web service `http://127.0.0.1:8000/network` (default), to manage the network. + +### Start a network member 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.json` + +``` +$ cp config.network.json config.json +``` + +Then modify `config.json` to enable it to connect to the network +``` +{ + "Client": true, + "Server": true, + "NetworkManager": false, + "NetworkMember": true, + "ManagerAddress": "manager.0ec192083....", + "nodeName": "alice", + + "identifier": "alice", + +} + +``` + +Remember to fill in `ManagerAddress`, and identify your node name `nodeName` + +Then you can start this node to join the network: + +``` +$ ./nConnect +``` + +After the node joins the network, the administrator should open the manager's web administrate page, to authorize this node. After being authorized, the node can access other nodes or be accessed by other nodes just like in a private local network. + ## Contributing **Can I submit a bug, suggestion or feature request?** diff --git a/admin/client.go b/admin/client.go index 71810c3..3799c23 100644 --- a/admin/client.go +++ b/admin/client.go @@ -14,7 +14,7 @@ const ( ) var ( - errReplyTimeout = errors.New("wait for reply timeout") + ErrReplyTimeout = errors.New("wait for reply timeout") ) var ( @@ -23,18 +23,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,6 +46,10 @@ func NewClient(account *nkn.Account, clientConfig *nkn.ClientConfig) (*Client, e } func (c *Client) RPCCall(addr, method string, params interface{}, result interface{}) error { + if c.ReplyTimeout == 0 { + c.ReplyTimeout = replyTimeout + } + req, err := json.Marshal(map[string]interface{}{ "id": "nConnect", "method": method, @@ -64,15 +71,15 @@ Loop: select { case reply = <-onReply.C: break Loop - case <-time.After(c.replyTimeout): - err = errReplyTimeout + case <-time.After(c.ReplyTimeout): + err = ErrReplyTimeout } } if err != nil { return err } - resp := &rpcResp{ + resp := &RpcResp{ Result: result, } err = json.Unmarshal(reply.Data, resp) 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..5f4749b 100644 --- a/admin/server.go +++ b/admin/server.go @@ -10,6 +10,12 @@ import ( tunnel "github.com/nknorg/nkn-tunnel" ) +var networkMemberIAccept []string + +func SetNetworkMemberIAccept(addr []string) { + networkMemberIAccept = addr +} + func StartNKNServer(account *nkn.Account, identifier string, clientConfig *nkn.ClientConfig, tun *tunnel.Tunnel, persistConf, mergedConf *config.Config) error { m, err := nkn.NewMultiClient(account, identifier, 4, false, clientConfig) if err != nil { @@ -23,7 +29,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) @@ -32,18 +38,19 @@ func StartNKNServer(account *nkn.Account, identifier string, clientConfig *nkn.C isAcceptAddr := util.MatchRegex(persistConf.GetAcceptAddrs(), msg.Src) isAdminAddr := util.MatchRegex(persistConf.GetAdminAddrs(), msg.Src) + isNetworkMemberAddr := util.MatchRegex(networkMemberIAccept, msg.Src) if !isAdminAddr && tokenStore.IsValid(req.Token) { isAdminAddr = true } - if !isAcceptAddr && !isAdminAddr { - log.Println("Ignore authorized message from", msg.Src) + if !isAcceptAddr && !isAdminAddr && !isNetworkMemberAddr { + log.Println("Ignore unauthorized message from", msg.Src) continue } var perm permission - if isAcceptAddr { + if isAcceptAddr || isNetworkMemberAddr { perm |= rpcPermissionAcceptClient } if isAdminAddr { 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/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/bin/main.go b/bin/main.go index 6aef4bf..5e5566e 100644 --- a/bin/main.go +++ b/bin/main.go @@ -48,4 +48,16 @@ func main() { log.Fatal(err) } } + if opts.NetworkManager { + err = nc.StartNetworkManager() + if err != nil { + log.Fatal(err) + } + } + if opts.NetworkMember { + err = nc.StartNetworkMember() + if err != nil { + log.Fatal(err) + } + } } diff --git a/config.network.json b/config.network.json new file mode 100644 index 0000000..a304b54 --- /dev/null +++ b/config.network.json @@ -0,0 +1,30 @@ +{ + "Client": true, + "Server": true, + "NetworkManager": false, + "NetworkMember": true, + "ManagerAddress": "", + "nodeName": "", + + "identifier": "", + "seed": "", + "cipher": "dummy", + "adminIdentifier": "nConnect", + + "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, + "ConfigFile": "network_save.json", + "verbose": false +} + \ No newline at end of file diff --git a/config/config.go b/config/config.go index 52123f1..cdec597 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 `long:"network-member" description:"Join nConnect network as a member"` 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:"Node name, will be used as nConnect network node name"` + ManagerAddress string `json:"managerAddress,omitempty" long:"manager-address" description:"Manager address, will be used as nConnect network manager address"` } func NewConfig() *Config { diff --git a/go.mod b/go.mod index 704dfff..9a9b420 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ 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 @@ -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..bf9f046 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= @@ -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.go b/nconnect.go index 95fa227..fccf8b5 100644 --- a/nconnect.go +++ b/nconnect.go @@ -2,6 +2,7 @@ package nconnect import ( "encoding/hex" + "errors" "fmt" "io" "log" @@ -19,6 +20,7 @@ import ( "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" @@ -42,11 +44,12 @@ 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 remoteInfoCache map[string]*admin.GetInfoJSON // map remote admin address to remote info @@ -54,6 +57,9 @@ type nconnect struct { tunnels []*tunnel.Tunnel tunaNode *types.Node // It is used to connect specified tuna node, mainly is for testing. + + networkMember *network.Member + networkTunnels []*tunnel.Tunnel // tunnels for network nodes } 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 { + 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) @@ -209,18 +217,17 @@ func NewNconnect(opts *config.Opts) (*nconnect, error) { } tsConfig := &ts.Config{ - TunaMaxPrice: opts.TunaMaxPrice, - TunaMinNanoPayFee: opts.TunaMinFee, - TunaNanoPayFeeRatio: opts.TunaFeeRatio, - TunaIPFilter: &geo.IPFilter{Allow: allowedIP, Disallow: disallowedIP}, - TunaNknFilter: &filter.NknFilter{Allow: allowedNknAddrs, Disallow: disallowedNknAddrs}, - TunaServiceName: opts.TunaServiceName, - TunaDownloadGeoDB: !opts.TunaDisableDownloadGeoDB, - TunaGeoDBPath: opts.TunaGeoDBPath, - TunaMeasureBandwidth: !opts.TunaDisableMeasureBandwidth, - TunaMeasureStoragePath: opts.TunaMeasureStoragePath, - TunaMeasurementBytesDownLink: opts.TunaMeasureBandwidthBytes, - TunaMinBalance: opts.TunaMinBalance, + TunaMaxPrice: opts.TunaMaxPrice, + TunaMinNanoPayFee: opts.TunaMinFee, + TunaNanoPayFeeRatio: opts.TunaFeeRatio, + TunaIPFilter: &geo.IPFilter{Allow: allowedIP, Disallow: disallowedIP}, + TunaNknFilter: &filter.NknFilter{Allow: allowedNknAddrs, Disallow: disallowedNknAddrs}, + TunaServiceName: opts.TunaServiceName, + TunaDownloadGeoDB: !opts.TunaDisableDownloadGeoDB, + TunaGeoDBPath: opts.TunaGeoDBPath, + TunaMeasureBandwidth: !opts.TunaDisableMeasureBandwidth, + TunaMeasureStoragePath: opts.TunaMeasureStoragePath, + TunaMinBalance: opts.TunaMinBalance, } if opts.SessionWindowSize > 0 { @@ -239,7 +246,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,17 +259,19 @@ 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), @@ -276,7 +285,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 } @@ -310,9 +328,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,7 +346,7 @@ 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") } @@ -371,37 +391,44 @@ func (nc *nconnect) StartClient() error { 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 + identifier := config.RandomIdentifier() + tunnels, err := tunnel.NewTunnels(nc.account, identifier, from, to, nc.opts.Tuna, nc.tunnelConfig) + if err != nil { + return err + } + nc.tunnels = tunnels + + 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:0" + nc.ssClientConfig.DefaultClient = "" + } + nc.ssClientConfig.Socks = nc.opts.LocalSocksAddr 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) + tunDevice, err := arch.OpenTunDevice(nc.opts.TunName, nc.opts.TunAddr, nc.opts.TunGateway, nc.opts.TunMask, nc.opts.TunDNS, false) if err != nil { return fmt.Errorf("failed to open TUN device: %v", err) } @@ -447,7 +474,7 @@ func (nc *nconnect) StartClient() error { } } - nc.startSSAndTunnel() + nc.startSSAndTunnel(true) nc.waitForSignal() return nil @@ -459,12 +486,12 @@ 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 if nc.opts.Tuna { minBalance, err := common.StringToFixed64(nc.opts.TunaMinBalance) @@ -492,6 +519,7 @@ 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) if err != nil { return err @@ -525,15 +553,21 @@ func (nc *nconnect) StartServer() error { log.Println("Admin web dashboard listening address:", nc.opts.AdminHTTPAddr) } - nc.startSSAndTunnel() + 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) } @@ -562,5 +596,129 @@ func (nc *nconnect) SetTunaNode(node *types.Node) { } func (nc *nconnect) GetTunnels() []*tunnel.Tunnel { - return nc.tunnels + return append(nc.tunnels, nc.networkTunnels...) +} + +func (nc *nconnect) StartNetworkManager() error { + m, err := network.NewManager(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 + } + + networkMember, err := network.NewMember(nc.opts, mc) // network member + if err != nil { + return err + } + + nc.networkMember = networkMember + + if err = networkMember.JoinNetwork(); err != nil { + return err + } + + if err = nc.SetupNetworkTunnel(); err != nil { + return err + } + networkMember.CbNodeICanAccessUpdated = nc.SetupNetworkTunnel + + if err = networkMember.StartMember(); err != nil { + return err + } + + return nil +} + +func (nc *nconnect) SetupNetworkTunnel() error { + addresses, err := nc.networkMember.GetAddressICanAccess() + if err != nil { + return err + } + if len(addresses) == 0 { + return nil + } + + existTunnels := make(map[string]*tunnel.Tunnel) + for _, t := range nc.networkTunnels { + existTunnels[t.ToAddr()] = t + } + + if nc.opts.Verbose { + log.Printf("nconnect SetupNetworkTunnels to addresses: %v\n", addresses) + } + + var from, to []string + for _, addr := range addresses { + if _, ok := existTunnels[addr]; ok { + delete(existTunnels, addr) + continue + } + + port, err := util.GetFreePort(0) + if err != nil { + return err + } + ssAddr := "127.0.0.1:" + strconv.Itoa(port) + + toAddr := strings.Replace(addr, "client.", "", -1) // remove network member's client. prefix, it should be server's address + adminAddr := nc.opts.AdminIdentifier + "." + toAddr + if remoteInfo, err := nc.getRemoteInfo(adminAddr); err == nil { + for _, ip := range remoteInfo.LocalIP.Ipv4 { + nc.ssClientConfig.TargetToClient[ip] = ssAddr + } + } else { + log.Printf("get remote info %v failed: %v", toAddr, err) + continue + } + + from = append(from, ssAddr) + to = append(to, toAddr) + delete(existTunnels, toAddr) + } + + if len(from) > 0 { + identifier := config.RandomIdentifier() + tunnels, err := tunnel.NewTunnels(nc.account, identifier, from, to, nc.opts.Tuna, nc.tunnelConfig) + if err != nil { + return err + } + nc.networkTunnels = tunnels + + if nc.ssClientConfig.DefaultClient == "" { + nc.ssClientConfig.DefaultClient = from[0] + } + + ss.UpdateTargetToClient(nc.ssClientConfig.TargetToClient) + + for _, t := range tunnels { + t.Start() + } + } + + for _, t := range existTunnels { + t.Close() + } + + return nil } diff --git a/network/manager.go b/network/manager.go new file mode 100644 index 0000000..9ed3a08 --- /dev/null +++ b/network/manager.go @@ -0,0 +1,569 @@ +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 = "192.168.0.1" + defaultIpEnd = "192.168.0.254" + defaultNetmask = "255.255.255.0" + defaultGateway = "192.168.0.1" + defaultDNS = "192.168.0.1" + + networkDataFile = "network.json" + + AcceptAll = "all" +) + +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 + mc *nkn.MultiClient + c *admin.Client + + sync.RWMutex + networkData *networkData // persisted data that will be saved to disk +} + +var manager *Manager + +func NewManager(opts *config.Opts) (*Manager, error) { + if manager != nil { + return manager, nil + } + + manager = &Manager{opts: opts} + err := manager.loadNetworkData() + if err != nil { + log.Println("Failed to loadNetworkData:", err) + return nil, err + } + + mc, err := GetMultiClient(opts) + if err != nil { + log.Println("Failed to get multi client:", err) + return nil, err + } + manager.mc = mc + manager.c = &admin.Client{MultiClient: mc} + + return manager, nil +} + +func (m *Manager) StartManager() error { + log.Println("nConnect network manager is listening at:", m.mc.Address()) + + for { + msg := <-m.mc.OnMessage.C + resp, err := m.handleRequest(msg) + if err != nil { + log.Println("nConnect manager handle request error", err) + continue + } + if m.opts.Verbose { + log.Printf("nConnect manager send response: %+v\n", resp) + } + + 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 { + log.Println("nConnect manager Unmarshal request error:", err) + return nil, err + } + + if m.opts.Verbose { + log.Printf("nConnect manager received request from %s: %+v\n", msg.Src, req) + } + + 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) + 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 = 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_MY_INFO: + resp.NetworkInfo = m.networkData.NetworkInfo + err = m.UpdateName(msg.Src, req.Name) + + default: + return nil, fmt.Errorf("nConnect manager got unknown message type: %v", req.MsgType) + } + + if err != nil { + resp.Err = err + } + + return resp, nil +} + +func (m *Manager) JoinNetwork(address, name string) (*nodeInfo, error) { + if node, ok := m.networkData.Member[address]; ok { + node.LastSeen = time.Now() + m.networkData.Member[address] = node + if err := m.saveNetworkData(); err != nil { + return nil, err + } + noti := &managerToMember{ + MsgType: NOTI_MEMBER_ONLINE, + NodeInfo: []*nodeInfo{node}, + } + SendMsg(m.c, node.Address, noti, false) + + return node, nil + } + + m.Lock() + defer m.Unlock() + _, ok := m.networkData.Waiting[address] + if ok { + return nil, errWaitForAuth + } + + if _, nameExists := m.networkData.NameToAddress[name]; nameExists { + return nil, errNameExist + } + + if len(name) > 0 { + m.networkData.NameToAddress[name] = address + } + m.networkData.Waiting[address] = &nodeInfo{Name: name, Address: address, LastSeen: time.Now()} + + if err := m.saveNetworkData(); err != nil { + return nil, err + } + + return nil, errWaitForAuth +} + +func (m *Manager) LeaveNetwork(address, name string) error { + modified := false + if _, ok := m.networkData.Member[address]; ok { + delete(m.networkData.Member, address) + modified = true + } + m.Lock() + defer m.Unlock() + if _, ok := m.networkData.Waiting[address]; ok { + delete(m.networkData.Waiting, address) + modified = true + } + if modified { + return m.saveNetworkData() + } + + return nil +} + +func (m *Manager) AuthorizeMemeber(n *nodeInfo) error { + m.Lock() + defer m.Unlock() + + if nw, ok := m.networkData.Waiting[n.Address]; ok { + ip, err := m.GetAvailableIp() + if err != nil { + log.Println("GetAvailableIp error:", err) + return err + } + nw.IP = ip + nw.Netmask = m.networkData.Netmask + m.networkData.Member[n.Address] = nw + + if m.opts.Verbose { + log.Printf("AuthorizeMemeber, Member: %+v\n", m.networkData.Member) + } + + delete(m.networkData.Waiting, n.Address) + + if err = m.saveNetworkData(); err != nil { + return err + } + + noti := &managerToMember{ + MsgType: NOTI_AUTHORIZED, + NetworkInfo: m.networkData.NetworkInfo, + NodeInfo: []*nodeInfo{nw}, + } + + if _, err = SendMsg(m.c, n.Address, noti, false); err != nil { + log.Printf("Send NOTI_AUTHORIZED to %v error %v\n", n.Address, err) + } + + for _, n := range m.networkData.Member { + acceptAddresses := m.networkData.AcceptAddress[n.Address] + if len(acceptAddresses) > 0 && acceptAddresses[0] == AcceptAll { + noti := &managerToMember{ + MsgType: NOTI_SET_ACCEPT, + } + if _, err = SendMsg(m.c, n.Address, noti, false); err != nil { + log.Printf("Send NOTI_SET_ACCEPT to %v error %v\n", n.Address, err) + } + } + } + } + + return nil +} + +func (m *Manager) DeleteWaiting(n *nodeInfo) error { + m.Lock() + defer m.Unlock() + delete(m.networkData.Waiting, n.Address) + + 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 { + log.Println("saveNetworkData error:", err) + return err + } + + for _, node := range m.networkData.Member { + changed := false + acceptAddresses := m.networkData.AcceptAddress[node.Address] + if len(acceptAddresses) > 0 && acceptAddresses[0] == AcceptAll { + 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 { + noti := &managerToMember{ + MsgType: NOTI_SET_ACCEPT, + } + if _, err = SendMsg(m.c, node.Address, noti, false); err != nil { + log.Printf("Send NOTI_SET_ACCEPT to %v error %v\n", node.Address, err) + } + } + } + } + + return nil +} + +func (m *Manager) UpdateName(address, name string) error { + m.Lock() + defer m.Unlock() + + if name == "" { + return nil + } + + if addr, ok := m.networkData.NameToAddress[name]; ok { + if addr == address { + return nil + } + return errNameExist + } + + var ok1, ok2 bool + n, ok1 := m.networkData.Member[address] + if !ok1 { + n, ok2 = m.networkData.Waiting[address] + } + + if ok1 || ok2 { + oldName := n.Name + if oldName != "" { + delete(m.networkData.NameToAddress, oldName) + } + + n.Name = name + m.networkData.NameToAddress[name] = address + + if ok1 { + m.networkData.Member[address] = n + } else { + m.networkData.Waiting[address] = n + } + + if err := m.saveNetworkData(); err != nil { + return err + } + } else { + return errNodeNotFound + } + + 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] == AcceptAll { + 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() + + var list []*nodeInfo + for addr, acceptAddress := range m.networkData.AcceptAddress { + if addr == address { + continue + } + + if len(acceptAddress) > 0 && acceptAddress[0] == AcceptAll { + 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 + } + noti := &managerToMember{MsgType: NOTI_SET_ACCEPT} + if _, err := SendMsg(m.c, address, noti, false); err != nil { + log.Printf("Send NOTI_SET_ACCEPT to %v error %v\n", address, err) + } + + m.RLock() + defer m.RUnlock() + + n := m.networkData.Member[address] + noti = &managerToMember{MsgType: NOTI_NEW_MEMBER, NodeInfo: []*nodeInfo{n}} + if len(acceptAddress) > 0 && acceptAddress[0] == AcceptAll { + for _, n := range m.networkData.Member { + if n.Address != address { + if _, err := SendMsg(m.c, n.Address, noti, false); err != nil { + log.Printf("Send NOTI_NEW_MEMBER to %v error %v\n", n.Address, err) + } + } + } + } else { + for _, addr := range acceptAddress { // broadcast accept info to nodes + if _, err := SendMsg(m.c, addr, noti, false); err != nil { + log.Printf("Send NOTI_NEW_MEMBER to %v error %v\n", 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) { + if m.networkData.NextIp == "" { + return "", errors.New("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 { + log.Printf("loadNetworkData os.Open err: %v", err) + return err + } + + defer jsonFile.Close() + + b, err := io.ReadAll(jsonFile) + if err != nil { + log.Printf("loadNetworkData io.ReadAll err: %v", err) + 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 { + log.Printf("saveNodeInfo Marshal err: %v", err) + 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..4cad02f --- /dev/null +++ b/network/member.go @@ -0,0 +1,415 @@ +package network + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net" + "os" + "time" + + "github.com/eycorsican/go-tun2socks/core" + "github.com/eycorsican/go-tun2socks/proxy/socks" + "github.com/nknorg/nconnect/admin" + "github.com/nknorg/nconnect/arch" + "github.com/nknorg/nconnect/config" + "github.com/nknorg/nconnect/util" +) + +const ( + mtu = 1500 + memberFile = "member.json" +) + +var ( + errNoDataInFile = errors.New("no data in file") + errWaitForAuth = errors.New("wait for authorization") + errNameExist = errors.New("name already exists") + errNodeNotFound = errors.New("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() error + +type Member struct { + opts *config.Opts + mc *admin.Client + networkData memberNetworkData // node info of this node + joinedNetwork bool + CbNodeICanAccessUpdated callbackNodeICanAccessUpdated +} + +func NewMember(opts *config.Opts, mc *admin.Client) (*Member, error) { + return &Member{opts: opts, mc: mc}, nil +} + +func (m *Member) StartMember() error { + err := m.loadMemberData() + if err != nil && err != errNoDataInFile { + return err + } + + if err = m.JoinNetwork(); err != nil { + return err + } + + // if config node name is updated, update it to network + if m.networkData.NodeInfo != nil && len(m.opts.NodeName) > 0 && len(m.networkData.NodeInfo.Name) > 0 && + m.opts.NodeName != m.networkData.NodeInfo.Name { + err = m.UpdateName(m.opts.NodeName) + if err != nil { + log.Printf("Failed to update name from old name %v to new name %v, err: %v", m.networkData.NodeInfo.Name, m.opts.NodeName, err) + return err + } + } + + if m.joinedNetwork { + if _, err = m.GetNodeICanAccess(); err != nil { + return err + } + } + + log.Println("nConnect Network member is listening at:", m.mc.Address()) + for { + msg := <-m.mc.OnMessage.C + + resp := &managerToMember{} + err := json.Unmarshal(msg.Data, resp) + if err != nil { + log.Println("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(noti *managerToMember) error { + if m.opts.Verbose { + log.Printf("nConnect member got notification: %+v", noti) + } + + switch noti.MsgType { + case NOTI_AUTHORIZED: + if len(noti.NodeInfo) > 0 { + m.networkData.NodeInfo = noti.NodeInfo[0] + m.saveMemberData() + SetNetworkIp(m.opts, m.networkData.NodeInfo.IP, m.networkData.NetworkInfo.Gateway, m.networkData.NodeInfo.Netmask, m.networkData.NetworkInfo.DNS) + m.GetNodeICanAccess() + } + + case NOTI_NEW_MEMBER: + m.GetNodeICanAccess() + case NOTI_SET_ACCEPT: + m.GetNodeIAccept() + case NOTI_MEMBER_ONLINE: + if m.CbNodeICanAccessUpdated != nil { + m.CbNodeICanAccessUpdated() + } + + default: + return fmt.Errorf("nConnect member got unknown notification type: %v", noti.MsgType) + } + + return nil +} + +func (m *Member) JoinNetwork() error { + msg := memberToManager{MsgType: JOIN_NETWORK, Name: m.opts.NodeName} + resp, err := SendMsg(m.mc, 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 { + log.Printf("Node name %v already exists in this network, please use another name", m.opts.NodeName) + return errNameExist + } + + if resp.Err != nil { + return 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 + SetNetworkIp(m.opts, m.networkData.NodeInfo.IP, m.networkData.NetworkInfo.Gateway, m.networkData.NodeInfo.Netmask, m.networkData.NetworkInfo.DNS) + } + } + + return nil +} + +func (m *Member) LeaveNetwork() error { + msg := memberToManager{MsgType: LEAVE_NETWORK, Name: m.opts.NodeName} + resp, err := SendMsg(m.mc, m.opts.ManagerAddress, &msg, true) + if err != nil { + return err + } + if resp.Err != nil { + return resp.Err + } + + m.networkData = memberNetworkData{} + return m.saveMemberData() +} + +func (m *Member) GetMyInfo() (*nodeInfo, error) { + msg := memberToManager{MsgType: GET_MY_INFO} + + resp, err := SendMsg(m.mc, m.opts.ManagerAddress, &msg, true) + if err != nil { + return nil, err + } + if resp.Err != nil { + return nil, resp.Err + } + + if len(resp.NodeInfo) > 0 { + m.networkData.NodeInfo = resp.NodeInfo[0] + if err = m.saveMemberData(); err != nil { + log.Printf("Failed to save member data: %v", err) + } + if err = SetNetworkIp(m.opts, m.networkData.NodeInfo.IP, m.networkData.NetworkInfo.Gateway, m.networkData.NodeInfo.Netmask, + m.networkData.NetworkInfo.DNS); err != nil { + log.Printf("Failed to set network ip: %v", err) + } + } + + return m.networkData.NodeInfo, err +} + +func (m *Member) UpdateName(name string) error { + msg := memberToManager{MsgType: UPDATE_MY_INFO, Name: name} + + resp, err := SendMsg(m.mc, m.opts.ManagerAddress, &msg, true) + if err != nil { + return err + } + if resp == nil { + return errors.New("no response") + } + + if resp.Err != nil { + return resp.Err + } + + m.networkData.NodeInfo.Name = name + return m.saveMemberData() +} + +func SetNetworkIp(opts *config.Opts, ip, gateway, mask string, dns string) error { + tunDevice, err := arch.OpenTunDevice(opts.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", opts.LocalSocksAddr) + if err != nil { + return fmt.Errorf("invalid proxy server address %v err: %v", opts.LocalSocksAddr, 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 (m *Member) GetNodeIAccept() error { + msg := memberToManager{MsgType: GET_NODES_I_ACCEPT} + resp, err := SendMsg(m.mc, m.opts.ManagerAddress, &msg, true) + if err != nil { + return err + } + if resp.Err != nil { + return resp.Err + } + + if len(resp.NodeInfo) > 0 { + m.networkData.NodesIAccept = resp.NodeInfo + if err = m.saveMemberData(); err != nil { + log.Printf("Failed to save member data: %v", err) + } + + var addrs []string + for _, node := range m.networkData.NodesIAccept { + addrs = append(addrs, node.Address) + } + admin.SetNetworkMemberIAccept(addrs) + } + + return nil +} + +func (m *Member) GetNodeICanAccess() ([]*nodeInfo, error) { + msg := memberToManager{MsgType: GET_NODES_I_CAN_ACCESS, Name: m.opts.NodeName} + resp, err := SendMsg(m.mc, m.opts.ManagerAddress, &msg, true) + if err != nil { + return nil, err + } + if resp.Err != nil { + return nil, resp.Err + } + + if len(resp.NodeInfo) > 0 { + m.networkData.NodesICanAccess = resp.NodeInfo + if err = m.saveMemberData(); err != nil { + log.Printf("Failed to save member data: %v", err) + } + } + + if m.CbNodeICanAccessUpdated != nil { + m.CbNodeICanAccessUpdated() + } + + return m.networkData.NodesICanAccess, nil +} + +func (m *Member) GetAddressICanAccess() ([]string, error) { + var addrs []string + for _, node := range m.networkData.NodesICanAccess { + addrs = append(addrs, node.Address) + } + return addrs, nil +} + +func (m *Member) GetAddressByIp(ip string) (string, error) { + for _, node := range m.networkData.NodesICanAccess { + if node.IP == ip { + return node.Address, nil + } + } + return "", errNodeNotFound +} + +func (m *Member) loadMemberData() error { + jsonFile, err := os.OpenFile(memberFile, os.O_CREATE|os.O_RDONLY, os.ModePerm) + if err != nil { + log.Printf("loadMemberData os.Open err: %v", err) + return err + } + + defer jsonFile.Close() + + b, err := io.ReadAll(jsonFile) + if err != nil { + log.Printf("loadMemberData io.ReadAll err: %v", err) + return err + } + + data := &memberNetworkData{} + + if len(b) > 0 { + if err = json.Unmarshal(b, data); err != nil { + return err + } + m.networkData.NetworkInfo = data.NetworkInfo + m.networkData.NodeInfo = data.NodeInfo + } + + return errNoDataInFile +} + +func (m *Member) saveMemberData() error { + b, err := json.Marshal(m.networkData) + if err != nil { + log.Printf("saveMemberData Marshal err: %v", err) + 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..14f3201 --- /dev/null +++ b/network/message.go @@ -0,0 +1,142 @@ +package network + +import ( + "encoding/hex" + "encoding/json" + "errors" + "time" + + "github.com/imdario/mergo" + "github.com/nknorg/nconnect/admin" + "github.com/nknorg/nconnect/config" + "github.com/nknorg/nkn-sdk-go" + "github.com/nknorg/nkngomobile" +) + +// msgType constants +const ( + MT_NONE = 0 + JOIN_NETWORK = 1 + UPDATE_MY_INFO = 2 + GET_MY_INFO = 3 + GET_NODES_I_ACCEPT = 4 + GET_NODES_I_CAN_ACCESS = 5 + LEAVE_NETWORK = 6 + + NOTI_AUTHORIZED = 7 + NOTI_NEW_MEMBER = 8 + NOTI_SET_ACCEPT = 9 + NOTI_MEMBER_ONLINE = 10 +) + +type nodeInfo struct { + IP string `json:"ip"` + Netmask string `json:"netmask"` + Name string `json:"name"` + Address string `json:"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"` +} + +type managerToMember struct { + MsgType int `json:"msgType"` + Err error `json:"err"` + NetworkInfo *networkInfo `json:"networkInfo"` + NodeInfo []*nodeInfo `json:"nodeInfo"` +} + +func SendMsg(mc *admin.Client, address string, msg interface{}, waitResponse bool) (*managerToMember, error) { + reqBytes, err := json.Marshal(msg) + if err != nil { + return nil, err + } + + var onMessage *nkn.OnMessage + for i := 0; i < 3; i++ { + onMessage, err = mc.Send(nkn.NewStringArray(address), reqBytes, nil) + if err != nil { + return nil, err + } + + if !waitResponse { + return nil, nil + } + + select { + case resp := <-onMessage.C: + var respMsg managerToMember + if err = json.Unmarshal(resp.Data, &respMsg); err != nil { + return nil, err + } + return &respMsg, nil + + case <-time.After(10 * time.Second): + err = admin.ErrReplyTimeout + } + } + + return nil, err +} + +func GetMultiClient(opts *config.Opts) (*nkn.MultiClient, error) { + persistConf, err := config.LoadOrNewConfig(opts.ConfigFile) + if err != nil { + return nil, err + } + + if err = mergo.Merge(&opts.Config, persistConf); err != nil { + return nil, err + } + + if len(opts.Identifier) == 0 { + return nil, errors.New("Manager identifier should be configured") + } + + seed, err := hex.DecodeString(opts.Seed) + if err != nil { + return nil, err + } + + account, err := nkn.NewAccount(seed) + if err != nil { + return nil, err + } + + var seedRPCServerAddr *nkngomobile.StringArray + if len(opts.SeedRPCServerAddr) > 0 { + seedRPCServerAddr = nkn.NewStringArray(opts.SeedRPCServerAddr...) + } + clientConfig := &nkn.ClientConfig{ + SeedRPCServerAddr: seedRPCServerAddr, + } + + mc, err := nkn.NewMultiClient(account, opts.Identifier, 4, false, clientConfig) + if err != nil { + return nil, err + } + + <-mc.OnConnect.C + return mc, nil +} + +func GetAdminClient(opts *config.Opts) *admin.Client { + mc, err := GetMultiClient(opts) + if err != nil { + return nil + } + c := &admin.Client{ + MultiClient: mc, + } + + return c +} diff --git a/network/webservice.go b/network/webservice.go new file mode 100644 index 0000000..32d3a72 --- /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..c7441a5 100644 --- a/ss/multiconn.go +++ b/ss/multiconn.go @@ -23,3 +23,9 @@ func getClient(target string) string { } return routes.DefaultClient } + +func UpdateTargetToClient(targetToClient map[string]string) { + routes.Lock() + defer routes.Unlock() + routes.TargetToClient = targetToClient +} diff --git a/ss/udp.go b/ss/udp.go index 356be15..2d77921 100644 --- a/ss/udp.go +++ b/ss/udp.go @@ -38,6 +38,7 @@ func udpLocal(laddr, server, target string, shadow func(net.PacketConn) net.Pack return fmt.Errorf("UDP local listen error: %v", err) } defer c.Close() + fmt.Printf("udp local listen on %s\n", laddr) nm := newNATmap(config.UDPTimeout) buf := make([]byte, udpBufSize) @@ -75,9 +76,10 @@ 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() + fmt.Printf("udp socks local listen on %s\n", laddr) nm := newNATmap(config.UDPTimeout) buf := make([]byte, udpBufSize) @@ -123,6 +125,7 @@ func udpRemote(addr string, shadow func(net.PacketConn) net.PacketConn) error { return fmt.Errorf("UDP remote listen error: %v", err) } defer c.Close() + fmt.Printf("udp remote listen on %s\n", addr) c = shadow(c) nm := newNATmap(config.UDPTimeout) @@ -157,6 +160,7 @@ func udpRemote(addr string, shadow func(net.PacketConn) net.PacketConn) error { logf("UDP remote listen error: %v", err) continue } + fmt.Printf("udp remote listen on2 %s\n", pc.LocalAddr()) nm.Add(raddr, c, pc, remoteServer) } diff --git a/tests/config.manager.json b/tests/config.manager.json new file mode 100644 index 0000000..288b3e3 --- /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:8000", + "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..6580794 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") @@ -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..96a597a 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) @@ -76,29 +78,38 @@ func startNconnect(configFile string, tuna, udp, tun bool, n *types.Node) error 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..65dfea1 100644 --- a/tests/tcp.go +++ b/tests/tcp.go @@ -21,6 +21,7 @@ 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) @@ -71,6 +72,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) diff --git a/util/util.go b/util/util.go index 2ec97ba..7112987 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" @@ -12,22 +13,44 @@ import ( "regexp" "strings" "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 +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 - } + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + + port = l.Addr().(*net.TCPAddr).Port - defer l.Close() + 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() + + return l.Addr().(*net.TCPAddr).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..05d4e99 100644 --- a/web/dist/200.html +++ b/web/dist/200.html @@ -1,9 +1,9 @@
-