├── errors.go ├── tests ├── services.reverse.entry.json ├── services.entry.json ├── services.exit.json ├── services.reverse.exit.json ├── storage_test.go ├── measurement_test.go ├── config.reverse.entry.json ├── config.forward.exit.json ├── config.reverse.exit.json ├── config.forward.entry.json ├── entry_test.go ├── geo_test.go └── util.go ├── services.json.example ├── .gitignore ├── cmd ├── version.go ├── main.go ├── entry.go └── exit.go ├── util ├── queue.go ├── util.go └── measurement.go ├── types └── node.go ├── config.entry.json.example ├── .github └── workflows │ └── go.yml ├── config.exit.json.example ├── pb ├── tuna.proto └── tuna.pb.go ├── storage ├── storage.go └── measure.go ├── go.mod ├── encryption.go ├── filter └── nkn_filter.go ├── Makefile ├── subqueue.go ├── geo ├── ip2c.go ├── geolite.go ├── aws.go ├── gcp.go └── geo.go ├── udp.go ├── util.go ├── go.sum ├── README.md ├── LICENSE ├── config.go ├── exit.go └── entry.go /errors.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrClosed = errors.New("closed") 7 | ) 8 | -------------------------------------------------------------------------------- /tests/services.reverse.entry.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "test", 4 | "tcp": [1234], 5 | "udp": [1234] 6 | } 7 | ] -------------------------------------------------------------------------------- /tests/services.entry.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "test", 4 | "tcp": [12345], 5 | "udp": [12345], 6 | "encryption": "xsalsa20-poly1305" 7 | }, 8 | { 9 | "name": "test2", 10 | "tcp": [12346], 11 | "udp": [12346] 12 | } 13 | ] -------------------------------------------------------------------------------- /services.json.example: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "httpproxy", 4 | "tcp": [30080], 5 | "encryption": "xsalsa20-poly1305" 6 | }, 7 | { 8 | "name": "socksproxy", 9 | "tcp": [30489], 10 | "encryption": "xsalsa20-poly1305" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | *~ 4 | .DS_Store 5 | build 6 | tuna 7 | wallet.json 8 | wallet.pswd 9 | config.entry.json 10 | config.exit.json 11 | services.json 12 | aws-ip.json 13 | gcp-ip.json 14 | geolite2-country.mmdb* 15 | *.favorite-node.json 16 | *.avoid-node.json 17 | -------------------------------------------------------------------------------- /tests/services.exit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "test", 4 | "udp": [54321], 5 | "tcp": [54321], 6 | "udpBufferSize": 65536 7 | }, 8 | { 9 | "name": "test2", 10 | "udp": [54322], 11 | "tcp": [54322], 12 | "udpBufferSize": 65536 13 | } 14 | ] -------------------------------------------------------------------------------- /tests/services.reverse.exit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "test", 4 | "udp": [54321], 5 | "tcp": [54321], 6 | "udpBufferSize": 65536, 7 | "encryption": "xsalsa20-poly1305" 8 | }, 9 | { 10 | "name": "test2", 11 | "udp": [54322], 12 | "tcp": [54322], 13 | "udpBufferSize": 65536 14 | } 15 | ] -------------------------------------------------------------------------------- /tests/storage_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/nknorg/tuna/storage" 8 | ) 9 | 10 | func TestMeasureStorage(t *testing.T) { 11 | measureStorage := storage.NewMeasureStorage(".", "test") 12 | err := measureStorage.Load() 13 | if err != nil { 14 | log.Println(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type VersionCommand struct{} 6 | 7 | var versionCommand VersionCommand 8 | 9 | func (v *VersionCommand) Execute(args []string) error { 10 | fmt.Println(Version) 11 | return nil 12 | } 13 | 14 | func init() { 15 | parser.AddCommand("version", "Print version", "Print version", &versionCommand) 16 | } 17 | -------------------------------------------------------------------------------- /util/queue.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "sync" 4 | 5 | type Job func() 6 | 7 | func Worker(jobChan <-chan Job, wg *sync.WaitGroup) { 8 | for job := range jobChan { 9 | Process(job, wg) 10 | } 11 | } 12 | 13 | func WorkPool(workerNum int, jobChan chan Job, wg *sync.WaitGroup) { 14 | for i := 0; i < workerNum; i++ { 15 | go func(i int) { 16 | Worker(jobChan, wg) 17 | }(i) 18 | } 19 | } 20 | 21 | func Enqueue(jobChan chan<- Job, job Job) { 22 | jobChan <- job 23 | } 24 | 25 | func Process(job Job, wg *sync.WaitGroup) { 26 | defer wg.Done() 27 | job() 28 | } 29 | -------------------------------------------------------------------------------- /tests/measurement_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/nknorg/tuna/util" 10 | ) 11 | 12 | var ips = []string{ 13 | "example.com:80", 14 | } 15 | 16 | func TestDelayMeasurement(t *testing.T) { 17 | var wg sync.WaitGroup 18 | for _, ip := range ips { 19 | wg.Add(1) 20 | go func(ip string) { 21 | defer wg.Done() 22 | delay, err := util.DelayMeasurement("tcp", ip, time.Second*2, nil) 23 | if err != nil { 24 | log.Println("timeout, ip:", ip) 25 | return 26 | } 27 | log.Println("succeeded, ip:", ip, ", delay: ", delay) 28 | }(ip) 29 | } 30 | wg.Wait() 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /tests/config.reverse.entry.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "test": { 4 | "maxPrice": "0.0", 5 | "ipFilter": { 6 | "allow": [ 7 | {"countryCode": ""} 8 | ], 9 | "disallow": [ 10 | {"countryCode": ""} 11 | ] 12 | } 13 | } 14 | }, 15 | "downloadGeoDB": false, 16 | "geoDBPath": ".", 17 | "dialTimeout": 10, 18 | "udpTimeout": 0, 19 | "nanoPayFee": "", 20 | "minNanoPayFee": "0.00001", 21 | "nanoPayFeeRatio": 0.1, 22 | "reverse": true, 23 | "reverseBeneficiaryAddr": "", 24 | "reverseTCP": 30020, 25 | "reverseUDP": 30021, 26 | "reversePrice": "0.0", 27 | "reverseClaimInterval": 3600, 28 | "reverseSubscriptionDuration": 40000, 29 | "reverseSubscriptionFee": "0.0" 30 | } 31 | -------------------------------------------------------------------------------- /types/node.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/nknorg/tuna/pb" 5 | ) 6 | 7 | type Node struct { 8 | Delay float32 // ms 9 | Bandwidth float32 // byte/s 10 | Metadata *pb.ServiceMetadata 11 | Address string 12 | MetadataRaw string 13 | } 14 | 15 | type Nodes []*Node 16 | 17 | func (fs Nodes) Len() int { 18 | return len(fs) 19 | } 20 | 21 | func (fs Nodes) Swap(i, j int) { 22 | fs[i], fs[j] = fs[j], fs[i] 23 | } 24 | 25 | type SortByDelay struct{ Nodes } 26 | 27 | func (s SortByDelay) Less(i, j int) bool { 28 | return s.Nodes[i].Delay < s.Nodes[j].Delay 29 | } 30 | 31 | type SortByBandwidth struct{ Nodes } 32 | 33 | func (s SortByBandwidth) Less(i, j int) bool { 34 | return s.Nodes[i].Bandwidth > s.Nodes[j].Bandwidth 35 | } 36 | -------------------------------------------------------------------------------- /tests/config.forward.exit.json: -------------------------------------------------------------------------------- 1 | { 2 | "beneficiaryAddr": "", 3 | "listenTCP": 30010, 4 | "listenUDP": 30011, 5 | "dialTimeout": 10, 6 | "udpTimeout": 60, 7 | "claimInterval": 3600, 8 | "subscriptionDuration": 40000, 9 | "subscriptionFee": "0", 10 | "services": { 11 | "test": { 12 | "address": "127.0.0.1", 13 | "price": "0.0" 14 | } 15 | }, 16 | "reverse": false, 17 | "reverseRandomPorts": false, 18 | "reverseMaxPrice": "0.0", 19 | "reverseNanoPayFee": "", 20 | "minReverseNanoPayFee": "0.00001", 21 | "reverseNanoPayFeeRatio": 0.1, 22 | "reverseIPFilter": { 23 | "allow": [ 24 | {"countryCode": ""} 25 | ], 26 | "disallow": [ 27 | {"countryCode": ""} 28 | ] 29 | }, 30 | "downloadGeoDB": false, 31 | "geoDBPath": "." 32 | } 33 | -------------------------------------------------------------------------------- /config.entry.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "httpproxy": { 4 | "maxPrice": "0.001", 5 | "ipFilter": { 6 | "allow": [ 7 | {"countryCode": ""} 8 | ], 9 | "disallow": [ 10 | {"countryCode": ""} 11 | ] 12 | } 13 | } 14 | }, 15 | "downloadGeoDB": true, 16 | "geoDBPath": ".", 17 | "dialTimeout": 10, 18 | "udpTimeout": 60, 19 | "nanoPayFee": "", 20 | "minNanoPayFee": "0.00001", 21 | "nanoPayFeeRatio": 0.1, 22 | "reverse": false, 23 | "reverseBeneficiaryAddr": "", 24 | "reverseTCP": 30020, 25 | "reverseUDP": 30021, 26 | "reversePrice": "0.0002", 27 | "reverseClaimInterval": 3600, 28 | "reverseSubscriptionDuration": 40000, 29 | "reverseSubscriptionFee": "0.00001", 30 | "reverseSubscriptionReplaceTxPool": false 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | [ push, pull_request ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.20.2 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | 27 | - name: Gomobile Build 28 | run: | 29 | go install golang.org/x/mobile/cmd/gomobile@latest 30 | gomobile init 31 | gomobile bind -v -target=android -androidapi=33 -ldflags "-s -w" ./ 32 | -------------------------------------------------------------------------------- /config.exit.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "beneficiaryAddr": "", 3 | "listenTCP": 30010, 4 | "listenUDP": 30011, 5 | "dialTimeout": 10, 6 | "udpTimeout": 60, 7 | "claimInterval": 3600, 8 | "subscriptionDuration": 40000, 9 | "subscriptionFee": "0.00001", 10 | "subscriptionReplaceTxPool": false, 11 | "services": { 12 | "httpproxy": { 13 | "address": "127.0.0.1", 14 | "price": "0.0002" 15 | } 16 | }, 17 | "reverse": false, 18 | "reverseRandomPorts": true, 19 | "reverseMaxPrice": "0.001", 20 | "reverseNanoPayFee": "", 21 | "minReverseNanoPayFee": "0.00001", 22 | "reverseNanoPayFeeRatio": 0.1, 23 | "reverseIPFilter": { 24 | "allow": [ 25 | {"countryCode": ""} 26 | ], 27 | "disallow": [ 28 | {"countryCode": ""} 29 | ] 30 | }, 31 | "downloadGeoDB": true, 32 | "geoDBPath": "." 33 | } 34 | -------------------------------------------------------------------------------- /pb/tuna.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./pb"; 4 | 5 | package pb; 6 | 7 | enum EncryptionAlgo { 8 | ENCRYPTION_NONE = 0; 9 | ENCRYPTION_XSALSA20_POLY1305 = 1; 10 | ENCRYPTION_AES_GCM = 2; 11 | } 12 | 13 | message ConnectionMetadata { 14 | EncryptionAlgo encryption_algo = 1; 15 | bytes public_key = 2; 16 | bytes nonce = 3; 17 | bool is_measurement = 4; 18 | uint32 measurement_bytes_downlink = 5; 19 | bool is_ping = 6; 20 | } 21 | 22 | message ServiceMetadata { 23 | string ip = 1; 24 | uint32 tcp_port = 2; 25 | uint32 udp_port = 3; 26 | uint32 service_id = 4; 27 | repeated uint32 service_tcp = 5; 28 | repeated uint32 service_udp = 6; 29 | string price = 7; 30 | string beneficiary_addr = 8; 31 | } 32 | 33 | message StreamMetadata { 34 | uint32 service_id = 1; 35 | uint32 port_id = 2; 36 | bool is_payment = 3; 37 | } 38 | -------------------------------------------------------------------------------- /tests/config.reverse.exit.json: -------------------------------------------------------------------------------- 1 | { 2 | "beneficiaryAddr": "", 3 | "listenTCP": 30010, 4 | "listenUDP": 30011, 5 | "dialTimeout": 10, 6 | "udpTimeout": 60, 7 | "claimInterval": 3600, 8 | "subscriptionDuration": 40000, 9 | "subscriptionFee": "0", 10 | "services": { 11 | "test": { 12 | "address": "127.0.0.1", 13 | "price": "0.0" 14 | }, 15 | "test2": { 16 | "address": "127.0.0.1", 17 | "price": "0.0" 18 | } 19 | }, 20 | "reverse": true, 21 | "reverseRandomPorts": true, 22 | "reverseMaxPrice": "0.0", 23 | "reverseNanoPayFee": "", 24 | "minReverseNanoPayFee": "0.00001", 25 | "reverseNanoPayFeeRatio": 0.1, 26 | "reverseIPFilter": { 27 | "allow": [ 28 | {"countryCode": ""} 29 | ], 30 | "disallow": [ 31 | {"countryCode": ""} 32 | ] 33 | }, 34 | "downloadGeoDB": false, 35 | "geoDBPath": "." 36 | } 37 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/nknorg/tuna/util" 5 | "sync" 6 | ) 7 | 8 | type Storage struct { 9 | sync.RWMutex 10 | data map[string]interface{} 11 | } 12 | 13 | func NewStorage() *Storage { 14 | return &Storage{ 15 | data: make(map[string]interface{}), 16 | } 17 | } 18 | 19 | func (s *Storage) GetData() map[string]interface{} { 20 | s.RLock() 21 | defer s.RUnlock() 22 | return util.DeepCopyMap(s.data) 23 | } 24 | 25 | func (s *Storage) Get(key string) (interface{}, bool) { 26 | s.RLock() 27 | defer s.RUnlock() 28 | v, ok := s.data[key] 29 | return v, ok 30 | } 31 | 32 | func (s *Storage) Add(key string, val interface{}) { 33 | s.Lock() 34 | defer s.Unlock() 35 | s.data[key] = val 36 | } 37 | 38 | func (s *Storage) Delete(key string) { 39 | s.Lock() 40 | defer s.Unlock() 41 | delete(s.data, key) 42 | } 43 | 44 | func (s *Storage) Len() int { 45 | s.RLock() 46 | defer s.RUnlock() 47 | return len(s.data) 48 | } 49 | -------------------------------------------------------------------------------- /tests/config.forward.entry.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "test": { 4 | "maxPrice": "0.0", 5 | "ipFilter": { 6 | "allow": [ 7 | {"countryCode": ""} 8 | ], 9 | "disallow": [ 10 | {"countryCode": ""} 11 | ] 12 | } 13 | }, 14 | "test2": { 15 | "maxPrice": "0.0", 16 | "ipFilter": { 17 | "allow": [ 18 | {"countryCode": ""} 19 | ], 20 | "disallow": [ 21 | {"countryCode": ""} 22 | ] 23 | } 24 | } 25 | }, 26 | "downloadGeoDB": false, 27 | "geoDBPath": ".", 28 | "dialTimeout": 10, 29 | "udpTimeout": 0, 30 | "nanoPayFee": "", 31 | "minNanoPayFee": "0.00001", 32 | "nanoPayFeeRatio": 0.1, 33 | "reverse": false, 34 | "reverseBeneficiaryAddr": "", 35 | "reverseTCP": 30020, 36 | "reverseUDP": 30021, 37 | "reversePrice": "0.0", 38 | "reverseClaimInterval": 3600, 39 | "reverseSubscriptionDuration": 40000, 40 | "reverseSubscriptionFee": "0.0" 41 | } 42 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | 8 | "github.com/jessevdk/go-flags" 9 | ) 10 | 11 | var opts struct { 12 | BeneficiaryAddr string `short:"b" long:"beneficiary-addr" description:"Beneficiary address (NKN wallet address to receive rewards)"` 13 | ServicesFile string `short:"s" long:"services" description:"Services file path" default:"services.json"` 14 | WalletFile string `short:"w" long:"wallet" description:"Wallet file path" default:"wallet.json"` 15 | PasswordFile string `short:"p" long:"password-file" description:"Wallet password file path" default:"wallet.pswd"` 16 | SeedRPCServerAddr string `long:"rpc" description:"Seed RPC server address, separated by comma"` 17 | Version bool `short:"v" long:"version" description:"Print version"` 18 | } 19 | 20 | var ( 21 | parser = flags.NewParser(&opts, flags.Default) 22 | Version string 23 | ) 24 | 25 | func main() { 26 | defer func() { 27 | if r := recover(); r != nil { 28 | log.Fatalf("Panic: %+v", r) 29 | } 30 | }() 31 | 32 | _, err := parser.Parse() 33 | if err != nil { 34 | var e *flags.Error 35 | if errors.As(err, &e) && e.Type == flags.ErrHelp { 36 | os.Exit(0) 37 | } 38 | log.Fatalln(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nknorg/tuna 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/imdario/mergo v0.3.13 7 | github.com/jessevdk/go-flags v1.5.0 8 | github.com/nknorg/encrypted-stream v1.0.2-0.20230320101720-9891f770de86 9 | github.com/nknorg/nkn-sdk-go v1.4.8-0.20240427043332-a40386d2b50a 10 | github.com/nknorg/nkn/v2 v2.2.0 11 | github.com/oschwald/geoip2-golang v1.4.0 12 | github.com/patrickmn/go-cache v2.1.0+incompatible 13 | github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40 14 | github.com/xtaci/smux v2.0.1+incompatible 15 | golang.org/x/crypto v0.17.0 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c 17 | google.golang.org/protobuf v1.33.0 18 | ) 19 | 20 | require ( 21 | github.com/golang/protobuf v1.5.3 // indirect 22 | github.com/gorilla/websocket v1.5.0 // indirect 23 | github.com/hashicorp/errwrap v1.1.0 // indirect 24 | github.com/hashicorp/go-multierror v1.1.1 // indirect 25 | github.com/itchyny/base58-go v0.2.1 // indirect 26 | github.com/jpillora/backoff v1.0.0 // indirect 27 | github.com/nknorg/ncp-go v1.0.5 // indirect 28 | github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9 // indirect 29 | github.com/oschwald/maxminddb-golang v1.6.0 // indirect 30 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 31 | github.com/pkg/errors v0.9.1 // indirect 32 | golang.org/x/sys v0.15.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /encryption.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "net" 7 | 8 | stream "github.com/nknorg/encrypted-stream" 9 | "github.com/nknorg/tuna/pb" 10 | ) 11 | 12 | const ( 13 | connNonceSize = 32 14 | sharedKeySize = 32 15 | encryptKeySize = 32 16 | ) 17 | 18 | func computeEncryptKey(connNonce []byte, sharedKey []byte) *[encryptKeySize]byte { 19 | encryptKey := sha256.Sum256(append(connNonce, sharedKey...)) 20 | return &encryptKey 21 | } 22 | 23 | func encryptConn(conn net.Conn, encryptKey *[encryptKeySize]byte, encryptionAlgo pb.EncryptionAlgo, initiator bool) (net.Conn, error) { 24 | var cipher stream.Cipher 25 | var err error 26 | switch encryptionAlgo { 27 | case pb.EncryptionAlgo_ENCRYPTION_NONE: 28 | return conn, nil 29 | case pb.EncryptionAlgo_ENCRYPTION_XSALSA20_POLY1305: 30 | cipher = stream.NewXSalsa20Poly1305Cipher(encryptKey) 31 | case pb.EncryptionAlgo_ENCRYPTION_AES_GCM: 32 | cipher, err = stream.NewAESGCMCipher(encryptKey[:]) 33 | if err != nil { 34 | return nil, err 35 | } 36 | default: 37 | return nil, fmt.Errorf("unsupported encryption algo %v", encryptionAlgo) 38 | } 39 | config := &stream.Config{ 40 | Cipher: cipher, 41 | Initiator: initiator, 42 | SequentialNonce: true, 43 | DisableNonceVerification: true, // for compatibility with old version, will be removed in the future 44 | } 45 | return stream.NewEncryptedStream(conn, config) 46 | } 47 | -------------------------------------------------------------------------------- /filter/nkn_filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | type NknClient struct { 8 | Address string `json:"address"` 9 | Metadata string `json:"metadata"` 10 | } 11 | 12 | var emptyNknClient = NknClient{} 13 | 14 | func (c *NknClient) Empty() bool { 15 | if c == nil { 16 | return true 17 | } 18 | return *c == emptyNknClient 19 | } 20 | 21 | func (c *NknClient) Match(nknClient *NknClient) bool { 22 | if len(c.Address) > 0 { 23 | if nknClient.Address == c.Address { 24 | return true 25 | } 26 | return false 27 | } 28 | return false 29 | } 30 | 31 | type NknFilter struct { 32 | Allow []NknClient `json:"allow"` 33 | Disallow []NknClient `json:"disallow"` 34 | } 35 | 36 | func (f *NknFilter) Empty() bool { 37 | if f == nil { 38 | return true 39 | } 40 | for _, a := range f.Allow { 41 | if !a.Empty() { 42 | return false 43 | } 44 | } 45 | for _, d := range f.Disallow { 46 | if !d.Empty() { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | func (f *NknFilter) IsAllow(nknClient *NknClient) bool { 54 | if f == nil { 55 | return true 56 | } 57 | if nknClient.Empty() { 58 | return true 59 | } 60 | 61 | for _, d := range f.Disallow { 62 | if d.Match(nknClient) { 63 | log.Printf("%s dropped", nknClient.Address) 64 | return false 65 | } 66 | } 67 | 68 | empty := true 69 | for _, a := range f.Allow { 70 | if a.Match(nknClient) { 71 | log.Printf("%s passed", nknClient.Address) 72 | return true 73 | } 74 | if !a.Empty() { 75 | empty = false 76 | } 77 | } 78 | 79 | return empty 80 | } 81 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL:=local_or_with_proxy 2 | 3 | USE_PROXY=GOPROXY=https://goproxy.io 4 | VERSION:=$(shell git describe --abbrev=7 --dirty --always --tags) 5 | BUILD=go build -ldflags "-s -w -X main.Version=$(VERSION)" 6 | BUILD_DIR=build 7 | ifdef GOARM 8 | BIN_DIR=$(GOOS)-$(GOARCH)v$(GOARM) 9 | else 10 | BIN_DIR=$(GOOS)-$(GOARCH) 11 | endif 12 | 13 | .PHONY: tuna 14 | tuna: 15 | $(BUILD) $(BUILD_PARAMS) ./cmd 16 | 17 | .PHONY: local 18 | local: 19 | ${MAKE} tuna BUILD_PARAMS="-o tuna" 20 | 21 | .PHONY: local_with_proxy 22 | local_with_proxy: 23 | $(USE_PROXY) ${MAKE} tuna BUILD_PARAMS="-o tuna" 24 | 25 | .PHONY: local_or_with_proxy 26 | local_or_with_proxy: 27 | ${MAKE} local || ${MAKE} local_with_proxy 28 | 29 | .PHONY: build 30 | build: 31 | mkdir -p $(BUILD_DIR)/$(BIN_DIR) 32 | ${MAKE} tuna BUILD_PARAMS="-o $(BUILD_DIR)/$(BIN_DIR)/tuna$(EXT)" GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) 33 | cp config.entry.json.example $(BUILD_DIR)/$(BIN_DIR)/config.entry.json 34 | cp config.exit.json.example $(BUILD_DIR)/$(BIN_DIR)/config.exit.json 35 | cp services.json.example $(BUILD_DIR)/$(BIN_DIR)/services.json 36 | ${MAKE} zip 37 | 38 | .PHONY: tar 39 | tar: 40 | cd $(BUILD_DIR) && rm -f $(BIN_DIR).tar.gz && tar --exclude ".DS_Store" --exclude "__MACOSX" -czvf $(BIN_DIR).tar.gz $(BIN_DIR) 41 | 42 | .PHONY: zip 43 | zip: 44 | cd $(BUILD_DIR) && rm -f $(BIN_DIR).zip && zip --exclude "*.DS_Store*" --exclude "*__MACOSX*" -r $(BIN_DIR).zip $(BIN_DIR) 45 | 46 | .PHONY: all 47 | all: 48 | ${MAKE} build GOOS=linux GOARCH=amd64 49 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=5 50 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=6 51 | ${MAKE} build GOOS=linux GOARCH=arm GOARM=7 52 | ${MAKE} build GOOS=linux GOARCH=arm64 53 | ${MAKE} build GOOS=darwin GOARCH=amd64 54 | ${MAKE} build GOOS=darwin GOARCH=arm64 55 | ${MAKE} build GOOS=windows GOARCH=amd64 EXT=.exe 56 | 57 | .PHONY: pb 58 | pb: 59 | protoc --go_out=. pb/*.proto 60 | -------------------------------------------------------------------------------- /subqueue.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/nknorg/nkn-sdk-go" 8 | "github.com/nknorg/nkn/v2/config" 9 | ) 10 | 11 | const ( 12 | subQueueLen = 1024 13 | maxRetry = 3 14 | ) 15 | 16 | type subscribeData struct { 17 | client *nkn.MultiClient 18 | identifier string 19 | topic string 20 | duration int 21 | meta string 22 | config *nkn.TransactionConfig 23 | replaceTxPool bool 24 | } 25 | 26 | var subQueue chan *subscribeData 27 | 28 | func init() { 29 | subQueue = make(chan *subscribeData, subQueueLen) 30 | go func() { 31 | for subData := range subQueue { 32 | for i := 0; i < maxRetry; i++ { 33 | if subData.replaceTxPool { 34 | nonce, err := subData.client.GetNonce(false) 35 | if err != nil { 36 | log.Println("get nonce error:", err) 37 | time.Sleep(time.Second) 38 | continue 39 | } 40 | subData.config.Nonce = nonce 41 | subData.config.FixNonce = true 42 | } 43 | 44 | txnHash, err := subData.client.Subscribe(subData.identifier, subData.topic, subData.duration, subData.meta, subData.config) 45 | if err != nil { 46 | log.Println("subscribe to topic", subData.topic, "error:", err) 47 | time.Sleep(time.Second) 48 | continue 49 | } 50 | 51 | log.Println("Subscribed to topic", subData.topic, "success:", txnHash) 52 | break 53 | } 54 | time.Sleep(config.ConsensusTimeout) 55 | } 56 | }() 57 | } 58 | 59 | func addToSubscribeQueue(client *nkn.MultiClient, identifier string, topic string, duration int, meta string, config *nkn.TransactionConfig, replaceTxPool bool) { 60 | subData := &subscribeData{ 61 | client: client, 62 | identifier: identifier, 63 | topic: topic, 64 | duration: duration, 65 | meta: meta, 66 | config: config, 67 | replaceTxPool: replaceTxPool, 68 | } 69 | select { 70 | case subQueue <- subData: 71 | default: 72 | log.Println("Subscribe queue full, discard request.") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func ReadJSON(fileName string, value interface{}) error { 15 | file, err := os.ReadFile(fileName) 16 | if err != nil { 17 | return fmt.Errorf("read file error: %v", err) 18 | } 19 | 20 | err = json.Unmarshal(file, value) 21 | if err != nil { 22 | return fmt.Errorf("parse json error: %v", err) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func WriteJSON(path string, data interface{}) error { 29 | f, err := os.Create(path) 30 | if err != nil { 31 | return err 32 | } 33 | defer f.Close() 34 | 35 | b, err := json.MarshalIndent(data, "", " ") 36 | if err != nil { 37 | return err 38 | } 39 | _, err = f.Write(b) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | func Exists(path string) bool { 47 | _, err := os.Stat(path) 48 | if err != nil { 49 | if os.IsExist(err) { 50 | return true 51 | } 52 | return false 53 | } 54 | return true 55 | } 56 | 57 | func DownloadJsonFile(ctx context.Context, url, filename string) error { 58 | req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 59 | if err != nil { 60 | return err 61 | } 62 | client := http.Client{ 63 | Timeout: 60 * time.Second, 64 | } 65 | resp, err := client.Do(req) 66 | if err != nil { 67 | return err 68 | } 69 | defer resp.Body.Close() 70 | b, err := io.ReadAll(resp.Body) 71 | if err != nil { 72 | return err 73 | } 74 | if !json.Valid(b) { 75 | return errors.New("invalid json") 76 | } 77 | f, err := os.Create(filename) 78 | if err != nil { 79 | return err 80 | } 81 | defer f.Close() 82 | _, err = f.Write(b) 83 | if err != nil { 84 | os.Remove(filename) 85 | return err 86 | } 87 | return nil 88 | } 89 | 90 | func DeepCopyMap(value map[string]interface{}) map[string]interface{} { 91 | newMap := make(map[string]interface{}) 92 | for k, v := range value { 93 | newMap[k] = v 94 | } 95 | 96 | return newMap 97 | } 98 | 99 | func CopyFile(src, dst string) error { 100 | input, err := os.ReadFile(src) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | err = os.WriteFile(dst, input, 0644) 106 | if err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /geo/ip2c.go: -------------------------------------------------------------------------------- 1 | package geo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | GeoIPRetry = 3 15 | IP2CUrl = "http://ip2c.org/" 16 | ) 17 | 18 | type IP2CProvider struct { 19 | } 20 | 21 | func NewIP2CProvider() *IP2CProvider { 22 | return new(IP2CProvider) 23 | } 24 | 25 | func (p *IP2CProvider) MaybeUpdate() error { 26 | return p.MaybeUpdateContext(context.Background()) 27 | } 28 | 29 | func (p *IP2CProvider) MaybeUpdateContext(ctx context.Context) error { 30 | return nil 31 | } 32 | 33 | func (p *IP2CProvider) GetLocation(ip string) (*Location, error) { 34 | loc, err := p.getLocationFromIP2C(ip, GeoIPRetry) 35 | if err != nil { 36 | return &emptyLocation, err 37 | } 38 | return loc, nil 39 | } 40 | 41 | func (p *IP2CProvider) getLocationFromIP2C(ip string, retry int) (*Location, error) { 42 | queryURL := IP2CUrl + ip 43 | client := http.Client{ 44 | Timeout: 10 * time.Second, 45 | } 46 | 47 | i := 0 48 | var resp *http.Response 49 | var err error 50 | for ; i < retry; i++ { 51 | resp, err = client.Get(queryURL) 52 | if err != nil { 53 | log.Println(err) 54 | continue 55 | } 56 | break 57 | } 58 | if i == retry { 59 | return nil, err 60 | } 61 | 62 | defer resp.Body.Close() 63 | 64 | body, err := ioutil.ReadAll(resp.Body) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | loc, err := parseIP2C(ip, string(body)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return loc, nil 75 | } 76 | 77 | func parseIP2C(ip, body string) (*Location, error) { 78 | if len(body) == 0 { 79 | return nil, nil 80 | } 81 | 82 | if body[0] != byte('1') { 83 | return nil, errors.New("get ip2c result err") 84 | } 85 | 86 | res := strings.Split(body, ";") 87 | if len(res) != 4 { 88 | return nil, errors.New("invalid response from ip2c service") 89 | } 90 | 91 | l := &Location{} 92 | l.CountryCode = res[1] 93 | l.IP = ip 94 | return l, nil 95 | } 96 | 97 | func (p *IP2CProvider) FileName() string { 98 | return "" 99 | } 100 | 101 | func (p *IP2CProvider) DownloadUrl() string { 102 | return "" 103 | } 104 | 105 | func (p *IP2CProvider) LastUpdate() time.Time { 106 | return time.Time{} 107 | } 108 | 109 | func (p *IP2CProvider) NeedUpdate() bool { 110 | return false 111 | } 112 | 113 | func (p *IP2CProvider) Ready() bool { 114 | return true 115 | } 116 | 117 | func (p *IP2CProvider) SetReady(bool) {} 118 | 119 | func (p *IP2CProvider) SetFileName(string) {} 120 | -------------------------------------------------------------------------------- /geo/geolite.go: -------------------------------------------------------------------------------- 1 | package geo 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "time" 14 | 15 | "github.com/nknorg/tuna/util" 16 | 17 | "github.com/oschwald/geoip2-golang" 18 | ) 19 | 20 | const ( 21 | Geolite2Url = "https://gitlab.com/leo108/geolite2-db/-/raw/master/Country.mmdb" 22 | MaxMindExpired = 30 * 24 * time.Hour 23 | MaxMindFile = "geolite2-country.mmdb" 24 | ) 25 | 26 | type MaxMindProvider struct { 27 | DB *geoip2.Reader 28 | fileName string 29 | url string 30 | expire time.Duration 31 | ready bool 32 | } 33 | 34 | func (p *MaxMindProvider) GetLocation(ip string) (*Location, error) { 35 | loc, err := p.getLocationFromMM(ip) 36 | if err != nil { 37 | return &emptyLocation, err 38 | } 39 | return loc, nil 40 | } 41 | 42 | func NewMaxMindProvider(path string) *MaxMindProvider { 43 | return &MaxMindProvider{ 44 | url: MaxMindFile, 45 | fileName: filepath.Join(path, MaxMindFile), 46 | expire: MaxMindExpired, 47 | } 48 | } 49 | 50 | func (p *MaxMindProvider) MaybeUpdate() error { 51 | return p.MaybeUpdateContext(context.Background()) 52 | } 53 | 54 | func (p *MaxMindProvider) MaybeUpdateContext(ctx context.Context) error { 55 | geoLock.Lock() 56 | defer geoLock.Unlock() 57 | if !p.NeedUpdate() && p.DB != nil { 58 | return nil 59 | } 60 | if p.NeedUpdate() { 61 | log.Println("Updating geolite db") 62 | tmpFile, err := ioutil.TempFile(path.Dir(p.fileName), path.Base(p.fileName)+"-*") 63 | if err != nil { 64 | return err 65 | } 66 | defer os.Remove(tmpFile.Name()) 67 | defer tmpFile.Close() 68 | 69 | req, err := http.NewRequestWithContext(ctx, "GET", Geolite2Url, nil) 70 | if err != nil { 71 | return err 72 | } 73 | client := http.Client{ 74 | Timeout: 300 * time.Second, 75 | } 76 | resp, err := client.Do(req) 77 | if err != nil { 78 | return err 79 | } 80 | defer resp.Body.Close() 81 | 82 | _, err = io.Copy(tmpFile, resp.Body) 83 | if err != nil { 84 | return err 85 | } 86 | err = util.CopyFile(tmpFile.Name(), p.fileName) 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | 92 | db, err := geoip2.Open(p.fileName) 93 | if err != nil { 94 | os.Remove(p.fileName) 95 | return err 96 | } 97 | 98 | p.DB = db 99 | p.ready = true 100 | return nil 101 | } 102 | 103 | func (p *MaxMindProvider) getLocationFromMM(ip string) (*Location, error) { 104 | parsed := net.ParseIP(ip) 105 | record, err := p.DB.Country(parsed) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return &Location{CountryCode: record.Country.IsoCode, IP: ip}, nil 110 | } 111 | 112 | func (p *MaxMindProvider) FileName() string { 113 | return p.fileName 114 | } 115 | 116 | func (p *MaxMindProvider) DownloadUrl() string { 117 | return p.url 118 | } 119 | 120 | func (p *MaxMindProvider) LastUpdate() time.Time { 121 | return getModTime(p.fileName) 122 | } 123 | 124 | func (p *MaxMindProvider) NeedUpdate() bool { 125 | return time.Since(p.LastUpdate()) > p.expire 126 | } 127 | 128 | func (p *MaxMindProvider) SetReady(ready bool) { 129 | p.ready = ready 130 | } 131 | 132 | func (p *MaxMindProvider) Ready() bool { 133 | return p.ready 134 | } 135 | 136 | func (p *MaxMindProvider) SetFileName(name string) { 137 | p.fileName = name 138 | } 139 | -------------------------------------------------------------------------------- /tests/entry_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "github.com/nknorg/nkn/v2/crypto" 5 | "net" 6 | "os" 7 | "strconv" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | server := NewServer("0.0.0.0", "54321") 15 | server2 := NewServer("0.0.0.0", "54322") 16 | go server.RunUDPEchoServer() 17 | go server.RunTCPEchoServer() 18 | go server2.RunUDPEchoServer() 19 | go server2.RunTCPEchoServer() 20 | os.Exit(m.Run()) 21 | } 22 | 23 | func TestForwardProxy(t *testing.T) { 24 | exitPubKey, exitPrivKey, _ := crypto.GenKeyPair() 25 | exitSeed := crypto.GetSeedFromPrivateKey(exitPrivKey) 26 | 27 | _, entryPrivKey, _ := crypto.GenKeyPair() 28 | entrySeed := crypto.GetSeedFromPrivateKey(entryPrivKey) 29 | 30 | exitReady := make(chan struct{}) 31 | go runForwardExit(exitSeed, exitReady) 32 | go runForwardEntry(entrySeed, exitPubKey, exitReady) 33 | <-exitReady 34 | time.Sleep(time.Second * 5) 35 | 36 | tcpConn, err := net.Dial("tcp", "127.0.0.1:12345") 37 | if err != nil { 38 | t.Fatal("dial err:", err) 39 | } 40 | err = testTCP(tcpConn) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | tcpConn2, err := net.Dial("tcp", "127.0.0.1:12346") 46 | if err != nil { 47 | t.Fatal("dial err:", err) 48 | } 49 | err = testTCP(tcpConn2) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{ 55 | IP: net.ParseIP("127.0.0.1"), 56 | Port: 12345, 57 | }) 58 | err = testUDP(udpConn) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | udpConn2, err := net.DialUDP("udp", nil, &net.UDPAddr{ 64 | IP: net.ParseIP("127.0.0.1"), 65 | Port: 12346, 66 | }) 67 | err = testUDP(udpConn2) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | } 72 | 73 | func TestMultipleClientReverseProxy(t *testing.T) { 74 | entryPubKey, entryPrivKey, _ := crypto.GenKeyPair() 75 | entrySeed := crypto.GetSeedFromPrivateKey(entryPrivKey) 76 | 77 | ready := make(chan struct{}) 78 | go runReverseEntry(entrySeed, ready) 79 | <-ready 80 | exitNum := 4 81 | clientNum := 4 82 | var wg sync.WaitGroup 83 | for i := 0; i < exitNum; i++ { 84 | wg.Add(1) 85 | go func() { 86 | defer wg.Done() 87 | _, exitPrivKey, _ := crypto.GenKeyPair() 88 | exitSeed := crypto.GetSeedFromPrivateKey(exitPrivKey) 89 | tcpPort := make([]int, 2) 90 | udpPort := make([]int, 2) 91 | go runReverseExit(&tcpPort, &udpPort, exitSeed, entryPubKey) 92 | time.Sleep(3 * time.Second) 93 | for j := 0; j < len(tcpPort); j++ { 94 | tcpAddr := "127.0.0.1:" + strconv.Itoa(tcpPort[j]) 95 | tcpConn, err := net.Dial("tcp", tcpAddr) 96 | if err != nil { 97 | t.Error("dial err:", err) 98 | return 99 | } 100 | err = testTCP(tcpConn) 101 | if err != nil { 102 | t.Error(err) 103 | return 104 | } 105 | } 106 | 107 | var wg2 sync.WaitGroup 108 | for i := 0; i < clientNum; i++ { 109 | wg2.Add(1) 110 | go func() { 111 | defer wg2.Done() 112 | for j := 0; j < len(udpPort); j++ { 113 | udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{ 114 | IP: net.ParseIP("127.0.0.1"), 115 | Port: udpPort[j], 116 | }) 117 | if err != nil { 118 | t.Error(err) 119 | return 120 | } 121 | udpConn.SetDeadline(time.Now().Add(10 * time.Second)) 122 | err = testUDP(udpConn) 123 | if err != nil { 124 | t.Error(err) 125 | return 126 | } 127 | } 128 | }() 129 | } 130 | wg2.Wait() 131 | }() 132 | } 133 | wg.Wait() 134 | } 135 | -------------------------------------------------------------------------------- /cmd/entry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/nknorg/nkn-sdk-go" 8 | "github.com/nknorg/tuna" 9 | "github.com/nknorg/tuna/util" 10 | ) 11 | 12 | type EntryCommand struct { 13 | ConfigFile string `short:"c" long:"config" description:"Config file path" default:"config.entry.json"` 14 | Reverse bool `long:"reverse" description:"Reverse mode"` 15 | } 16 | 17 | var entryCommand EntryCommand 18 | 19 | func (e *EntryCommand) Execute(args []string) error { 20 | config := &tuna.EntryConfiguration{} 21 | err := util.ReadJSON(e.ConfigFile, config) 22 | if err != nil { 23 | log.Fatalln("Load config error:", err) 24 | } 25 | 26 | if len(opts.BeneficiaryAddr) > 0 { 27 | config.ReverseBeneficiaryAddr = opts.BeneficiaryAddr 28 | } 29 | 30 | if entryCommand.Reverse { 31 | config.Reverse = true 32 | } 33 | 34 | if len(config.ReverseBeneficiaryAddr) > 0 { 35 | err = nkn.VerifyWalletAddress(config.ReverseBeneficiaryAddr) 36 | if err != nil { 37 | log.Fatalln("Invalid beneficiary address:", err) 38 | } 39 | } 40 | 41 | account, err := tuna.LoadOrCreateAccount(opts.WalletFile, opts.PasswordFile) 42 | if err != nil { 43 | log.Fatalln("Load or create account error:", err) 44 | } 45 | 46 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...) 47 | if len(opts.SeedRPCServerAddr) > 0 { 48 | seedRPCServerAddr = nkn.NewStringArrayFromString(strings.ReplaceAll(opts.SeedRPCServerAddr, ",", " ")) 49 | } else if len(config.SeedRPCServerAddr) > 0 { 50 | seedRPCServerAddr = nkn.NewStringArray(config.SeedRPCServerAddr...) 51 | } else if !config.Reverse && len(config.MeasureStoragePath) > 0 { 52 | c, err := tuna.MergedEntryConfig(config) 53 | if err == nil { 54 | for serviceName := range c.Services { 55 | rpcAddrs, err := tuna.GetFavoriteSeedRPCServer(c.MeasureStoragePath, c.SubscriptionPrefix+serviceName, 3000, c.HttpDialContext) 56 | if err == nil { 57 | seedRPCServerAddr = nkn.NewStringArray(append(rpcAddrs, nkn.DefaultSeedRPCServerAddr...)...) 58 | break 59 | } 60 | } 61 | } 62 | } 63 | 64 | config.SeedRPCServerAddr = seedRPCServerAddr.Elems() 65 | 66 | walletConfig := &nkn.WalletConfig{ 67 | SeedRPCServerAddr: seedRPCServerAddr, 68 | } 69 | wallet, err := nkn.NewWallet(&nkn.Account{Account: account}, walletConfig) 70 | if err != nil { 71 | log.Fatalln("Create wallet error:", err) 72 | } 73 | 74 | log.Println("Your NKN wallet address is:", wallet.Address()) 75 | 76 | if config.Reverse { 77 | err = tuna.StartReverse(config, wallet) 78 | if err != nil { 79 | log.Fatalln(err) 80 | } 81 | } else { 82 | var services []tuna.Service 83 | err = util.ReadJSON(opts.ServicesFile, &services) 84 | if err != nil { 85 | log.Fatalln("Load service file error:", err) 86 | } 87 | 88 | service: 89 | for serviceName, serviceInfo := range config.Services { 90 | for _, service := range services { 91 | if service.Name == serviceName { 92 | go func(service tuna.Service, serviceInfo tuna.ServiceInfo) { 93 | if len(service.UDP) > 0 && service.UDPBufferSize == 0 { 94 | service.UDPBufferSize = tuna.DefaultUDPBufferSize 95 | } 96 | for { 97 | te, err := tuna.NewTunaEntry(service, serviceInfo, wallet, nil, config) 98 | if err != nil { 99 | log.Fatalln(err) 100 | } 101 | 102 | err = te.Start(false) 103 | if err != nil { 104 | log.Println(err) 105 | } 106 | } 107 | }(service, serviceInfo) 108 | continue service 109 | } 110 | } 111 | log.Fatalln("Service", serviceName, "not found in service file") 112 | } 113 | } 114 | 115 | select {} 116 | } 117 | 118 | func init() { 119 | parser.AddCommand("entry", "Tuna entry mode", "Start tuna in entry mode", &entryCommand) 120 | } 121 | -------------------------------------------------------------------------------- /util/measurement.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "net" 7 | "time" 8 | ) 9 | 10 | const ( 11 | readBufferSize = 1024 12 | writeBufferSize = 1024 13 | ) 14 | 15 | func DelayMeasurement(network, address string, timeout time.Duration, dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) (time.Duration, error) { 16 | return DelayMeasurementContext(context.Background(), network, address, timeout, dialContext) 17 | } 18 | 19 | func DelayMeasurementContext(ctx context.Context, network, address string, timeout time.Duration, dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) (time.Duration, error) { 20 | now := time.Now() 21 | 22 | var conn net.Conn 23 | var err error 24 | if dialContext != nil { 25 | ctx, cancel := context.WithTimeout(ctx, timeout) 26 | defer cancel() 27 | conn, err = dialContext(ctx, network, address) 28 | } else { 29 | conn, err = net.DialTimeout(network, address, timeout) 30 | } 31 | delay := time.Since(now) 32 | if err != nil { 33 | return delay, err 34 | } 35 | 36 | conn.Close() 37 | 38 | return delay, nil 39 | } 40 | 41 | func BandwidthMeasurementClient(conn net.Conn, bytesDownlink int, timeout time.Duration) (float32, float32, error) { 42 | return BandwidthMeasurementClientContext(context.Background(), conn, bytesDownlink, timeout) 43 | } 44 | 45 | func BandwidthMeasurementClientContext(ctx context.Context, conn net.Conn, bytesDownlink int, timeout time.Duration) (float32, float32, error) { 46 | timeStart := time.Now() 47 | var timeToFirstByte time.Duration 48 | 49 | if timeout > 0 { 50 | err := conn.SetReadDeadline(timeStart.Add(timeout)) 51 | if err != nil { 52 | return 0, 0, err 53 | } 54 | } 55 | 56 | done := make(chan struct{}) 57 | go func() { 58 | select { 59 | case <-ctx.Done(): 60 | conn.SetDeadline(time.Now()) 61 | case <-done: 62 | } 63 | }() 64 | 65 | b := make([]byte, readBufferSize) 66 | var bytesRead, m int 67 | var err error 68 | for bytesRead = 0; bytesRead < bytesDownlink; { 69 | n := bytesDownlink - bytesRead 70 | if n > len(b) { 71 | n = len(b) 72 | } 73 | m, err = conn.Read(b[:n]) 74 | if err != nil { 75 | return 0, 0, err 76 | } 77 | if bytesRead == 0 { 78 | timeToFirstByte = time.Since(timeStart) 79 | } 80 | bytesRead += m 81 | } 82 | 83 | close(done) 84 | 85 | timeToLastByte := time.Since(timeStart) 86 | bps := float32(bytesDownlink) / float32(timeToLastByte) * float32(time.Second) 87 | bpsRead := bps 88 | if bytesRead > m { 89 | bpsRead = float32(bytesDownlink) / float32(timeToLastByte-timeToFirstByte) * float32(time.Second) 90 | } 91 | 92 | return bps, bpsRead, nil 93 | } 94 | 95 | func BandwidthMeasurementServer(conn net.Conn, bytesDownlink int, timeout time.Duration) error { 96 | return BandwidthMeasurementServerContext(context.Background(), conn, bytesDownlink, timeout) 97 | } 98 | 99 | func BandwidthMeasurementServerContext(ctx context.Context, conn net.Conn, bytesDownlink int, timeout time.Duration) error { 100 | if timeout > 0 { 101 | err := conn.SetWriteDeadline(time.Now().Add(timeout)) 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | 107 | done := make(chan struct{}) 108 | go func() { 109 | select { 110 | case <-ctx.Done(): 111 | conn.SetDeadline(time.Now()) 112 | case <-done: 113 | } 114 | }() 115 | 116 | b := make([]byte, writeBufferSize) 117 | for bytesWritten := 0; bytesWritten < bytesDownlink; { 118 | n := bytesDownlink - bytesWritten 119 | if n > len(b) { 120 | n = len(b) 121 | } 122 | _, err := rand.Read(b[:n]) 123 | if err != nil { 124 | return err 125 | } 126 | m, err := conn.Write(b[:n]) 127 | if err != nil { 128 | return err 129 | } 130 | bytesWritten += m 131 | } 132 | 133 | close(done) 134 | 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /cmd/exit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/nknorg/nkn-sdk-go" 8 | "github.com/nknorg/tuna" 9 | "github.com/nknorg/tuna/util" 10 | ) 11 | 12 | type ExitCommand struct { 13 | ConfigFile string `short:"c" long:"config" description:"Config file path" default:"config.exit.json"` 14 | Reverse bool `long:"reverse" description:"Reverse mode"` 15 | } 16 | 17 | var exitCommand ExitCommand 18 | 19 | func (e *ExitCommand) Execute(args []string) error { 20 | config := &tuna.ExitConfiguration{} 21 | err := util.ReadJSON(e.ConfigFile, config) 22 | if err != nil { 23 | log.Fatalln("Load config file error:", err) 24 | } 25 | 26 | if len(opts.BeneficiaryAddr) > 0 { 27 | config.BeneficiaryAddr = opts.BeneficiaryAddr 28 | } 29 | 30 | if exitCommand.Reverse { 31 | config.Reverse = true 32 | } 33 | 34 | if len(config.BeneficiaryAddr) > 0 { 35 | err = nkn.VerifyWalletAddress(config.BeneficiaryAddr) 36 | if err != nil { 37 | log.Fatalln("Invalid beneficiary address:", err) 38 | } 39 | } 40 | 41 | account, err := tuna.LoadOrCreateAccount(opts.WalletFile, opts.PasswordFile) 42 | if err != nil { 43 | log.Fatalln("Load or create account error:", err) 44 | } 45 | 46 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...) 47 | if len(opts.SeedRPCServerAddr) > 0 { 48 | seedRPCServerAddr = nkn.NewStringArrayFromString(strings.ReplaceAll(opts.SeedRPCServerAddr, ",", " ")) 49 | } else if len(config.SeedRPCServerAddr) > 0 { 50 | seedRPCServerAddr = nkn.NewStringArray(config.SeedRPCServerAddr...) 51 | } else if config.Reverse && len(config.MeasureStoragePath) > 0 { 52 | c, err := tuna.MergedExitConfig(config) 53 | if err == nil { 54 | rpcAddrs, err := tuna.GetFavoriteSeedRPCServer(config.MeasureStoragePath, c.SubscriptionPrefix+c.ReverseServiceName, 3000, config.HttpDialContext) 55 | if err == nil { 56 | seedRPCServerAddr = nkn.NewStringArray(append(rpcAddrs, nkn.DefaultSeedRPCServerAddr...)...) 57 | } 58 | } 59 | } 60 | 61 | config.SeedRPCServerAddr = seedRPCServerAddr.Elems() 62 | 63 | walletConfig := &nkn.WalletConfig{ 64 | SeedRPCServerAddr: seedRPCServerAddr, 65 | } 66 | wallet, err := nkn.NewWallet(&nkn.Account{Account: account}, walletConfig) 67 | if err != nil { 68 | log.Fatalln("Create wallet error:", err) 69 | } 70 | 71 | log.Println("Your NKN wallet address is:", wallet.Address()) 72 | 73 | var services []tuna.Service 74 | err = util.ReadJSON(opts.ServicesFile, &services) 75 | if err != nil { 76 | log.Fatalln("Load service file error:", err) 77 | } 78 | 79 | if config.Reverse { 80 | for _, service := range services { 81 | if _, ok := config.Services[service.Name]; ok { 82 | go func(service tuna.Service) { 83 | for { 84 | te, err := tuna.NewTunaExit([]tuna.Service{service}, wallet, nil, config) 85 | if err != nil { 86 | log.Fatalln(err) 87 | } 88 | 89 | go func() { 90 | for range te.OnConnect.C { 91 | log.Printf("Service: %s, Type: TCP, Address: %v:%v\n", service.Name, te.GetReverseIP(), te.GetReverseTCPPorts()) 92 | if len(service.UDP) > 0 { 93 | log.Printf("Service: %s, Type: UDP, Address: %v:%v\n", service.Name, te.GetReverseIP(), te.GetReverseUDPPorts()) 94 | } 95 | } 96 | }() 97 | 98 | err = te.StartReverse(false) 99 | if err != nil { 100 | log.Println(err) 101 | } 102 | } 103 | }(service) 104 | } 105 | } 106 | } else { 107 | te, err := tuna.NewTunaExit(services, wallet, nil, config) 108 | if err != nil { 109 | log.Fatalln(err) 110 | } 111 | 112 | err = te.Start() 113 | if err != nil { 114 | log.Fatalln(err) 115 | } 116 | 117 | defer te.Close() 118 | } 119 | 120 | select {} 121 | } 122 | 123 | func init() { 124 | parser.AddCommand("exit", "Tuna exit mode", "Start tuna in exit mode", &exitCommand) 125 | } 126 | -------------------------------------------------------------------------------- /geo/aws.go: -------------------------------------------------------------------------------- 1 | package geo 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/nknorg/tuna/util" 14 | ) 15 | 16 | const ( 17 | AWSGeoUrl = "https://ip-ranges.amazonaws.com/ip-ranges.json" 18 | AWSExpired = 7 * 24 * time.Hour 19 | AWSFile = "aws-ip.json" 20 | ) 21 | 22 | type AWSProvider struct { 23 | Info *AWSGeoInfo 24 | fileName string 25 | url string 26 | expire time.Duration 27 | ready bool 28 | } 29 | 30 | type AWSGeoInfo struct { 31 | SyncToken string `json:"syncToken"` 32 | CreateDate string `json:"createDate"` 33 | Prefixes []AWSIPInfo `json:"prefixes"` 34 | } 35 | 36 | type AWSIPInfo struct { 37 | IPPrefix string `json:"ip_prefix"` 38 | Region string `json:"region"` 39 | Service string `json:"service"` 40 | NetworkBorderGroup string `json:"network_border_group"` 41 | Subnet *net.IPNet `json:"-"` 42 | } 43 | 44 | var AWSRegionMapping = map[string]string{ 45 | "us-east-1": "US", 46 | "us-east-2": "US", 47 | "us-west-1": "US", 48 | "us-west-2": "US", 49 | "af-south-1": "ZA", 50 | "ap-east-1": "HK", 51 | "ap-south-1": "IN", 52 | "ap-northeast-1": "JP", 53 | "ap-northeast-2": "KR", 54 | "ap-northeast-3": "JP", 55 | "ap-southeast-1": "SG", 56 | "ap-southeast-2": "AU", 57 | "ca-central-1": "CA", 58 | "eu-central-1": "DE", 59 | "eu-west-1": "IE", 60 | "eu-west-2": "GB", 61 | "eu-west-3": "FR", 62 | "eu-south-1": "IT", 63 | "eu-north-1": "SE", 64 | "me-south-1": "BH", 65 | "sa-east-1": "BR", 66 | } 67 | 68 | func NewAWSProvider(path string) *AWSProvider { 69 | return &AWSProvider{ 70 | url: AWSGeoUrl, 71 | fileName: filepath.Join(path, AWSFile), 72 | expire: AWSExpired, 73 | } 74 | } 75 | 76 | func (p *AWSProvider) MaybeUpdate() error { 77 | return p.MaybeUpdateContext(context.Background()) 78 | } 79 | 80 | func (p *AWSProvider) MaybeUpdateContext(ctx context.Context) error { 81 | geoLock.Lock() 82 | defer geoLock.Unlock() 83 | if !p.NeedUpdate() && p.Info != nil { 84 | return nil 85 | } 86 | if p.NeedUpdate() { 87 | log.Println("Updating AWS geo db") 88 | tmpFile, err := ioutil.TempFile(path.Dir(p.fileName), path.Base(p.fileName)+"-*") 89 | if err != nil { 90 | return err 91 | } 92 | defer os.Remove(tmpFile.Name()) 93 | defer tmpFile.Close() 94 | err = util.DownloadJsonFile(ctx, p.url, tmpFile.Name()) 95 | if err != nil { 96 | return err 97 | } 98 | err = util.CopyFile(tmpFile.Name(), p.fileName) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | err := util.ReadJSON(p.fileName, &p.Info) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | for idx := range p.Info.Prefixes { 109 | if len(p.Info.Prefixes[idx].IPPrefix) == 0 { 110 | continue 111 | } 112 | _, subnet, err := net.ParseCIDR(p.Info.Prefixes[idx].IPPrefix) 113 | if err != nil { 114 | log.Print(err) 115 | continue 116 | } 117 | p.Info.Prefixes[idx].Subnet = subnet 118 | } 119 | p.ready = true 120 | return nil 121 | } 122 | 123 | func (p *AWSProvider) GetLocation(ip string) (*Location, error) { 124 | loc, err := p.getLocationFromAWS(ip) 125 | if err != nil { 126 | return &emptyLocation, err 127 | } 128 | return loc, nil 129 | } 130 | 131 | func (p *AWSProvider) getLocationFromAWS(ip string) (*Location, error) { 132 | loc := parseAWS(ip, p.Info) 133 | return loc, nil 134 | } 135 | 136 | func parseAWS(ip string, info *AWSGeoInfo) *Location { 137 | loc := Location{} 138 | parsed := net.ParseIP(ip) 139 | for _, p := range info.Prefixes { 140 | if p.Subnet == nil { 141 | continue 142 | } 143 | if p.Subnet.Contains(parsed) { 144 | if code, ok := AWSRegionMapping[p.Region]; ok { 145 | loc.CountryCode = code 146 | loc.IP = ip 147 | break 148 | } 149 | } 150 | } 151 | return &loc 152 | } 153 | 154 | func (p *AWSProvider) SetReady(ready bool) { 155 | p.ready = ready 156 | } 157 | 158 | func (p *AWSProvider) Ready() bool { 159 | return p.ready 160 | } 161 | 162 | func (p *AWSProvider) FileName() string { 163 | return p.fileName 164 | } 165 | 166 | func (p *AWSProvider) DownloadUrl() string { 167 | return p.url 168 | } 169 | 170 | func (p *AWSProvider) LastUpdate() time.Time { 171 | return getModTime(p.fileName) 172 | } 173 | 174 | func (p *AWSProvider) NeedUpdate() bool { 175 | return time.Since(p.LastUpdate()) > p.expire 176 | } 177 | 178 | func (p *AWSProvider) SetFileName(name string) { 179 | p.fileName = name 180 | } 181 | -------------------------------------------------------------------------------- /geo/gcp.go: -------------------------------------------------------------------------------- 1 | package geo 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/nknorg/tuna/util" 14 | ) 15 | 16 | const ( 17 | GCPGeoUrl = "https://gstatic.nkn.org/ipranges/cloud.json" 18 | GCPExpired = 7 * 24 * time.Hour 19 | GCPFile = "gcp-ip.json" 20 | ) 21 | 22 | type GCPProvider struct { 23 | Info *GCPGeoInfo 24 | fileName string 25 | url string 26 | expire time.Duration 27 | ready bool 28 | } 29 | 30 | type GCPGeoInfo struct { 31 | SyncToken string `json:"syncToken"` 32 | CreationTime string `json:"creationTime"` 33 | Prefixes []GCPIPInfo `json:"prefixes"` 34 | } 35 | 36 | type GCPIPInfo struct { 37 | Ipv4Prefix string `json:"ipv4Prefix"` 38 | Service string `json:"service"` 39 | Scope string `json:"scope"` 40 | Subnet *net.IPNet `json:"-"` 41 | } 42 | 43 | var GCPScopeMapping = map[string]string{ 44 | "asia-east1": "TW", 45 | "asia-east2": "HK", 46 | "asia-northeast1": "JP", 47 | "asia-northeast2": "JP", 48 | "asia-northeast3": "KR", 49 | "asia-south1": "IN", 50 | "asia-southeast1": "SG", 51 | "asia-southeast2": "ID", 52 | "australia-southeast1": "AU", 53 | "europe-north1": "FI", 54 | "europe-west1": "BE", 55 | "europe-west2": "GB", 56 | "europe-west3": "DE", 57 | "europe-west4": "NL", 58 | "europe-west6": "CH", 59 | "northamerica-northeast1": "CA", 60 | "southamerica-east1": "BR", 61 | "us-central1": "US", 62 | "us-east1": "US", 63 | "us-east4": "US", 64 | "us-west1": "US", 65 | "us-west2": "US", 66 | "us-west3": "US", 67 | "us-west4": "US", 68 | } 69 | 70 | func NewGCPProvider(path string) *GCPProvider { 71 | return &GCPProvider{ 72 | url: GCPGeoUrl, 73 | fileName: filepath.Join(path, GCPFile), 74 | expire: GCPExpired, 75 | } 76 | } 77 | 78 | func (p *GCPProvider) MaybeUpdate() error { 79 | return p.MaybeUpdateContext(context.Background()) 80 | } 81 | 82 | func (p *GCPProvider) MaybeUpdateContext(ctx context.Context) error { 83 | geoLock.Lock() 84 | defer geoLock.Unlock() 85 | if !p.NeedUpdate() && p.Info != nil { 86 | return nil 87 | } 88 | if p.NeedUpdate() { 89 | log.Println("Updating GCP geo db") 90 | tmpFile, err := ioutil.TempFile(path.Dir(p.fileName), path.Base(p.fileName)+"-*") 91 | if err != nil { 92 | return err 93 | } 94 | defer os.Remove(tmpFile.Name()) 95 | defer tmpFile.Close() 96 | err = util.DownloadJsonFile(ctx, p.url, tmpFile.Name()) 97 | if err != nil { 98 | return err 99 | } 100 | err = util.CopyFile(tmpFile.Name(), p.fileName) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | err := util.ReadJSON(p.fileName, &p.Info) 106 | if err != nil { 107 | return err 108 | } 109 | for idx := range p.Info.Prefixes { 110 | if len(p.Info.Prefixes[idx].Ipv4Prefix) == 0 { 111 | continue 112 | } 113 | _, subnet, err := net.ParseCIDR(p.Info.Prefixes[idx].Ipv4Prefix) 114 | if err != nil { 115 | log.Print(err) 116 | continue 117 | } 118 | p.Info.Prefixes[idx].Subnet = subnet 119 | } 120 | p.ready = true 121 | return nil 122 | } 123 | 124 | func (p *GCPProvider) GetLocation(ip string) (*Location, error) { 125 | loc, err := p.getLocationFromGCP(ip) 126 | if err != nil { 127 | return &emptyLocation, err 128 | } 129 | return loc, nil 130 | } 131 | 132 | func (p *GCPProvider) getLocationFromGCP(ip string) (*Location, error) { 133 | loc := parseGCP(ip, p.Info) 134 | return loc, nil 135 | } 136 | 137 | func parseGCP(ip string, info *GCPGeoInfo) *Location { 138 | loc := Location{} 139 | parsed := net.ParseIP(ip) 140 | for _, p := range info.Prefixes { 141 | if p.Subnet == nil { 142 | continue 143 | } 144 | if p.Subnet.Contains(parsed) { 145 | if code, ok := GCPScopeMapping[p.Scope]; ok { 146 | loc.CountryCode = code 147 | loc.IP = ip 148 | break 149 | } 150 | } 151 | } 152 | return &loc 153 | } 154 | 155 | func (p *GCPProvider) SetReady(ready bool) { 156 | p.ready = ready 157 | } 158 | 159 | func (p *GCPProvider) Ready() bool { 160 | return p.ready 161 | } 162 | 163 | func (p *GCPProvider) FileName() string { 164 | return p.fileName 165 | } 166 | 167 | func (p *GCPProvider) DownloadUrl() string { 168 | return p.url 169 | } 170 | 171 | func (p *GCPProvider) LastUpdate() time.Time { 172 | return getModTime(p.fileName) 173 | } 174 | 175 | func (p *GCPProvider) NeedUpdate() bool { 176 | return time.Since(p.LastUpdate()) > p.expire 177 | } 178 | 179 | func (p *GCPProvider) SetFileName(name string) { 180 | p.fileName = name 181 | } 182 | -------------------------------------------------------------------------------- /geo/geo.go: -------------------------------------------------------------------------------- 1 | package geo 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type GeoProvider interface { 15 | GetLocation(ip string) (*Location, error) 16 | FileName() string 17 | DownloadUrl() string 18 | LastUpdate() time.Time 19 | NeedUpdate() bool 20 | MaybeUpdate() error 21 | MaybeUpdateContext(ctx context.Context) error 22 | Ready() bool 23 | SetReady(bool) 24 | SetFileName(string) 25 | } 26 | 27 | type Location struct { 28 | IP string `json:"ip"` 29 | CountryCode string `json:"countryCode"` 30 | Country string `json:"country"` 31 | City string `json:"city"` 32 | cidr *net.IPNet 33 | } 34 | 35 | var emptyLocation = Location{} 36 | var geoLock sync.Mutex 37 | 38 | func (l *Location) Empty() bool { 39 | if l == nil { 40 | return true 41 | } 42 | return *l == emptyLocation 43 | } 44 | 45 | func (l *Location) Match(location *Location) bool { 46 | if len(l.IP) > 0 { 47 | if l.cidr == nil { 48 | matched, err := regexp.MatchString(`/\d{1,2}`, l.IP) 49 | if err != nil { 50 | log.Println(err) 51 | return false 52 | } 53 | if !matched { 54 | l.IP += "/32" 55 | } 56 | _, subnet, err := net.ParseCIDR(l.IP) 57 | if err != nil { 58 | log.Println(err) 59 | return false 60 | } 61 | l.cidr = subnet 62 | } 63 | 64 | if l.cidr.Contains(net.ParseIP(location.IP)) { 65 | return true 66 | } 67 | return false 68 | } 69 | 70 | if len(l.CountryCode) > 0 && strings.ToLower(location.CountryCode) == strings.ToLower(l.CountryCode) { 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | type IPFilter struct { 77 | Allow []Location `json:"allow"` 78 | Disallow []Location `json:"disallow"` 79 | providers []GeoProvider 80 | dbPath string 81 | downloadDB bool 82 | } 83 | 84 | func (f *IPFilter) Empty() bool { 85 | if f == nil { 86 | return true 87 | } 88 | for _, loc := range f.Allow { 89 | if !loc.Empty() { 90 | return false 91 | } 92 | } 93 | for _, loc := range f.Disallow { 94 | if !loc.Empty() { 95 | return false 96 | } 97 | } 98 | return true 99 | } 100 | 101 | func (f *IPFilter) NeedGeoInfo() bool { 102 | if f.Empty() { 103 | return false 104 | } 105 | for _, loc := range f.Allow { 106 | if len(loc.CountryCode) > 0 || len(loc.Country) > 0 || len(loc.City) > 0 { 107 | return true 108 | } 109 | } 110 | for _, loc := range f.Disallow { 111 | if len(loc.CountryCode) > 0 || len(loc.Country) > 0 || len(loc.City) > 0 { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | 118 | func (f *IPFilter) AllowIP(ip string) (bool, error) { 119 | if f.Empty() { 120 | return true, nil 121 | } 122 | 123 | var loc *Location 124 | if f.NeedGeoInfo() { 125 | loc = f.GetLocation(ip) 126 | } else { 127 | loc = &Location{IP: ip} 128 | } 129 | 130 | return f.AllowLocation(loc), nil 131 | } 132 | 133 | func (f *IPFilter) GetLocation(ip string) *Location { 134 | for _, p := range f.providers { 135 | if p.Ready() { 136 | loc := getLocationFromProvider(ip, p) 137 | if !loc.Empty() { 138 | return &loc 139 | } 140 | } 141 | } 142 | return &Location{CountryCode: "UNKNOWN", IP: ip} 143 | } 144 | 145 | func (f *IPFilter) AllowLocation(loc *Location) bool { 146 | if loc.Empty() { 147 | return true 148 | } 149 | 150 | for _, l := range f.Disallow { 151 | if l.Match(loc) { 152 | log.Printf("%s from %s dropped", loc.IP, loc.CountryCode) 153 | return false 154 | } 155 | } 156 | 157 | empty := true 158 | for _, l := range f.Allow { 159 | if l.Match(loc) { 160 | log.Printf("%s from %s passed", loc.IP, loc.CountryCode) 161 | return true 162 | } 163 | if !l.Empty() { 164 | empty = false 165 | } 166 | } 167 | 168 | return empty 169 | } 170 | 171 | func (f *IPFilter) AddProvider(download bool, path string) { 172 | f.downloadDB = download 173 | f.dbPath = path 174 | if f.downloadDB { 175 | aws := NewAWSProvider(f.dbPath) 176 | gcp := NewGCPProvider(f.dbPath) 177 | mm := NewMaxMindProvider(f.dbPath) 178 | f.providers = []GeoProvider{aws, gcp, mm} 179 | } 180 | 181 | ip2c := NewIP2CProvider() 182 | f.providers = append(f.providers, ip2c) 183 | } 184 | 185 | func (f *IPFilter) GetProviders() []GeoProvider { 186 | return f.providers 187 | } 188 | 189 | func (f *IPFilter) UpdateDataFile() { 190 | f.UpdateDataFileContext(context.Background()) 191 | } 192 | 193 | func (f *IPFilter) UpdateDataFileContext(ctx context.Context) { 194 | for _, p := range f.providers { 195 | if len(p.FileName()) == 0 { 196 | continue 197 | } 198 | err := p.MaybeUpdateContext(ctx) 199 | if err != nil { 200 | log.Print(err) 201 | continue 202 | } 203 | } 204 | } 205 | 206 | func (f *IPFilter) StartUpdateDataFile(c chan struct{}) { 207 | for { 208 | select { 209 | case _, ok := <-c: 210 | if !ok { 211 | return 212 | } 213 | default: 214 | f.UpdateDataFile() 215 | } 216 | time.Sleep(1 * time.Hour) 217 | } 218 | } 219 | 220 | func getLocationFromProvider(ip string, p GeoProvider) Location { 221 | loc, err := p.GetLocation(ip) 222 | if err != nil { 223 | log.Println(err) 224 | } 225 | return *loc 226 | } 227 | 228 | func getModTime(fileName string) time.Time { 229 | fs, err := os.Stat(fileName) 230 | if err != nil { 231 | if !os.IsNotExist(err) { 232 | log.Print(err) 233 | } 234 | return time.Time{} 235 | } 236 | return fs.ModTime() 237 | } 238 | -------------------------------------------------------------------------------- /tests/geo_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nknorg/tuna/geo" 7 | ) 8 | 9 | type testCase struct { 10 | f geo.IPFilter 11 | location geo.Location 12 | result bool 13 | } 14 | 15 | type testGeoCase struct { 16 | IP string 17 | Country string 18 | } 19 | 20 | var IP1 = "1.0.0.0" 21 | var IP2 = "2.0.0.0" 22 | var IP3 = "3.0.0.0" 23 | var IP4 = "4.0.0.0" 24 | 25 | var testData = []testCase{ 26 | { 27 | f: geo.IPFilter{ 28 | Allow: []geo.Location{}, 29 | Disallow: []geo.Location{{IP: IP1}}, 30 | }, 31 | location: geo.Location{IP: IP3}, 32 | result: true, 33 | }, 34 | { 35 | f: geo.IPFilter{ 36 | Allow: []geo.Location{}, 37 | Disallow: []geo.Location{{}}, 38 | }, 39 | location: geo.Location{IP: IP3}, 40 | result: true, 41 | }, 42 | { 43 | f: geo.IPFilter{ 44 | Allow: []geo.Location{{}}, 45 | Disallow: []geo.Location{{}}, 46 | }, 47 | location: geo.Location{IP: IP3}, 48 | result: true, 49 | }, 50 | { 51 | f: geo.IPFilter{ 52 | Disallow: []geo.Location{{IP: IP1}}, 53 | }, 54 | location: geo.Location{IP: IP1}, 55 | result: false, 56 | }, 57 | { 58 | f: geo.IPFilter{ 59 | Allow: []geo.Location{}, 60 | Disallow: []geo.Location{}, 61 | }, 62 | location: geo.Location{IP: IP1}, 63 | result: true, 64 | }, 65 | { 66 | f: geo.IPFilter{ 67 | Allow: []geo.Location{{IP: IP1}}, 68 | Disallow: []geo.Location{{IP: IP1}}, 69 | }, 70 | location: geo.Location{IP: IP1}, 71 | result: false, 72 | }, 73 | { 74 | f: geo.IPFilter{ 75 | Allow: []geo.Location{{IP: IP1}}, 76 | Disallow: []geo.Location{{IP: IP2}}, 77 | }, 78 | location: geo.Location{}, 79 | result: true, 80 | }, 81 | { 82 | f: geo.IPFilter{ 83 | Disallow: []geo.Location{{IP: IP3}}, 84 | }, 85 | location: geo.Location{IP: IP4}, 86 | result: true, 87 | }, 88 | { 89 | f: geo.IPFilter{ 90 | Allow: []geo.Location{{IP: IP3}}, 91 | }, 92 | location: geo.Location{IP: IP3}, 93 | result: true, 94 | }, 95 | { 96 | f: geo.IPFilter{ 97 | Allow: []geo.Location{}, 98 | Disallow: []geo.Location{}, 99 | }, 100 | location: geo.Location{}, 101 | result: true, 102 | }, 103 | { 104 | f: geo.IPFilter{ 105 | Allow: []geo.Location{{IP: IP1}}, 106 | Disallow: []geo.Location{{IP: IP2}}, 107 | }, 108 | location: geo.Location{IP: IP3}, 109 | result: false, 110 | }, 111 | { 112 | f: geo.IPFilter{ 113 | Allow: []geo.Location{{IP: IP1}, {IP: IP2}}, 114 | }, 115 | location: geo.Location{IP: IP3}, 116 | result: false, 117 | }, 118 | { 119 | f: geo.IPFilter{ 120 | Allow: []geo.Location{{IP: IP1}, {}}, 121 | }, 122 | location: geo.Location{IP: IP1}, 123 | result: true, 124 | }, 125 | { 126 | f: geo.IPFilter{ 127 | Allow: []geo.Location{{IP: IP1}, {}}, 128 | }, 129 | location: geo.Location{IP: IP2}, 130 | result: false, 131 | }, 132 | { 133 | f: geo.IPFilter{ 134 | Disallow: []geo.Location{{}, {IP: IP1}}, 135 | }, 136 | location: geo.Location{IP: IP1}, 137 | result: false, 138 | }, 139 | { 140 | f: geo.IPFilter{ 141 | Disallow: []geo.Location{{IP: IP1}, {}}, 142 | }, 143 | location: geo.Location{IP: IP2}, 144 | result: true, 145 | }, 146 | { 147 | f: geo.IPFilter{ 148 | Allow: []geo.Location{{CountryCode: "US"}}, 149 | }, 150 | location: geo.Location{CountryCode: "US"}, 151 | result: true, 152 | }, 153 | { 154 | f: geo.IPFilter{ 155 | Disallow: []geo.Location{{CountryCode: "US"}}, 156 | }, 157 | location: geo.Location{CountryCode: "US"}, 158 | result: false, 159 | }, 160 | { 161 | f: geo.IPFilter{ 162 | Disallow: []geo.Location{{CountryCode: "US"}}, 163 | }, 164 | location: geo.Location{CountryCode: "CA"}, 165 | result: true, 166 | }, 167 | { 168 | f: geo.IPFilter{ 169 | Allow: []geo.Location{{CountryCode: "US"}}, 170 | }, 171 | location: geo.Location{CountryCode: "CA"}, 172 | result: false, 173 | }, 174 | { 175 | f: geo.IPFilter{ 176 | Allow: []geo.Location{{CountryCode: "US"}}, 177 | }, 178 | location: geo.Location{}, 179 | result: true, 180 | }, 181 | { 182 | f: geo.IPFilter{ 183 | Disallow: []geo.Location{{CountryCode: "US"}}, 184 | }, 185 | location: geo.Location{}, 186 | result: true, 187 | }, 188 | { 189 | f: geo.IPFilter{ 190 | Disallow: []geo.Location{{CountryCode: "US"}}, 191 | }, 192 | location: geo.Location{CountryCode: "UNKNOWN"}, 193 | result: true, 194 | }, 195 | { 196 | f: geo.IPFilter{ 197 | Allow: []geo.Location{}, 198 | Disallow: []geo.Location{}, 199 | }, 200 | location: geo.Location{CountryCode: "US"}, 201 | result: true, 202 | }, 203 | { 204 | f: geo.IPFilter{ 205 | Allow: []geo.Location{}, 206 | Disallow: []geo.Location{}, 207 | }, 208 | location: geo.Location{}, 209 | result: true, 210 | }, 211 | } 212 | 213 | var testGeoData = []testGeoCase{ 214 | { 215 | IP: "34.68.157.1", // Google Cloud 216 | Country: "US", 217 | }, 218 | { 219 | IP: "34.68.152.156", // Google Cloud 220 | Country: "US", 221 | }, 222 | { 223 | IP: "52.12.134.239", // AWS 224 | Country: "US", 225 | }, 226 | { 227 | IP: "172.104.210.83", // Linode 228 | Country: "US", 229 | }, 230 | { 231 | IP: "207.246.120.132", // Vultr 232 | Country: "US", 233 | }, 234 | { 235 | IP: "52.194.247.186", // AWS 236 | Country: "JP", 237 | }, 238 | { 239 | IP: "144.91.100.54", // Contabo 240 | Country: "DE", 241 | }, 242 | { 243 | IP: "3.9.118.165", // AWS 244 | Country: "GB", 245 | }, 246 | { 247 | IP: "161.35.59.158", // DO 248 | Country: "US", 249 | }, 250 | } 251 | 252 | func TestLocationCheck(t *testing.T) { 253 | for num, data := range testData { 254 | res := data.f.AllowLocation(&data.location) 255 | if res != data.result { 256 | t.Fatalf("NO %d testcase failed", num+1) 257 | } 258 | } 259 | } 260 | 261 | func TestGetLocations(t *testing.T) { 262 | filter := &geo.IPFilter{} 263 | filter.AddProvider(true, ".") 264 | c := make(chan struct{}) 265 | go filter.StartUpdateDataFile(c) 266 | for _, data := range testGeoData { 267 | loc := filter.GetLocation(data.IP) 268 | if loc.CountryCode != data.Country { 269 | t.Fatal(data) 270 | } 271 | } 272 | close(c) 273 | } 274 | -------------------------------------------------------------------------------- /udp.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | stream "github.com/nknorg/encrypted-stream" 12 | "github.com/nknorg/tuna/pb" 13 | ) 14 | 15 | const ( 16 | PrefixLen = 4 17 | DefaultUDPBufferSize = 8192 18 | MaxUDPBufferSize = 65527 19 | ) 20 | 21 | type UDPConn interface { 22 | WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) 23 | ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) 24 | 25 | Close() error 26 | 27 | LocalAddr() net.Addr 28 | RemoteAddr() net.Addr 29 | 30 | SetWriteBuffer(bytes int) error 31 | SetReadBuffer(bytes int) error 32 | } 33 | 34 | type EncryptUDPConn struct { 35 | conn UDPConn 36 | 37 | encoders sync.Map 38 | decoders sync.Map 39 | 40 | lock sync.RWMutex 41 | isClosed bool 42 | 43 | readLock sync.Mutex 44 | writeLock sync.Mutex 45 | 46 | readBuffer []byte 47 | writeBuffer []byte 48 | } 49 | 50 | func NewEncryptUDPConn(conn *net.UDPConn) *EncryptUDPConn { 51 | ec := &EncryptUDPConn{ 52 | conn: conn, 53 | readBuffer: make([]byte, MaxUDPBufferSize), 54 | writeBuffer: make([]byte, MaxUDPBufferSize), 55 | } 56 | conn.SetReadBuffer(MaxUDPBufferSize) 57 | conn.SetWriteBuffer(MaxUDPBufferSize) 58 | return ec 59 | } 60 | 61 | func (ec *EncryptUDPConn) AddCodec(addr *net.UDPAddr, encryptKey *[32]byte, encryptionAlgo pb.EncryptionAlgo, initiator bool) error { 62 | var cipher stream.Cipher 63 | var err error 64 | switch encryptionAlgo { 65 | case pb.EncryptionAlgo_ENCRYPTION_NONE: 66 | cipher = nil 67 | case pb.EncryptionAlgo_ENCRYPTION_XSALSA20_POLY1305: 68 | cipher = stream.NewXSalsa20Poly1305Cipher(encryptKey) 69 | case pb.EncryptionAlgo_ENCRYPTION_AES_GCM: 70 | cipher, err = stream.NewAESGCMCipher(encryptKey[:]) 71 | if err != nil { 72 | return err 73 | } 74 | default: 75 | return fmt.Errorf("unsupported encryption algo %v", encryptionAlgo) 76 | } 77 | encoder, err := stream.NewEncoder(cipher, initiator, false) 78 | if err != nil { 79 | return err 80 | } 81 | decoder, err := stream.NewDecoder(cipher, initiator, false, true) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | ec.encoders.Store(addr.String(), encoder) 87 | ec.decoders.Store(addr.String(), decoder) 88 | return nil 89 | } 90 | 91 | func (ec *EncryptUDPConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { 92 | n, addr, _, err = ec.ReadFromUDPEncrypted(b) 93 | return 94 | } 95 | 96 | func (ec *EncryptUDPConn) ReadFromUDPEncrypted(b []byte) (n int, addr *net.UDPAddr, encrypted bool, err error) { 97 | if ec == nil { 98 | return 0, nil, false, fmt.Errorf("unconnected udp conn") 99 | } 100 | 101 | if ec.IsClosed() { 102 | return 0, nil, false, io.ErrClosedPipe 103 | } 104 | 105 | ec.readLock.Lock() 106 | defer ec.readLock.Unlock() 107 | 108 | n, addr, err = ec.conn.ReadFromUDP(ec.readBuffer) 109 | if err != nil { 110 | return 0, addr, false, err 111 | } 112 | 113 | d, ok := ec.decoders.Load(addr.String()) 114 | if !ok { 115 | copy(b, ec.readBuffer[:n]) 116 | return n, addr, false, nil 117 | } 118 | decoder := d.(*stream.Decoder) 119 | 120 | plain, err := decoder.Decode(b, ec.readBuffer[:n]) 121 | if err != nil { 122 | return 0, addr, false, err 123 | } 124 | return len(plain), addr, true, nil 125 | } 126 | 127 | func (ec *EncryptUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) { 128 | n, oobn, _, err = ec.WriteMsgUDPEncrypted(b, oob, addr) 129 | return 130 | } 131 | 132 | func (ec *EncryptUDPConn) WriteMsgUDPEncrypted(b, oob []byte, addr *net.UDPAddr) (n, oobn int, encrypted bool, err error) { 133 | if ec == nil { 134 | return 0, 0, false, fmt.Errorf("unconnected udp conn") 135 | } 136 | 137 | if ec.IsClosed() { 138 | return 0, 0, false, io.ErrClosedPipe 139 | } 140 | 141 | ec.writeLock.Lock() 142 | defer ec.writeLock.Unlock() 143 | 144 | var k string 145 | var ciphertext []byte 146 | encrypted = false 147 | 148 | if addr == nil { 149 | k = ec.RemoteUDPAddr().String() 150 | } else { 151 | k = addr.String() 152 | } 153 | e, ok := ec.encoders.Load(k) 154 | if !ok { 155 | ciphertext = b 156 | } else { 157 | encoder := e.(*stream.Encoder) 158 | ciphertext, err = encoder.Encode(ec.writeBuffer, b) 159 | if err != nil { 160 | return 0, 0, false, err 161 | } 162 | encrypted = true 163 | } 164 | 165 | n, oobn, err = ec.conn.WriteMsgUDP(ciphertext, oob, addr) 166 | if err != nil { 167 | return 0, 0, false, err 168 | } 169 | if n != len(ciphertext) { 170 | return 0, 0, false, io.ErrShortWrite 171 | } 172 | 173 | return len(b), oobn, encrypted, err 174 | } 175 | 176 | func (ec *EncryptUDPConn) SetWriteBuffer(size int) error { 177 | return ec.conn.SetWriteBuffer(size) 178 | } 179 | 180 | func (ec *EncryptUDPConn) SetReadBuffer(size int) error { 181 | return ec.conn.SetReadBuffer(size) 182 | } 183 | 184 | func (ec *EncryptUDPConn) LocalAddr() net.Addr { 185 | return ec.conn.LocalAddr() 186 | } 187 | 188 | func (ec *EncryptUDPConn) IsClosed() bool { 189 | ec.lock.RLock() 190 | defer ec.lock.RUnlock() 191 | return ec.isClosed 192 | } 193 | 194 | func (ec *EncryptUDPConn) Close() error { 195 | ec.lock.Lock() 196 | defer ec.lock.Unlock() 197 | ec.isClosed = true 198 | return ec.conn.Close() 199 | } 200 | 201 | func (ec *EncryptUDPConn) RemoteAddr() net.Addr { 202 | return ec.conn.RemoteAddr() 203 | } 204 | 205 | func (ec *EncryptUDPConn) RemoteUDPAddr() *net.UDPAddr { 206 | host, portStr, _ := net.SplitHostPort(ec.RemoteAddr().String()) 207 | port, _ := strconv.Atoi(portStr) 208 | return &net.UDPAddr{IP: net.ParseIP(host), Port: port} 209 | } 210 | 211 | func (ec *EncryptUDPConn) SetDeadline(t time.Time) error { 212 | ec.lock.RLock() 213 | defer ec.lock.RUnlock() 214 | 215 | if ec.isClosed { 216 | return ErrClosed 217 | } 218 | 219 | c := ec.conn.(*net.UDPConn) 220 | return c.SetDeadline(t) 221 | } 222 | 223 | func (ec *EncryptUDPConn) SetReadDeadline(t time.Time) error { 224 | ec.lock.RLock() 225 | defer ec.lock.RUnlock() 226 | 227 | if ec.isClosed { 228 | return ErrClosed 229 | } 230 | 231 | c := ec.conn.(*net.UDPConn) 232 | return c.SetReadDeadline(t) 233 | } 234 | 235 | func (ec *EncryptUDPConn) SetWriteDeadline(t time.Time) error { 236 | ec.lock.RLock() 237 | defer ec.lock.RUnlock() 238 | 239 | if ec.isClosed { 240 | return ErrClosed 241 | } 242 | 243 | c := ec.conn.(*net.UDPConn) 244 | return c.SetWriteDeadline(t) 245 | } 246 | -------------------------------------------------------------------------------- /storage/measure.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "net" 8 | "path/filepath" 9 | "sync" 10 | "time" 11 | 12 | "github.com/nknorg/tuna/util" 13 | ) 14 | 15 | const ( 16 | maxFavoriteLength = 8 17 | maskSize = 16 18 | favoriteExpired = 365 * 24 * time.Hour 19 | avoidExpired = 7 * 24 * time.Hour 20 | avoidCIDRMinIP = 3 21 | 22 | FavoriteFileSuffix = ".favorite-node.json" 23 | AvoidFileSuffix = ".avoid-node.json" 24 | ) 25 | 26 | var ( 27 | // file lock is global variable so it's shared among multiple tuna instance 28 | avoidNodeFileMutex sync.RWMutex 29 | favoriteNodeFileMutex sync.RWMutex 30 | ) 31 | 32 | type FavoriteNode struct { 33 | IP string `json:"ip"` 34 | Address string `json:"address"` 35 | Metadata string `json:"metadata"` 36 | Delay float32 `json:"delay"` 37 | MinBandwidth float32 `json:"minBandwidth"` 38 | MaxBandwidth float32 `json:"maxBandwidth"` 39 | ExpiresAt int64 `json:"expiredAt"` 40 | } 41 | 42 | type AvoidNodes = map[string]*AvoidNode 43 | 44 | type AvoidNode struct { 45 | IP string `json:"ip"` 46 | MaskSize int32 `json:"maskSize"` 47 | Address string `json:"address"` 48 | ExpiresAt int64 `json:"expiredAt"` 49 | } 50 | 51 | type MeasureStorage struct { 52 | path string 53 | favoriteFilePath string 54 | avoidFilePath string 55 | 56 | FavoriteNodes *Storage 57 | 58 | avoidNodeMutex sync.RWMutex 59 | AvoidNodes map[string]AvoidNodes 60 | } 61 | 62 | func NewMeasureStorage(path, filenamePrefix string) *MeasureStorage { 63 | return &MeasureStorage{ 64 | path: path, 65 | favoriteFilePath: filepath.Join(path, filenamePrefix+FavoriteFileSuffix), 66 | avoidFilePath: filepath.Join(path, filenamePrefix+AvoidFileSuffix), 67 | } 68 | } 69 | 70 | // Load must be called before all other methods 71 | func (s *MeasureStorage) Load() error { 72 | err := s.loadFavoriteData() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | err = s.loadAvoidData() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | err = s.ClearFavoriteExpired() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = s.ClearAvoidExpired() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (s *MeasureStorage) loadFavoriteData() error { 96 | favoriteNodeFileMutex.Lock() 97 | defer favoriteNodeFileMutex.Unlock() 98 | 99 | favoriteData := make(map[string]*FavoriteNode) 100 | if util.Exists(s.favoriteFilePath) { 101 | err := util.ReadJSON(s.favoriteFilePath, &favoriteData) 102 | if err != nil { 103 | err = util.WriteJSON(s.favoriteFilePath, favoriteData) 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | } 109 | 110 | s.FavoriteNodes = NewStorage() 111 | for k, v := range favoriteData { 112 | s.FavoriteNodes.Add(k, v) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func (s *MeasureStorage) loadAvoidData() error { 119 | avoidNodeFileMutex.Lock() 120 | defer avoidNodeFileMutex.Unlock() 121 | 122 | avoidData := make(map[string]AvoidNodes) 123 | if util.Exists(s.avoidFilePath) { 124 | err := util.ReadJSON(s.avoidFilePath, &avoidData) 125 | if err != nil { 126 | err = util.WriteJSON(s.avoidFilePath, avoidData) 127 | if err != nil { 128 | return err 129 | } 130 | } 131 | } 132 | 133 | s.AvoidNodes = avoidData 134 | 135 | return nil 136 | } 137 | 138 | func (s *MeasureStorage) ClearFavoriteExpired() error { 139 | for k, v := range s.FavoriteNodes.GetData() { 140 | if time.Now().Unix() > v.(*FavoriteNode).ExpiresAt { 141 | s.FavoriteNodes.Delete(k) 142 | } 143 | } 144 | return s.SaveFavoriteNodes() 145 | } 146 | 147 | func (s *MeasureStorage) ClearAvoidExpired() error { 148 | s.avoidNodeMutex.Lock() 149 | for k1, v1 := range s.AvoidNodes { 150 | for k2, v2 := range v1 { 151 | if time.Now().Unix() > v2.ExpiresAt { 152 | delete(v1, k2) 153 | } 154 | } 155 | if len(v1) == 0 { 156 | delete(s.AvoidNodes, k1) 157 | } 158 | } 159 | // Unlock must be called before save to avoid deadlock 160 | s.avoidNodeMutex.Unlock() 161 | return s.SaveAvoidNodes() 162 | } 163 | 164 | func (s *MeasureStorage) SaveFavoriteNodes() error { 165 | favoriteNodeFileMutex.Lock() 166 | defer favoriteNodeFileMutex.Unlock() 167 | err := util.WriteJSON(s.favoriteFilePath, s.FavoriteNodes.GetData()) 168 | if err != nil { 169 | return err 170 | } 171 | return nil 172 | } 173 | 174 | func (s *MeasureStorage) SaveAvoidNodes() error { 175 | s.avoidNodeMutex.RLock() 176 | defer s.avoidNodeMutex.RUnlock() 177 | avoidNodeFileMutex.Lock() 178 | defer avoidNodeFileMutex.Unlock() 179 | err := util.WriteJSON(s.avoidFilePath, s.AvoidNodes) 180 | if err != nil { 181 | return err 182 | } 183 | return nil 184 | } 185 | 186 | func (s *MeasureStorage) AddFavoriteNode(key string, val *FavoriteNode) bool { 187 | if val.ExpiresAt == 0 { 188 | val.ExpiresAt = time.Now().Add(favoriteExpired).Unix() 189 | } 190 | 191 | if s.FavoriteNodes.Len() >= maxFavoriteLength { 192 | minBandwidth := float32(0) 193 | for _, v := range s.FavoriteNodes.GetData() { 194 | item := v.(*FavoriteNode) 195 | if item.MinBandwidth < minBandwidth || minBandwidth == 0 { 196 | minBandwidth = item.MinBandwidth 197 | } 198 | } 199 | if val.MinBandwidth > minBandwidth { 200 | deleteKey := "" 201 | minExpire := int64(math.MaxInt32) 202 | for k, v := range s.FavoriteNodes.GetData() { 203 | item := v.(*FavoriteNode) 204 | if item.ExpiresAt < minExpire { 205 | minExpire = item.ExpiresAt 206 | deleteKey = k 207 | } 208 | } 209 | s.FavoriteNodes.Delete(deleteKey) 210 | s.FavoriteNodes.Add(key, val) 211 | return true 212 | } 213 | } else { 214 | s.FavoriteNodes.Add(key, val) 215 | return true 216 | } 217 | return false 218 | } 219 | 220 | func (s *MeasureStorage) AddAvoidNode(key string, val *AvoidNode) { 221 | if val.ExpiresAt == 0 { 222 | val.ExpiresAt = time.Now().Add(avoidExpired).Unix() 223 | } 224 | 225 | if val.MaskSize == 0 { 226 | val.MaskSize = maskSize 227 | } 228 | 229 | _, subnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", key, val.MaskSize)) 230 | if err != nil { 231 | log.Println(err) 232 | return 233 | } 234 | 235 | s.avoidNodeMutex.Lock() 236 | defer s.avoidNodeMutex.Unlock() 237 | 238 | if _, ok := s.AvoidNodes[subnet.String()]; ok { 239 | s.AvoidNodes[subnet.String()][key] = val 240 | } else { 241 | s.AvoidNodes[subnet.String()] = map[string]*AvoidNode{ 242 | val.IP: val, 243 | } 244 | } 245 | } 246 | 247 | func (s *MeasureStorage) GetAvoidCIDR() []*net.IPNet { 248 | s.avoidNodeMutex.RLock() 249 | defer s.avoidNodeMutex.RUnlock() 250 | var results []*net.IPNet 251 | for k, v := range s.AvoidNodes { 252 | if len(v) > avoidCIDRMinIP { 253 | _, subnet, err := net.ParseCIDR(k) 254 | if err != nil { 255 | log.Printf("parseCIDR error: %s", k) 256 | continue 257 | } 258 | results = append(results, subnet) 259 | } 260 | } 261 | return results 262 | } 263 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "log" 9 | "math/rand" 10 | "net" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/nknorg/nkn-sdk-go" 16 | "github.com/nknorg/nkn/v2/common" 17 | nknPb "github.com/nknorg/nkn/v2/pb" 18 | "github.com/nknorg/tuna/pb" 19 | "github.com/nknorg/tuna/storage" 20 | "github.com/xtaci/smux" 21 | "google.golang.org/protobuf/proto" 22 | ) 23 | 24 | const ( 25 | nodeRPCPort = 30003 26 | randomIdentifierChars = "abcdefghijklmnopqrstuvwxyz0123456789" 27 | randomIdentifierLength = 8 28 | heartbeatInterval = 30 * time.Second 29 | ) 30 | 31 | var encryptionAlgoMap = map[string]pb.EncryptionAlgo{ 32 | "none": pb.EncryptionAlgo_ENCRYPTION_NONE, 33 | "xsalsa20-poly1305": pb.EncryptionAlgo_ENCRYPTION_XSALSA20_POLY1305, 34 | "aes-gcm": pb.EncryptionAlgo_ENCRYPTION_AES_GCM, 35 | } 36 | 37 | // OnConnectFunc is a wrapper type for gomobile compatibility. 38 | type OnConnectFunc interface{ OnConnect() } 39 | 40 | // OnConnect is a wrapper type for gomobile compatibility. 41 | type OnConnect struct { 42 | C chan struct{} 43 | Callback OnConnectFunc 44 | 45 | closeLock sync.RWMutex 46 | isClosed bool 47 | } 48 | 49 | // NewOnConnect creates an OnConnect channel with a channel size and callback 50 | // function. 51 | func NewOnConnect(size int, cb OnConnectFunc) *OnConnect { 52 | return &OnConnect{ 53 | C: make(chan struct{}, size), 54 | Callback: cb, 55 | } 56 | } 57 | 58 | // Next waits and returns the next element from the channel. 59 | func (c *OnConnect) Next() { 60 | <-c.C 61 | return 62 | } 63 | 64 | func (c *OnConnect) receive() { 65 | c.closeLock.RLock() 66 | if c.isClosed { 67 | c.closeLock.RUnlock() 68 | return 69 | } 70 | if c.Callback != nil { 71 | // RUnlock is called first to prevent OnConnect callback takeing long time 72 | c.closeLock.RUnlock() 73 | c.Callback.OnConnect() 74 | } else { 75 | select { 76 | case c.C <- struct{}{}: 77 | default: 78 | } 79 | c.closeLock.RUnlock() 80 | } 81 | } 82 | 83 | func (c *OnConnect) close() { 84 | c.closeLock.Lock() 85 | close(c.C) 86 | c.isClosed = true 87 | c.closeLock.Unlock() 88 | } 89 | 90 | func ParseEncryptionAlgo(encryptionAlgoStr string) (pb.EncryptionAlgo, error) { 91 | if encryptionAlgo, ok := encryptionAlgoMap[strings.ToLower(strings.TrimSpace(encryptionAlgoStr))]; ok { 92 | return encryptionAlgo, nil 93 | } 94 | return 0, fmt.Errorf("unknown encryption algo %v", encryptionAlgoStr) 95 | } 96 | 97 | func ParsePrice(priceStr string) (common.Fixed64, common.Fixed64, error) { 98 | price := strings.Split(priceStr, ",") 99 | entryToExitPrice, err := common.StringToFixed64(strings.Trim(price[0], " ")) 100 | if err != nil { 101 | return 0, 0, err 102 | } 103 | var exitToEntryPrice common.Fixed64 104 | if len(price) > 1 { 105 | exitToEntryPrice, err = common.StringToFixed64(strings.Trim(price[1], " ")) 106 | if err != nil { 107 | return 0, 0, err 108 | } 109 | } else { 110 | exitToEntryPrice = entryToExitPrice 111 | } 112 | return entryToExitPrice, exitToEntryPrice, nil 113 | } 114 | 115 | func ReadVarBytes(reader io.Reader, maxMsgSize uint32) ([]byte, error) { 116 | b := make([]byte, 4) 117 | _, err := io.ReadFull(reader, b) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | msgSize := binary.LittleEndian.Uint32(b) 123 | if msgSize > maxMsgSize { 124 | return nil, fmt.Errorf("msg size %d is larger than %d bytes", msgSize, maxMsgSize) 125 | } 126 | 127 | b = make([]byte, msgSize) 128 | _, err = io.ReadFull(reader, b) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return b, nil 134 | } 135 | 136 | func WriteVarBytes(writer io.Writer, b []byte) error { 137 | lenBuf := make([]byte, 4) 138 | binary.LittleEndian.PutUint32(lenBuf, uint32(len(b))) 139 | 140 | _, err := writer.Write(lenBuf) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | _, err = writer.Write(b) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func readConnMetadata(conn net.Conn) (*pb.ConnectionMetadata, error) { 154 | b, err := ReadVarBytes(conn, maxConnMetadataSize) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | connMetadata := &pb.ConnectionMetadata{} 160 | err = proto.Unmarshal(b, connMetadata) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return connMetadata, nil 166 | } 167 | 168 | func writeConnMetadata(conn net.Conn, connMetadata *pb.ConnectionMetadata) error { 169 | b, err := proto.Marshal(connMetadata) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | return WriteVarBytes(conn, b) 175 | } 176 | 177 | func parseUDPConnMetadata(metadata []byte) (*pb.ConnectionMetadata, error) { 178 | connMetadata := new(pb.ConnectionMetadata) 179 | err := proto.Unmarshal(metadata, connMetadata) 180 | if err != nil { 181 | return nil, err 182 | } 183 | return connMetadata, nil 184 | } 185 | 186 | func writeUDPConnMetadata(conn UDPConn, addr *net.UDPAddr, connMetadata *pb.ConnectionMetadata) error { 187 | b, err := proto.Marshal(connMetadata) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | _, _, err = conn.WriteMsgUDP(append([]byte{PrefixLen - 1: 0}, b...), nil, addr) 193 | return err 194 | } 195 | 196 | func sendPingMsg(conn UDPConn, closeChan chan struct{}) { 197 | pingMsg := new(pb.ConnectionMetadata) 198 | pingMsg.IsPing = true 199 | 200 | for { 201 | select { 202 | case <-closeChan: 203 | break 204 | default: 205 | } 206 | err := writeUDPConnMetadata(conn, nil, pingMsg) 207 | if err != nil { 208 | log.Println("write udp ping msg error:", err) 209 | break 210 | } 211 | time.Sleep(heartbeatInterval) 212 | } 213 | return 214 | } 215 | 216 | func readStreamMetadata(stream *smux.Stream) (*pb.StreamMetadata, error) { 217 | b, err := ReadVarBytes(stream, maxStreamMetadataSize) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | streamMetadata := &pb.StreamMetadata{} 223 | err = proto.Unmarshal(b, streamMetadata) 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | return streamMetadata, nil 229 | } 230 | 231 | func writeStreamMetadata(stream *smux.Stream, streamMetadata *pb.StreamMetadata) error { 232 | b, err := proto.Marshal(streamMetadata) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | err = WriteVarBytes(stream, b) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | return nil 243 | } 244 | 245 | // GetFavoriteSeedRPCServer wraps GetFavoriteSeedRPCServerContext with 246 | // background context. 247 | func GetFavoriteSeedRPCServer(path, filenamePrefix string, timeout int32, dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) ([]string, error) { 248 | return GetFavoriteSeedRPCServerContext(context.Background(), path, filenamePrefix, timeout, dialContext) 249 | } 250 | 251 | // GetFavoriteSeedRPCServerContext returns an array of node rpc address from 252 | // favorite node file. Timeout is in unit of millisecond. 253 | func GetFavoriteSeedRPCServerContext(ctx context.Context, path, filenamePrefix string, timeout int32, dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) ([]string, error) { 254 | measureStorage := storage.NewMeasureStorage(path, filenamePrefix) 255 | err := measureStorage.Load() 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | if measureStorage.FavoriteNodes.Len() == 0 { 261 | return nil, nil 262 | } 263 | 264 | var wg sync.WaitGroup 265 | var lock sync.Mutex 266 | rpcAddrs := make([]string, 0, measureStorage.FavoriteNodes.Len()) 267 | 268 | for _, node := range measureStorage.FavoriteNodes.GetData() { 269 | wg.Add(1) 270 | go func(addr string) { 271 | defer wg.Done() 272 | nodeState, err := nkn.GetNodeStateContext(ctx, &nkn.RPCConfig{ 273 | SeedRPCServerAddr: nkn.NewStringArray(addr), 274 | RPCTimeout: timeout, 275 | HttpDialContext: dialContext, 276 | }) 277 | if err != nil { 278 | return 279 | } 280 | if nodeState.SyncState != nknPb.SyncState_name[int32(nknPb.SyncState_PERSIST_FINISHED)] { 281 | log.Printf("Skip rpc node %s in state %s\n", addr, nodeState.SyncState) 282 | return 283 | } 284 | lock.Lock() 285 | rpcAddrs = append(rpcAddrs, addr) 286 | lock.Unlock() 287 | }(fmt.Sprintf("http://%s:%d", node.(*storage.FavoriteNode).IP, nodeRPCPort)) 288 | } 289 | 290 | done := make(chan struct{}) 291 | go func() { 292 | wg.Wait() 293 | close(done) 294 | }() 295 | 296 | select { 297 | case <-ctx.Done(): 298 | return nil, ctx.Err() 299 | case <-done: 300 | } 301 | 302 | return rpcAddrs, nil 303 | } 304 | 305 | func randomIdentifier() string { 306 | b := make([]byte, randomIdentifierLength) 307 | for i := range b { 308 | b[i] = randomIdentifierChars[rand.Intn(len(randomIdentifierChars))] 309 | } 310 | return string(b) 311 | } 312 | 313 | func closeChan(ch chan struct{}) { 314 | select { 315 | case _, _ = <-ch: 316 | return 317 | default: 318 | close(ch) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /tests/util.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "github.com/nknorg/nkn-sdk-go" 10 | "github.com/nknorg/nkn/v2/vault" 11 | "github.com/nknorg/tuna" 12 | "github.com/nknorg/tuna/pb" 13 | "github.com/nknorg/tuna/types" 14 | "github.com/nknorg/tuna/util" 15 | "io" 16 | "log" 17 | "net" 18 | "strconv" 19 | "sync" 20 | ) 21 | 22 | type Server struct { 23 | host string 24 | port string 25 | } 26 | 27 | func NewServer(host, port string) *Server { 28 | return &Server{ 29 | host: host, 30 | port: port, 31 | } 32 | } 33 | 34 | func (server *Server) RunTCPEchoServer() { 35 | listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", server.host, server.port)) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | defer listener.Close() 40 | 41 | for { 42 | conn, err := listener.Accept() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | go func(conn net.Conn) { 47 | defer func() { 48 | conn.Close() 49 | }() 50 | io.Copy(conn, conn) 51 | }(conn) 52 | } 53 | } 54 | 55 | func (server *Server) RunUDPEchoServer() { 56 | ip := net.ParseIP(server.host) 57 | port, _ := strconv.Atoi(server.port) 58 | conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip, Port: port}) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | buffer := make([]byte, 65536) 63 | for { 64 | n, addr, err := conn.ReadFromUDP(buffer) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | n, _, err = conn.WriteMsgUDP(buffer[:n], nil, addr) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | } 73 | } 74 | 75 | func runForwardEntry(seed, exitPubKey []byte, exitReady <-chan struct{}) error { 76 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...) 77 | 78 | walletConfig := &nkn.WalletConfig{ 79 | SeedRPCServerAddr: seedRPCServerAddr, 80 | } 81 | entryAccount, err := vault.NewAccountWithSeed(seed) 82 | if err != nil { 83 | return err 84 | } 85 | entryWallet, err := nkn.NewWallet(&nkn.Account{Account: entryAccount}, walletConfig) 86 | if err != nil { 87 | return err 88 | } 89 | entryConfig := new(tuna.EntryConfiguration) 90 | err = util.ReadJSON("config.forward.entry.json", entryConfig) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | var entryServices []tuna.Service 96 | err = util.ReadJSON("services.entry.json", &entryServices) 97 | 98 | <-exitReady 99 | 100 | for serviceName, serviceInfo := range entryConfig.Services { 101 | for _, service := range entryServices { 102 | if service.Name == serviceName { 103 | go func(service tuna.Service, serviceInfo tuna.ServiceInfo) { 104 | if len(service.UDP) > 0 && service.UDPBufferSize == 0 { 105 | service.UDPBufferSize = tuna.DefaultUDPBufferSize 106 | } 107 | 108 | te, err := tuna.NewTunaEntry(service, serviceInfo, entryWallet, nil, entryConfig) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | node := types.Node{ 113 | Delay: 0, 114 | Bandwidth: 0, 115 | Metadata: &pb.ServiceMetadata{ 116 | Ip: "127.0.0.1", 117 | TcpPort: 30010, 118 | UdpPort: 30011, 119 | ServiceId: 0, 120 | Price: "0.0", 121 | BeneficiaryAddr: "", 122 | }, 123 | Address: hex.EncodeToString(exitPubKey), 124 | MetadataRaw: "Cg4xOTIuMTY4LjMxLjIwNhC66gEYu+oBOgUwLjAwMQ==", 125 | } 126 | te.SetRemoteNode(&node) 127 | err = te.Start(false) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | }(service, serviceInfo) 132 | } 133 | } 134 | } 135 | select {} 136 | } 137 | 138 | func runForwardExit(seed []byte, ready chan<- struct{}) error { 139 | exitAccount, err := vault.NewAccountWithSeed(seed) 140 | if err != nil { 141 | return err 142 | } 143 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...) 144 | walletConfig := &nkn.WalletConfig{ 145 | SeedRPCServerAddr: seedRPCServerAddr, 146 | } 147 | exitWallet, err := nkn.NewWallet(&nkn.Account{Account: exitAccount}, walletConfig) 148 | if err != nil { 149 | return err 150 | } 151 | exitConfig := &tuna.ExitConfiguration{} 152 | err = util.ReadJSON("config.forward.exit.json", exitConfig) 153 | if err != nil { 154 | log.Fatal("Load exitConfig file error:", err) 155 | return err 156 | } 157 | var exitServices []tuna.Service 158 | err = util.ReadJSON("services.reverse.exit.json", &exitServices) 159 | if err != nil { 160 | log.Fatal("Load service file error:", err) 161 | return err 162 | } 163 | te, err := tuna.NewTunaExit(exitServices, exitWallet, nil, exitConfig) 164 | if err != nil { 165 | log.Fatal(err) 166 | return err 167 | } 168 | err = te.Start() 169 | log.Println("exit server start...") 170 | if err != nil { 171 | log.Fatal(err) 172 | return err 173 | } 174 | defer te.Close() 175 | 176 | close(ready) 177 | 178 | select {} 179 | } 180 | 181 | func runReverseEntry(seed []byte, ready chan<- struct{}) error { 182 | entryAccount, err := vault.NewAccountWithSeed(seed) 183 | if err != nil { 184 | return err 185 | } 186 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...) 187 | 188 | walletConfig := &nkn.WalletConfig{ 189 | SeedRPCServerAddr: seedRPCServerAddr, 190 | } 191 | entryWallet, err := nkn.NewWallet(&nkn.Account{Account: entryAccount}, walletConfig) 192 | if err != nil { 193 | return err 194 | } 195 | entryConfig := new(tuna.EntryConfiguration) 196 | err = util.ReadJSON("config.reverse.entry.json", entryConfig) 197 | if err != nil { 198 | return err 199 | } 200 | entryConfig.Reverse = true 201 | err = tuna.StartReverse(entryConfig, entryWallet) 202 | if err != nil { 203 | return err 204 | } 205 | ready <- struct{}{} 206 | 207 | select {} 208 | } 209 | 210 | func runReverseExit(tcpPort, udpPort *[]int, seed, entryPubKey []byte) error { 211 | exitAccount, err := vault.NewAccountWithSeed(seed) 212 | if err != nil { 213 | return err 214 | } 215 | seedRPCServerAddr := nkn.NewStringArray(nkn.DefaultSeedRPCServerAddr...) 216 | walletConfig := &nkn.WalletConfig{ 217 | SeedRPCServerAddr: seedRPCServerAddr, 218 | } 219 | exitWallet, err := nkn.NewWallet(&nkn.Account{Account: exitAccount}, walletConfig) 220 | if err != nil { 221 | return err 222 | } 223 | exitConfig := &tuna.ExitConfiguration{} 224 | err = util.ReadJSON("config.reverse.exit.json", exitConfig) 225 | if err != nil { 226 | log.Fatal("Load exitConfig file error:", err) 227 | return err 228 | } 229 | exitConfig.Reverse = true 230 | var exitServices []tuna.Service 231 | err = util.ReadJSON("services.reverse.exit.json", &exitServices) 232 | if err != nil { 233 | log.Fatal("Load service file error:", err) 234 | return err 235 | } 236 | node := types.Node{ 237 | Delay: 0, 238 | Bandwidth: 0, 239 | Metadata: &pb.ServiceMetadata{ 240 | Ip: "127.0.0.1", 241 | TcpPort: 30020, 242 | UdpPort: 30021, 243 | ServiceId: 0, 244 | Price: "0.000", 245 | BeneficiaryAddr: "", 246 | }, 247 | Address: hex.EncodeToString(entryPubKey), 248 | MetadataRaw: "CgkxMjcuMC4wLjEQxOoBGMXqAToFMC4wMDE=", 249 | } 250 | var lock sync.Mutex 251 | var wg sync.WaitGroup 252 | for i, service := range exitServices { 253 | if _, ok := exitConfig.Services[service.Name]; ok { 254 | go func(service tuna.Service, i int) { 255 | for { 256 | wg.Add(1) 257 | te, err := tuna.NewTunaExit([]tuna.Service{service}, exitWallet, nil, exitConfig) 258 | if err != nil { 259 | log.Fatalln(err) 260 | } 261 | te.SetRemoteNode(&node) 262 | 263 | i := i 264 | go func(i int) { 265 | for range te.OnConnect.C { 266 | lock.Lock() 267 | log.Printf("Service: %s, Type: TCP, Address: %v:%v\n", service.Name, te.GetReverseIP(), te.GetReverseTCPPorts()) 268 | port := int(te.GetReverseTCPPorts()[0]) 269 | (*tcpPort)[i] = port 270 | if len(service.UDP) > 0 { 271 | log.Printf("Service: %s, Type: UDP, Address: %v:%v\n", service.Name, te.GetReverseIP(), te.GetReverseUDPPorts()) 272 | port := int(te.GetReverseUDPPorts()[0]) 273 | (*udpPort)[i] = port 274 | } 275 | wg.Done() 276 | lock.Unlock() 277 | } 278 | }(i) 279 | 280 | err = te.StartReverse(false) 281 | if err != nil { 282 | log.Println(err) 283 | } 284 | } 285 | }(service, i) 286 | } 287 | } 288 | wg.Wait() 289 | 290 | select {} 291 | } 292 | 293 | func testTCP(conn net.Conn) error { 294 | send := make([]byte, 4096) 295 | receive := make([]byte, 4096) 296 | 297 | for i := 0; i < 10; i++ { 298 | rand.Read(send) 299 | _, err := conn.Write(send) 300 | if err != nil { 301 | return err 302 | } 303 | _, err = conn.Read(receive) 304 | if err != nil { 305 | return err 306 | } 307 | if !bytes.Equal(send, receive) { 308 | log.Println("got:", hex.EncodeToString(receive)) 309 | log.Println("want:", hex.EncodeToString(send)) 310 | return errors.New("bytes not equal") 311 | } 312 | } 313 | return nil 314 | } 315 | 316 | func testUDP(conn *net.UDPConn) error { 317 | send := make([]byte, 4096) 318 | receive := make([]byte, 4096) 319 | for i := 0; i < 100; i++ { 320 | rand.Read(send) 321 | _, err := conn.Write(send) 322 | if err != nil { 323 | return err 324 | } 325 | _, err = conn.Read(receive) 326 | if err != nil { 327 | return err 328 | } 329 | if !bytes.Equal(send, receive) { 330 | log.Println("got:", hex.EncodeToString(receive)) 331 | log.Println("want:", hex.EncodeToString(send)) 332 | return errors.New("bytes not equal") 333 | } 334 | } 335 | return nil 336 | } 337 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 5 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 6 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 7 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 8 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 9 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 13 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 14 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 15 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 17 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 19 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 20 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 21 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 22 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 23 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 24 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 25 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 26 | github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 27 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 28 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 29 | github.com/itchyny/base58-go v0.2.1 h1:wtnhAVdOcW3WuHEASmGHMms4juOB8yEpj/KJxlB57+k= 30 | github.com/itchyny/base58-go v0.2.1/go.mod h1:BNvrKeAtWNSca1GohNbyhfff9/v0IrZjzWCAGeAvZZE= 31 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 32 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 33 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 34 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 35 | github.com/nknorg/encrypted-stream v1.0.2-0.20230320101720-9891f770de86 h1:YraQ9G+P/DibBBVsLbfLatsDUngiCA0JWVkL1bzECAE= 36 | github.com/nknorg/encrypted-stream v1.0.2-0.20230320101720-9891f770de86/go.mod h1:VXJDhlUoF3uJSFLwIWnRLkiX5QPFB3E8oe2EUBwPoU0= 37 | github.com/nknorg/mockconn-go v0.0.0-20230125231524-d664e728352a/go.mod h1:/SvBORYxt9wlm8ZbaEFEri6ooOSDcU3ovU0L2eRRdS4= 38 | github.com/nknorg/ncp-go v1.0.5 h1:alJjq6bi6tRwUAAv932FIfE/R3S7DRR0pgXOgBXNHAk= 39 | github.com/nknorg/ncp-go v1.0.5/go.mod h1:ze88qf5e9/DBXSOaJPL2Caa0IbdZJLzESAFM1S7mGwg= 40 | github.com/nknorg/nkn-sdk-go v1.4.8-0.20240427043332-a40386d2b50a h1:SofheBu2lTEYM2yy/yKCfCebiK1xrd/Uds1WJo2kET0= 41 | github.com/nknorg/nkn-sdk-go v1.4.8-0.20240427043332-a40386d2b50a/go.mod h1:GIamdg0g4y8F/40b0+wyddZ3glZCqbz8L4PW55xK4Dk= 42 | github.com/nknorg/nkn/v2 v2.2.0 h1:sXOawvVF/T3bBTuWbzBCyrGuxldA3be+f+BDjoWcOEA= 43 | github.com/nknorg/nkn/v2 v2.2.0/go.mod h1:yv3jkg0aOtN9BDHS4yerNSZJtJNBfGvlaD5K6wL6U3E= 44 | github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9 h1:Gr37j7Ttvcn8g7TdC5fs6Y6IJKdmfqCvj03UbsrS77o= 45 | github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9/go.mod h1:zNY9NCyBcJCCDrXhwOjKarkW5cngPs/Z82xVNy/wvEA= 46 | github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug= 47 | github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng= 48 | github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= 49 | github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= 50 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 51 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 52 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 53 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 54 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 55 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40 h1:31Y7UZ1yTYBU4E79CE52I/1IRi3TqiuwquXGNtZDXWs= 59 | github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40/go.mod h1:j4c6zEU0eMG1oiZPUy+zD4ykX0NIpjZAEOEAviTWC18= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 62 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 63 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 64 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 65 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 66 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 67 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 68 | github.com/xtaci/smux v2.0.1+incompatible h1:4NrCD5VzuFktMCxK08IShR0C5vKyNICJRShUzvk0U34= 69 | github.com/xtaci/smux v2.0.1+incompatible/go.mod h1:f+nYm6SpuHMy/SH0zpbvAFHT1QoMcgLOsWcFip5KfPw= 70 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 71 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 72 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 73 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 74 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4= 75 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= 76 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 77 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 78 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 82 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 83 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 84 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 85 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 86 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 88 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 89 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 90 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 91 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 92 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 93 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 94 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 95 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 96 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 99 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TUNA 🐟 2 | 3 | TUNA software is a decentralized, peer-to-peer networking software that enables users to share their unused network 4 | bandwidth with others. 5 | It is designed to create a more efficient and decentralized internet by allowing users to earn rewards for sharing their 6 | resources. 7 | The TUNA software is integrated with NKN blockchain technology, which allows for secure and transparent transactions 8 | between users. 9 | Based services through NKN. Node will receive payment for tunneled traffic 10 | directly from user 11 | 12 | ## Build 13 | 14 | Simply run `make` will do the work. The output binary name will be `tuna`. 15 | 16 | ## Get Started 17 | 18 | Either entry mode or exit mode will need a service definition file 19 | `services.json`. You can start by using `services.json.example` as template. The 20 | service file defines what services to use or provide, which ports a service 21 | uses, and various configurations like encryption. 22 | 23 | ## How to use 24 | 25 | ### Forward mode 26 | 27 | If you want to use a forward proxy, whether it's an HTTP proxy or a SOCKS5 proxy, 28 | you can configure it by modifying the `services.json` file from `services.json.example` and starting the TUNA entry. 29 | By modifying this file, you can specify the type of proxy you want to use (i.e. HTTP or SOCKS5), as well as the port 30 | listening on your local machine. 31 | 32 | For example, the following configuration can be used to start TUNA entry client(forward mode) 33 | The TUNA entry client automatically searches for TUNA entry servers that provide `httpproxy` services in the network, 34 | and selects the node with the best speed for connection, then listens on the local port `30080`. 35 | It provides TCP proxy services. With this functionality, the client can automatically find the best 36 | available proxy server based on network conditions and provide optimal performance for the user. 37 | 38 | Here is an example of `services.json` 39 | 40 | ```json 41 | [ 42 | { 43 | "name": "httpproxy", 44 | "tcp": [ 45 | 30080 46 | ], 47 | "encryption": "aes-gcm" 48 | } 49 | ] 50 | ``` 51 | 52 | Then you can start your entry client 53 | 54 | You will need a config file `config.entry.json`. You can start by using 55 | `config.entry.json.example` as template. You can just run the command below 56 | 57 | ```shell 58 | cp config.entry.json.example config.entry.json 59 | ``` 60 | 61 | Start tuna in entry mode: 62 | 63 | ``` 64 | ./tuna entry 65 | ``` 66 | 67 | Then you can start using configured services as if they're on your local machine 68 | (e.g. `127.0.0.1:30080` for HTTP proxy). 69 | 70 | ### Reverse mode 71 | 72 | TUNA reverse mode is a reverse proxy. Exit is the internal service that need to be exposed to public Internet. 73 | Entry is the service provider that provides public IP address. 74 | 75 | For example, if you have a web server running on a local network, and you want to access it from the internet, you can 76 | start TUNA exit to connect TUNA entry server. This way, when someone accesses the TUNA entry server with the IP address 77 | and port that is 78 | configured, TUNA will forward the request to the TUNA exit, then it will forward to the local web server. 79 | 80 | Reverse mode is a powerful feature that allows users to access devices and services that are not directly accessible 81 | from the internet, it can be useful for remote access to local resources, testing, and troubleshooting. 82 | 83 | Here is an example of `services.json` for reverse clients which TUNA exit need 84 | 85 | ```json 86 | [ 87 | { 88 | "name": "httpproxy", 89 | "tcp": [ 90 | 30081 91 | ], 92 | "encryption": "xsalsa20-poly1305" 93 | } 94 | ] 95 | ``` 96 | 97 | You will need a config file `config.exit.json`. You can start by using 98 | `config.exit.json.example` as template. 99 | 100 | Set `reverse` to `true` in `config.exit.json` and start tuna in exit mode. Then 101 | your service will be exposed to public network by reverse entry. Your node does 102 | not need to have public IP address or open port at all. 103 | 104 | If you want to use specific port on reverse entry, you need to set `reverseRandomPorts` == `true` in `config.exit.json`, 105 | otherwise, you will be assigned to a random port. 106 | 107 | If the client side does not set the TCP and UDP ports, then the TUNA server will automatically allocate random ports. 108 | 109 | Start tuna in exit mode: 110 | 111 | ``` 112 | ./tuna exit 113 | ``` 114 | 115 | ## How to become a TUNA service provider and earn NKN from it 116 | 117 | There are two ways for users to earn NKN by providing services using TUNA. 118 | 119 | ### Entry Mode 120 | 121 | **The first way is to use the reverse mode of the entry to provide reverse proxy services** 122 | 123 | ```shell 124 | ./tuna -b=[YOUR_BENEFICIARY_ADDR] entry --reverse 125 | ``` 126 | 127 | Set `reverse` to `true` in `config.entry.json` and start tuna in entry mode. 128 | Then users can use your node as reverse proxy and pay you NKN based on bandwidth 129 | consumption. 130 | 131 | Or you can set `reverseBeneficiaryAddr` in `config.entry.json` 132 | 133 | You can also change the default listening port by setting `reverseTCP` & `reverseUDP` in entry config file. 134 | 135 | Remember to set your price of your services by setting `reversePrice` default value is 0.0002 NKN per MB. 136 | 137 | ### Exit Mode 138 | 139 | **The second way to earn revenue using TUNA is by using the forward mode of the exit to provide forward proxy services** 140 | 141 | Here is an example of `services.json` 142 | 143 | ```json 144 | [ 145 | { 146 | "name": "httpproxy", 147 | "tcp": [ 148 | 30080 149 | ], 150 | "encryption": "xsalsa20-poly1305" 151 | }, 152 | { 153 | "name": "socksproxy", 154 | "tcp": [ 155 | 30489 156 | ], 157 | "udp": [ 158 | 30489 159 | ], 160 | "encryption": "xsalsa20-poly1305" 161 | } 162 | ] 163 | ``` 164 | 165 | Then you can start your exit server by `./tuna -b=[YOUR_BENEFICIARY_ADDR] exit` 166 | 167 | Don't forget to deploy your proxy services at port 30080 & 30489. 168 | 169 | You can change the port as long as you ensure that the listening port is consistent with `services.json` 170 | 171 | Then users can connect to your services through their tuna entry and pay you NKN 172 | based on bandwidth consumption. 173 | 174 | ### Config 175 | 176 | #### Entry mode config `config.entry.json`: 177 | 178 | * `services` services you want to use 179 | * `dialTimeout` timeout for NKN node connection 180 | * `udpTimeout` timeout for UDP connections 181 | * `nanoPayFee` fee used for nano pay transaction 182 | * `reverse` should be used to provide reverse tunnel for those who don't have public IP 183 | * `reverseBeneficiaryAddr` Beneficiary address (NKN wallet address to receive rewards) 184 | * `reverseTCP` TCP port to listen for connections 185 | * `reverseUDP` UDP port to listen for connections 186 | * `reversePrice` price for reverse connections 187 | * `reverseClaimInterval` payment claim interval for reverse connections 188 | * `reverseSubscriptionDuration` duration for subscription in blocks 189 | * `reverseSubscriptionFee` fee used for subscription 190 | 191 | #### Exit mode config `config.exit.json`: 192 | 193 | * `beneficiaryAddr` beneficiary address (NKN wallet address to receive rewards) 194 | * `listenTCP` TCP port to listen for connections 195 | * `listenUDP` UDP port to listen for connections 196 | * `dialTimeout` timeout for connections to services 197 | * `udpTimeout` timeout for UDP connections 198 | * `claimInterval` payment claim interval for connections 199 | * `subscriptionDuration` duration for subscription in blocks 200 | * `subscriptionFee` fee used for subscription 201 | * `services` services you want to provide 202 | * `reverse` should be used if you don't have public IP and want to use another `server` for accepting clients 203 | * `reverseRandomPorts` meaning reverse entry can use random ports instead of specified ones (useful when service has 204 | dynamic ports) 205 | * `reverseMaxPrice` max accepted price for reverse service, unit is NKN per MB traffic 206 | * `reverseNanoPayFee` nanoPay transaction fee for reverse service 207 | * `reverseIPFilter` reverse service IP address filter 208 | 209 | ### encryption 210 | 211 | TUNA supports AES and Salsa20 encryption algorithms, you can refer to the JSON configuration example above. 212 | 213 | ### Service filter 214 | 215 | Users can configure several settings for the services offered by TUNA, such as setting a maximum price for the service, 216 | creating IP whitelists and blacklists, creating a whitelist and blacklist of service provider public keys and specifying 217 | the geographical location of the server. Remember using same service name in `services.json` 218 | and `config.exit(or entry).json` when you set those settings 219 | You can check `geo.IPFilter` and `filter.NknFilter` for more details. 220 | 221 | ## Use TUNA as library 222 | 223 | Most of them times you just need to run tuna entry/exit as a separate program 224 | together with the services, but you can also use tuna as a library. See 225 | [tests/util.go](tests/util.go) for entry/exit & forward/reverse examples. 226 | 227 | ## Compiling to iOS/Android native library 228 | 229 | This library is designed to work with 230 | [gomobile](https://godoc.org/golang.org/x/mobile/cmd/gomobile) and run natively 231 | on iOS/Android without any modification. You can use `gomobile bind` to compile 232 | it to Objective-C framework for iOS: 233 | 234 | ```shell 235 | gomobile bind -target=ios -ldflags "-s -w" github.com/nknorg/tuna github.com/nknorg/nkn-sdk-go github.com/nknorg/nkngomobile 236 | ``` 237 | 238 | and Java AAR for Android: 239 | 240 | ```shell 241 | gomobile bind -target=android -ldflags "-s -w" github.com/nknorg/tuna github.com/nknorg/nkn-sdk-go github.com/nknorg/nkngomobile 242 | ``` 243 | 244 | More likely you might want to write a simple wrapper uses tuna and compile it 245 | using gomobile. 246 | 247 | It's recommended to use the latest version of gomobile that supports go modules. 248 | 249 | ## Contributing 250 | 251 | **Can I submit a bug, suggestion or feature request?** 252 | 253 | Yes. Please open an issue for that. 254 | 255 | **Can I contribute patches?** 256 | 257 | Yes, we appreciate your help! To make contributions, please fork the repo, push 258 | your changes to the forked repo with signed-off commits, and open a pull request 259 | here. 260 | 261 | Please sign off your commit. This means adding a line "Signed-off-by: Name 262 | " at the end of each commit, indicating that you wrote the code and have 263 | the right to pass it on as an open source patch. This can be done automatically 264 | by adding -s when committing: 265 | 266 | ```shell 267 | git commit -s 268 | ``` 269 | 270 | ## Community 271 | 272 | - [Forum](https://forum.nkn.org/) 273 | - [Discord](https://discord.gg/c7mTynX) 274 | - [Telegram](https://t.me/nknorg) 275 | - [Reddit](https://www.reddit.com/r/nknblockchain/) 276 | - [Twitter](https://twitter.com/NKN_ORG) 277 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | "github.com/nknorg/tuna/filter" 9 | 10 | "github.com/imdario/mergo" 11 | "github.com/nknorg/tuna/geo" 12 | "github.com/nknorg/tuna/pb" 13 | "github.com/nknorg/tuna/types" 14 | ) 15 | 16 | const ( 17 | DefaultSubscriptionPrefix = "tuna_v1." 18 | DefaultReverseServiceName = "reverse" 19 | 20 | defaultNanoPayDuration = 4320 * 30 21 | defaultNanoPayUpdateInterval = time.Minute 22 | defaultNanoPayMinFlushAmount = "0.01" 23 | defaultServiceListenIP = "127.0.0.1" 24 | defaultReverseServiceListenIP = "0.0.0.0" 25 | defaultGetSubscribersBatchSize = 128 26 | defaultEncryptionAlgo = pb.EncryptionAlgo_ENCRYPTION_NONE 27 | defaultMeasureDelayTimeout = 1 * time.Second 28 | defaultMeasureDelayConcurrentWorkers = 64 29 | defaultMeasureBandwidthConcurrentWorkers = 16 // should be >= measureBandwidthTopCount 30 | defaultMeasureBandwidthTimeout = 2 // second 31 | defaultMeasureBandwidthWorkersTimeout = 8 // second 32 | defaultMeasurementBytesDownLink = 256 << 8 33 | defaultMaxMeasureWorkerPoolSize = 64 34 | defaultReverseTestTimeout = 3 * time.Second 35 | maxMeasureBandwidthTimeout = 30 * time.Second 36 | nanoPayClaimerLinger = 24 * time.Hour 37 | maxCheckSubscribeInterval = time.Hour 38 | defaultMinBalance = "0.0" // default minimum wallet balance for use tuna service 39 | ) 40 | 41 | type EntryConfiguration struct { 42 | SeedRPCServerAddr []string `json:"seedRPCServerAddr"` 43 | Services map[string]ServiceInfo `json:"services"` 44 | DialTimeout int32 `json:"dialTimeout"` 45 | UDPTimeout int32 `json:"udpTimeout"` 46 | NanoPayFee string `json:"nanoPayFee"` 47 | MinNanoPayFee string `json:"minNanoPayFee"` 48 | NanoPayFeeRatio float64 `json:"nanoPayFeeRatio"` 49 | SubscriptionPrefix string `json:"subscriptionPrefix"` 50 | Reverse bool `json:"reverse"` 51 | ReverseBeneficiaryAddr string `json:"reverseBeneficiaryAddr"` 52 | ReverseTCP int32 `json:"reverseTCP"` 53 | ReverseUDP int32 `json:"reverseUDP"` 54 | ReverseServiceListenIP string `json:"reverseServiceListenIP"` 55 | ReversePrice string `json:"reversePrice"` 56 | ReverseClaimInterval int32 `json:"reverseClaimInterval"` 57 | ReverseMinFlushAmount string `json:"reverseMinFlushAmount"` 58 | ReverseServiceName string `json:"reverseServiceName"` 59 | ReverseSubscriptionPrefix string `json:"reverseSubscriptionPrefix"` 60 | ReverseSubscriptionDuration int32 `json:"reverseSubscriptionDuration"` 61 | ReverseSubscriptionFee string `json:"reverseSubscriptionFee"` 62 | ReverseSubscriptionReplaceTxPool bool `json:"reverseSubscriptionReplaceTxPool"` 63 | GeoDBPath string `json:"geoDBPath"` 64 | DownloadGeoDB bool `json:"downloadGeoDB"` 65 | GetSubscribersBatchSize int32 `json:"getSubscribersBatchSize"` 66 | MeasureBandwidth bool `json:"measureBandwidth"` 67 | MeasureBandwidthTimeout int32 `json:"measureBandwidthTimeout"` 68 | MeasureBandwidthWorkersTimeout int32 `json:"measureBandwidthWorkersTimeout"` 69 | MeasurementBytesDownLink int32 `json:"measurementBytesDownLink"` 70 | MeasureStoragePath string `json:"measureStoragePath"` 71 | MaxMeasureWorkerPoolSize int32 `json:"maxMeasureWorkerPoolSize"` 72 | SortMeasuredNodes func(types.Nodes) `json:"-"` 73 | TcpDialContext func(ctx context.Context, network, addr string) (net.Conn, error) `json:"-"` 74 | HttpDialContext func(ctx context.Context, network, addr string) (net.Conn, error) `json:"-"` 75 | WsDialContext func(ctx context.Context, network, addr string) (net.Conn, error) `json:"-"` 76 | MinBalance string `json:"minBalance"` 77 | } 78 | 79 | var defaultEntryConfiguration = EntryConfiguration{ 80 | SubscriptionPrefix: DefaultSubscriptionPrefix, 81 | GetSubscribersBatchSize: defaultGetSubscribersBatchSize, 82 | MeasureBandwidthTimeout: defaultMeasureBandwidthTimeout, 83 | MeasureBandwidthWorkersTimeout: defaultMeasureBandwidthWorkersTimeout, 84 | MeasurementBytesDownLink: defaultMeasurementBytesDownLink, 85 | MaxMeasureWorkerPoolSize: defaultMaxMeasureWorkerPoolSize, 86 | ReverseSubscriptionPrefix: DefaultSubscriptionPrefix, 87 | ReverseServiceName: DefaultReverseServiceName, 88 | ReverseMinFlushAmount: defaultNanoPayMinFlushAmount, 89 | ReverseServiceListenIP: defaultReverseServiceListenIP, 90 | MinBalance: defaultMinBalance, 91 | } 92 | 93 | func DefaultEntryConfig() *EntryConfiguration { 94 | conf := defaultEntryConfiguration 95 | return &conf 96 | } 97 | 98 | type ExitConfiguration struct { 99 | SeedRPCServerAddr []string `json:"seedRPCServerAddr"` 100 | BeneficiaryAddr string `json:"beneficiaryAddr"` 101 | ListenTCP int32 `json:"listenTCP"` 102 | ListenUDP int32 `json:"listenUDP"` 103 | DialTimeout int32 `json:"dialTimeout"` 104 | UDPTimeout int32 `json:"udpTimeout"` 105 | SubscriptionPrefix string `json:"subscriptionPrefix"` 106 | SubscriptionDuration int32 `json:"subscriptionDuration"` 107 | SubscriptionFee string `json:"subscriptionFee"` 108 | SubscriptionReplaceTxPool bool `json:"subscriptionReplaceTxPool"` 109 | ClaimInterval int32 `json:"claimInterval"` 110 | MinFlushAmount string `json:"minFlushAmount"` 111 | Services map[string]ExitServiceInfo `json:"services"` 112 | Reverse bool `json:"reverse"` 113 | ReverseRandomPorts bool `json:"reverseRandomPorts"` 114 | ReverseMaxPrice string `json:"reverseMaxPrice"` 115 | ReverseNanoPayFee string `json:"reverseNanopayfee"` 116 | MinReverseNanoPayFee string `json:"minReverseNanoPayFee"` 117 | ReverseNanoPayFeeRatio float64 `json:"reverseNanopayfeeRatio"` 118 | ReverseServiceName string `json:"reverseServiceName"` 119 | ReverseSubscriptionPrefix string `json:"reverseSubscriptionPrefix"` 120 | ReverseEncryption string `json:"reverseEncryption"` 121 | GeoDBPath string `json:"geoDBPath"` 122 | DownloadGeoDB bool `json:"downloadGeoDB"` 123 | GetSubscribersBatchSize int32 `json:"getSubscribersBatchSize"` 124 | ReverseIPFilter geo.IPFilter `json:"reverseIPFilter"` 125 | ReverseNknFilter filter.NknFilter `json:"reverseNknFilter"` 126 | MeasureBandwidth bool `json:"measureBandwidth"` 127 | MeasureBandwidthTimeout int32 `json:"measureBandwidthTimeout"` 128 | MeasureBandwidthWorkersTimeout int32 `json:"measureBandwidthWorkersTimeout"` 129 | MeasurementBytesDownLink int32 `json:"measurementBytesDownLink"` 130 | MeasureStoragePath string `json:"measureStoragePath"` 131 | MaxMeasureWorkerPoolSize int32 `json:"maxMeasureWorkerPoolSize"` 132 | SortMeasuredNodes func(types.Nodes) `json:"-"` 133 | TcpDialContext func(ctx context.Context, network, addr string) (net.Conn, error) `json:"-"` 134 | HttpDialContext func(ctx context.Context, network, addr string) (net.Conn, error) `json:"-"` 135 | WsDialContext func(ctx context.Context, network, addr string) (net.Conn, error) `json:"-"` 136 | ReverseMinBalance string `json:"reverseMinBalance"` 137 | } 138 | 139 | var defaultExitConfiguration = ExitConfiguration{ 140 | SubscriptionPrefix: DefaultSubscriptionPrefix, 141 | GetSubscribersBatchSize: defaultGetSubscribersBatchSize, 142 | MeasureBandwidthTimeout: defaultMeasureBandwidthTimeout, 143 | MeasureBandwidthWorkersTimeout: defaultMeasureBandwidthWorkersTimeout, 144 | MeasurementBytesDownLink: defaultMeasurementBytesDownLink, 145 | MaxMeasureWorkerPoolSize: defaultMaxMeasureWorkerPoolSize, 146 | MinFlushAmount: defaultNanoPayMinFlushAmount, 147 | ReverseSubscriptionPrefix: DefaultSubscriptionPrefix, 148 | ReverseServiceName: DefaultReverseServiceName, 149 | ReverseMinBalance: defaultMinBalance, 150 | } 151 | 152 | func DefaultExitConfig() *ExitConfiguration { 153 | conf := defaultExitConfiguration 154 | return &conf 155 | } 156 | 157 | func MergedEntryConfig(conf *EntryConfiguration) (*EntryConfiguration, error) { 158 | merged := DefaultEntryConfig() 159 | if conf != nil { 160 | err := mergo.Merge(merged, conf, mergo.WithOverride) 161 | if err != nil { 162 | return nil, err 163 | } 164 | } 165 | return merged, nil 166 | } 167 | 168 | func MergedExitConfig(conf *ExitConfiguration) (*ExitConfiguration, error) { 169 | merged := DefaultExitConfig() 170 | if conf != nil { 171 | err := mergo.Merge(merged, conf, mergo.WithOverride) 172 | if err != nil { 173 | return nil, err 174 | } 175 | } 176 | return merged, nil 177 | } 178 | -------------------------------------------------------------------------------- /pb/tuna.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v4.23.3 5 | // source: pb/tuna.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type EncryptionAlgo int32 24 | 25 | const ( 26 | EncryptionAlgo_ENCRYPTION_NONE EncryptionAlgo = 0 27 | EncryptionAlgo_ENCRYPTION_XSALSA20_POLY1305 EncryptionAlgo = 1 28 | EncryptionAlgo_ENCRYPTION_AES_GCM EncryptionAlgo = 2 29 | ) 30 | 31 | // Enum value maps for EncryptionAlgo. 32 | var ( 33 | EncryptionAlgo_name = map[int32]string{ 34 | 0: "ENCRYPTION_NONE", 35 | 1: "ENCRYPTION_XSALSA20_POLY1305", 36 | 2: "ENCRYPTION_AES_GCM", 37 | } 38 | EncryptionAlgo_value = map[string]int32{ 39 | "ENCRYPTION_NONE": 0, 40 | "ENCRYPTION_XSALSA20_POLY1305": 1, 41 | "ENCRYPTION_AES_GCM": 2, 42 | } 43 | ) 44 | 45 | func (x EncryptionAlgo) Enum() *EncryptionAlgo { 46 | p := new(EncryptionAlgo) 47 | *p = x 48 | return p 49 | } 50 | 51 | func (x EncryptionAlgo) String() string { 52 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 53 | } 54 | 55 | func (EncryptionAlgo) Descriptor() protoreflect.EnumDescriptor { 56 | return file_pb_tuna_proto_enumTypes[0].Descriptor() 57 | } 58 | 59 | func (EncryptionAlgo) Type() protoreflect.EnumType { 60 | return &file_pb_tuna_proto_enumTypes[0] 61 | } 62 | 63 | func (x EncryptionAlgo) Number() protoreflect.EnumNumber { 64 | return protoreflect.EnumNumber(x) 65 | } 66 | 67 | // Deprecated: Use EncryptionAlgo.Descriptor instead. 68 | func (EncryptionAlgo) EnumDescriptor() ([]byte, []int) { 69 | return file_pb_tuna_proto_rawDescGZIP(), []int{0} 70 | } 71 | 72 | type ConnectionMetadata struct { 73 | state protoimpl.MessageState 74 | sizeCache protoimpl.SizeCache 75 | unknownFields protoimpl.UnknownFields 76 | 77 | EncryptionAlgo EncryptionAlgo `protobuf:"varint,1,opt,name=encryption_algo,json=encryptionAlgo,proto3,enum=pb.EncryptionAlgo" json:"encryption_algo,omitempty"` 78 | PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` 79 | Nonce []byte `protobuf:"bytes,3,opt,name=nonce,proto3" json:"nonce,omitempty"` 80 | IsMeasurement bool `protobuf:"varint,4,opt,name=is_measurement,json=isMeasurement,proto3" json:"is_measurement,omitempty"` 81 | MeasurementBytesDownlink uint32 `protobuf:"varint,5,opt,name=measurement_bytes_downlink,json=measurementBytesDownlink,proto3" json:"measurement_bytes_downlink,omitempty"` 82 | IsPing bool `protobuf:"varint,6,opt,name=is_ping,json=isPing,proto3" json:"is_ping,omitempty"` 83 | } 84 | 85 | func (x *ConnectionMetadata) Reset() { 86 | *x = ConnectionMetadata{} 87 | if protoimpl.UnsafeEnabled { 88 | mi := &file_pb_tuna_proto_msgTypes[0] 89 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 90 | ms.StoreMessageInfo(mi) 91 | } 92 | } 93 | 94 | func (x *ConnectionMetadata) String() string { 95 | return protoimpl.X.MessageStringOf(x) 96 | } 97 | 98 | func (*ConnectionMetadata) ProtoMessage() {} 99 | 100 | func (x *ConnectionMetadata) ProtoReflect() protoreflect.Message { 101 | mi := &file_pb_tuna_proto_msgTypes[0] 102 | if protoimpl.UnsafeEnabled && x != nil { 103 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 104 | if ms.LoadMessageInfo() == nil { 105 | ms.StoreMessageInfo(mi) 106 | } 107 | return ms 108 | } 109 | return mi.MessageOf(x) 110 | } 111 | 112 | // Deprecated: Use ConnectionMetadata.ProtoReflect.Descriptor instead. 113 | func (*ConnectionMetadata) Descriptor() ([]byte, []int) { 114 | return file_pb_tuna_proto_rawDescGZIP(), []int{0} 115 | } 116 | 117 | func (x *ConnectionMetadata) GetEncryptionAlgo() EncryptionAlgo { 118 | if x != nil { 119 | return x.EncryptionAlgo 120 | } 121 | return EncryptionAlgo_ENCRYPTION_NONE 122 | } 123 | 124 | func (x *ConnectionMetadata) GetPublicKey() []byte { 125 | if x != nil { 126 | return x.PublicKey 127 | } 128 | return nil 129 | } 130 | 131 | func (x *ConnectionMetadata) GetNonce() []byte { 132 | if x != nil { 133 | return x.Nonce 134 | } 135 | return nil 136 | } 137 | 138 | func (x *ConnectionMetadata) GetIsMeasurement() bool { 139 | if x != nil { 140 | return x.IsMeasurement 141 | } 142 | return false 143 | } 144 | 145 | func (x *ConnectionMetadata) GetMeasurementBytesDownlink() uint32 { 146 | if x != nil { 147 | return x.MeasurementBytesDownlink 148 | } 149 | return 0 150 | } 151 | 152 | func (x *ConnectionMetadata) GetIsPing() bool { 153 | if x != nil { 154 | return x.IsPing 155 | } 156 | return false 157 | } 158 | 159 | type ServiceMetadata struct { 160 | state protoimpl.MessageState 161 | sizeCache protoimpl.SizeCache 162 | unknownFields protoimpl.UnknownFields 163 | 164 | Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` 165 | TcpPort uint32 `protobuf:"varint,2,opt,name=tcp_port,json=tcpPort,proto3" json:"tcp_port,omitempty"` 166 | UdpPort uint32 `protobuf:"varint,3,opt,name=udp_port,json=udpPort,proto3" json:"udp_port,omitempty"` 167 | ServiceId uint32 `protobuf:"varint,4,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` 168 | ServiceTcp []uint32 `protobuf:"varint,5,rep,packed,name=service_tcp,json=serviceTcp,proto3" json:"service_tcp,omitempty"` 169 | ServiceUdp []uint32 `protobuf:"varint,6,rep,packed,name=service_udp,json=serviceUdp,proto3" json:"service_udp,omitempty"` 170 | Price string `protobuf:"bytes,7,opt,name=price,proto3" json:"price,omitempty"` 171 | BeneficiaryAddr string `protobuf:"bytes,8,opt,name=beneficiary_addr,json=beneficiaryAddr,proto3" json:"beneficiary_addr,omitempty"` 172 | } 173 | 174 | func (x *ServiceMetadata) Reset() { 175 | *x = ServiceMetadata{} 176 | if protoimpl.UnsafeEnabled { 177 | mi := &file_pb_tuna_proto_msgTypes[1] 178 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 179 | ms.StoreMessageInfo(mi) 180 | } 181 | } 182 | 183 | func (x *ServiceMetadata) String() string { 184 | return protoimpl.X.MessageStringOf(x) 185 | } 186 | 187 | func (*ServiceMetadata) ProtoMessage() {} 188 | 189 | func (x *ServiceMetadata) ProtoReflect() protoreflect.Message { 190 | mi := &file_pb_tuna_proto_msgTypes[1] 191 | if protoimpl.UnsafeEnabled && x != nil { 192 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 193 | if ms.LoadMessageInfo() == nil { 194 | ms.StoreMessageInfo(mi) 195 | } 196 | return ms 197 | } 198 | return mi.MessageOf(x) 199 | } 200 | 201 | // Deprecated: Use ServiceMetadata.ProtoReflect.Descriptor instead. 202 | func (*ServiceMetadata) Descriptor() ([]byte, []int) { 203 | return file_pb_tuna_proto_rawDescGZIP(), []int{1} 204 | } 205 | 206 | func (x *ServiceMetadata) GetIp() string { 207 | if x != nil { 208 | return x.Ip 209 | } 210 | return "" 211 | } 212 | 213 | func (x *ServiceMetadata) GetTcpPort() uint32 { 214 | if x != nil { 215 | return x.TcpPort 216 | } 217 | return 0 218 | } 219 | 220 | func (x *ServiceMetadata) GetUdpPort() uint32 { 221 | if x != nil { 222 | return x.UdpPort 223 | } 224 | return 0 225 | } 226 | 227 | func (x *ServiceMetadata) GetServiceId() uint32 { 228 | if x != nil { 229 | return x.ServiceId 230 | } 231 | return 0 232 | } 233 | 234 | func (x *ServiceMetadata) GetServiceTcp() []uint32 { 235 | if x != nil { 236 | return x.ServiceTcp 237 | } 238 | return nil 239 | } 240 | 241 | func (x *ServiceMetadata) GetServiceUdp() []uint32 { 242 | if x != nil { 243 | return x.ServiceUdp 244 | } 245 | return nil 246 | } 247 | 248 | func (x *ServiceMetadata) GetPrice() string { 249 | if x != nil { 250 | return x.Price 251 | } 252 | return "" 253 | } 254 | 255 | func (x *ServiceMetadata) GetBeneficiaryAddr() string { 256 | if x != nil { 257 | return x.BeneficiaryAddr 258 | } 259 | return "" 260 | } 261 | 262 | type StreamMetadata struct { 263 | state protoimpl.MessageState 264 | sizeCache protoimpl.SizeCache 265 | unknownFields protoimpl.UnknownFields 266 | 267 | ServiceId uint32 `protobuf:"varint,1,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` 268 | PortId uint32 `protobuf:"varint,2,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty"` 269 | IsPayment bool `protobuf:"varint,3,opt,name=is_payment,json=isPayment,proto3" json:"is_payment,omitempty"` 270 | } 271 | 272 | func (x *StreamMetadata) Reset() { 273 | *x = StreamMetadata{} 274 | if protoimpl.UnsafeEnabled { 275 | mi := &file_pb_tuna_proto_msgTypes[2] 276 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 277 | ms.StoreMessageInfo(mi) 278 | } 279 | } 280 | 281 | func (x *StreamMetadata) String() string { 282 | return protoimpl.X.MessageStringOf(x) 283 | } 284 | 285 | func (*StreamMetadata) ProtoMessage() {} 286 | 287 | func (x *StreamMetadata) ProtoReflect() protoreflect.Message { 288 | mi := &file_pb_tuna_proto_msgTypes[2] 289 | if protoimpl.UnsafeEnabled && x != nil { 290 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 291 | if ms.LoadMessageInfo() == nil { 292 | ms.StoreMessageInfo(mi) 293 | } 294 | return ms 295 | } 296 | return mi.MessageOf(x) 297 | } 298 | 299 | // Deprecated: Use StreamMetadata.ProtoReflect.Descriptor instead. 300 | func (*StreamMetadata) Descriptor() ([]byte, []int) { 301 | return file_pb_tuna_proto_rawDescGZIP(), []int{2} 302 | } 303 | 304 | func (x *StreamMetadata) GetServiceId() uint32 { 305 | if x != nil { 306 | return x.ServiceId 307 | } 308 | return 0 309 | } 310 | 311 | func (x *StreamMetadata) GetPortId() uint32 { 312 | if x != nil { 313 | return x.PortId 314 | } 315 | return 0 316 | } 317 | 318 | func (x *StreamMetadata) GetIsPayment() bool { 319 | if x != nil { 320 | return x.IsPayment 321 | } 322 | return false 323 | } 324 | 325 | var File_pb_tuna_proto protoreflect.FileDescriptor 326 | 327 | var file_pb_tuna_proto_rawDesc = []byte{ 328 | 0x0a, 0x0d, 0x70, 0x62, 0x2f, 0x74, 0x75, 0x6e, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 329 | 0x02, 0x70, 0x62, 0x22, 0x84, 0x02, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 330 | 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3b, 0x0a, 0x0f, 0x65, 0x6e, 331 | 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x18, 0x01, 0x20, 332 | 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 333 | 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x52, 0x0e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 334 | 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 335 | 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 336 | 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 337 | 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 338 | 0x69, 0x73, 0x5f, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 339 | 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 340 | 0x65, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x1a, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 341 | 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 342 | 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x18, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 343 | 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 344 | 0x6b, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 345 | 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x50, 0x69, 0x6e, 0x67, 0x22, 0xf9, 0x01, 0x0a, 0x0f, 0x53, 346 | 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 347 | 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x19, 348 | 0x0a, 0x08, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 349 | 0x52, 0x07, 0x74, 0x63, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x64, 0x70, 350 | 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x75, 0x64, 0x70, 351 | 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 352 | 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 353 | 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 354 | 0x63, 0x70, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 355 | 0x65, 0x54, 0x63, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 356 | 0x75, 0x64, 0x70, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 357 | 0x63, 0x65, 0x55, 0x64, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x07, 358 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 359 | 0x65, 0x6e, 0x65, 0x66, 0x69, 0x63, 0x69, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 360 | 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x65, 0x6e, 0x65, 0x66, 0x69, 0x63, 0x69, 0x61, 361 | 0x72, 0x79, 0x41, 0x64, 0x64, 0x72, 0x22, 0x67, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 362 | 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 363 | 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x73, 0x65, 364 | 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x72, 0x74, 0x5f, 365 | 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x64, 366 | 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 367 | 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2a, 368 | 0x5f, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 369 | 0x6f, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 370 | 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 371 | 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x58, 0x53, 0x41, 0x4c, 0x53, 0x41, 0x32, 0x30, 0x5f, 0x50, 0x4f, 372 | 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x45, 0x4e, 0x43, 0x52, 373 | 0x59, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x45, 0x53, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x02, 374 | 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 375 | } 376 | 377 | var ( 378 | file_pb_tuna_proto_rawDescOnce sync.Once 379 | file_pb_tuna_proto_rawDescData = file_pb_tuna_proto_rawDesc 380 | ) 381 | 382 | func file_pb_tuna_proto_rawDescGZIP() []byte { 383 | file_pb_tuna_proto_rawDescOnce.Do(func() { 384 | file_pb_tuna_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_tuna_proto_rawDescData) 385 | }) 386 | return file_pb_tuna_proto_rawDescData 387 | } 388 | 389 | var file_pb_tuna_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 390 | var file_pb_tuna_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 391 | var file_pb_tuna_proto_goTypes = []interface{}{ 392 | (EncryptionAlgo)(0), // 0: pb.EncryptionAlgo 393 | (*ConnectionMetadata)(nil), // 1: pb.ConnectionMetadata 394 | (*ServiceMetadata)(nil), // 2: pb.ServiceMetadata 395 | (*StreamMetadata)(nil), // 3: pb.StreamMetadata 396 | } 397 | var file_pb_tuna_proto_depIdxs = []int32{ 398 | 0, // 0: pb.ConnectionMetadata.encryption_algo:type_name -> pb.EncryptionAlgo 399 | 1, // [1:1] is the sub-list for method output_type 400 | 1, // [1:1] is the sub-list for method input_type 401 | 1, // [1:1] is the sub-list for extension type_name 402 | 1, // [1:1] is the sub-list for extension extendee 403 | 0, // [0:1] is the sub-list for field type_name 404 | } 405 | 406 | func init() { file_pb_tuna_proto_init() } 407 | func file_pb_tuna_proto_init() { 408 | if File_pb_tuna_proto != nil { 409 | return 410 | } 411 | if !protoimpl.UnsafeEnabled { 412 | file_pb_tuna_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 413 | switch v := v.(*ConnectionMetadata); i { 414 | case 0: 415 | return &v.state 416 | case 1: 417 | return &v.sizeCache 418 | case 2: 419 | return &v.unknownFields 420 | default: 421 | return nil 422 | } 423 | } 424 | file_pb_tuna_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 425 | switch v := v.(*ServiceMetadata); i { 426 | case 0: 427 | return &v.state 428 | case 1: 429 | return &v.sizeCache 430 | case 2: 431 | return &v.unknownFields 432 | default: 433 | return nil 434 | } 435 | } 436 | file_pb_tuna_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 437 | switch v := v.(*StreamMetadata); i { 438 | case 0: 439 | return &v.state 440 | case 1: 441 | return &v.sizeCache 442 | case 2: 443 | return &v.unknownFields 444 | default: 445 | return nil 446 | } 447 | } 448 | } 449 | type x struct{} 450 | out := protoimpl.TypeBuilder{ 451 | File: protoimpl.DescBuilder{ 452 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 453 | RawDescriptor: file_pb_tuna_proto_rawDesc, 454 | NumEnums: 1, 455 | NumMessages: 3, 456 | NumExtensions: 0, 457 | NumServices: 0, 458 | }, 459 | GoTypes: file_pb_tuna_proto_goTypes, 460 | DependencyIndexes: file_pb_tuna_proto_depIdxs, 461 | EnumInfos: file_pb_tuna_proto_enumTypes, 462 | MessageInfos: file_pb_tuna_proto_msgTypes, 463 | }.Build() 464 | File_pb_tuna_proto = out.File 465 | file_pb_tuna_proto_rawDesc = nil 466 | file_pb_tuna_proto_goTypes = nil 467 | file_pb_tuna_proto_depIdxs = nil 468 | } 469 | -------------------------------------------------------------------------------- /exit.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/rdegges/go-ipify" 15 | 16 | "github.com/nknorg/nkn-sdk-go" 17 | "github.com/nknorg/nkn/v2/common" 18 | "github.com/nknorg/tuna/pb" 19 | "github.com/nknorg/tuna/util" 20 | "github.com/patrickmn/go-cache" 21 | "github.com/xtaci/smux" 22 | ) 23 | 24 | type ExitServiceInfo struct { 25 | Address string `json:"address"` 26 | Price string `json:"price"` 27 | } 28 | 29 | type TunaExit struct { 30 | // It's important to keep these uint64 field on top to avoid panic on arm32 31 | // architecture: https://github.com/golang/go/issues/23345 32 | reverseBytesEntryToExit uint64 33 | reverseBytesExitToEntry uint64 34 | reverseBytesEntryToExitPaid uint64 35 | reverseBytesExitToEntryPaid uint64 36 | 37 | *Common 38 | OnConnect *OnConnect // override Common.OnConnect 39 | config *ExitConfiguration 40 | services []Service 41 | serviceConn *cache.Cache 42 | tcpListener net.Listener 43 | reverseIP net.IP 44 | reverseTCP []uint32 45 | reverseUDP []uint32 46 | } 47 | 48 | func NewTunaExit(services []Service, wallet *nkn.Wallet, client *nkn.MultiClient, config *ExitConfiguration) (*TunaExit, error) { 49 | config, err := MergedExitConfig(config) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | var service *Service 55 | var serviceInfo *ServiceInfo 56 | var subscriptionPrefix string 57 | var reverseMetadata *pb.ServiceMetadata 58 | if config.Reverse { 59 | if len(services) != 1 { 60 | return nil, errors.New("services should have length 1") 61 | } 62 | 63 | subscriptionPrefix = config.ReverseSubscriptionPrefix 64 | 65 | service = &Service{ 66 | Name: config.ReverseServiceName, 67 | Encryption: services[0].Encryption, 68 | UDPBufferSize: services[0].UDPBufferSize, 69 | } 70 | if service.UDPBufferSize == 0 { 71 | service.UDPBufferSize = DefaultUDPBufferSize 72 | } 73 | 74 | serviceInfo = &ServiceInfo{ 75 | MaxPrice: config.ReverseMaxPrice, 76 | IPFilter: &config.ReverseIPFilter, 77 | NknFilter: &config.ReverseNknFilter, 78 | } 79 | 80 | reverseMetadata = &pb.ServiceMetadata{} 81 | reverseMetadata.ServiceTcp = services[0].TCP 82 | reverseMetadata.ServiceUdp = services[0].UDP 83 | _, err = common.StringToFixed64(config.ReverseNanoPayFee) 84 | if err != nil { 85 | return nil, err 86 | } 87 | _, err = common.StringToFixed64(config.MinReverseNanoPayFee) 88 | if err != nil { 89 | return nil, err 90 | } 91 | } else { 92 | subscriptionPrefix = config.SubscriptionPrefix 93 | } 94 | 95 | c, err := NewCommon( 96 | service, 97 | serviceInfo, 98 | wallet, 99 | client, 100 | config.SeedRPCServerAddr, 101 | config.DialTimeout, 102 | subscriptionPrefix, 103 | config.Reverse, 104 | !config.Reverse, 105 | config.GeoDBPath, 106 | config.DownloadGeoDB, 107 | config.GetSubscribersBatchSize, 108 | config.MeasureBandwidth, 109 | config.MeasureBandwidthTimeout, 110 | config.MeasureBandwidthWorkersTimeout, 111 | config.MeasurementBytesDownLink, 112 | config.MeasureStoragePath, 113 | config.MaxMeasureWorkerPoolSize, 114 | config.TcpDialContext, 115 | config.HttpDialContext, 116 | config.WsDialContext, 117 | config.SortMeasuredNodes, 118 | reverseMetadata, 119 | config.ReverseMinBalance, 120 | ) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | te := &TunaExit{ 126 | Common: c, 127 | OnConnect: NewOnConnect(1, nil), 128 | config: config, 129 | services: services, 130 | serviceConn: cache.New(time.Duration(config.UDPTimeout)*time.Second, time.Second), 131 | } 132 | 133 | return te, nil 134 | } 135 | 136 | func (te *TunaExit) getServiceID(serviceName string) (byte, error) { 137 | for i, service := range te.services { 138 | if service.Name == serviceName { 139 | return byte(i), nil 140 | } 141 | } 142 | 143 | return 0, errors.New("Service " + serviceName + " not found") 144 | } 145 | 146 | func (te *TunaExit) handleSession(session *smux.Session, connMetadata *pb.ConnectionMetadata) { 147 | bytesEntryToExit := make([]uint64, 256) 148 | bytesExitToEntry := make([]uint64, 256) 149 | var k string 150 | 151 | var npc *nkn.NanoPayClaimer 152 | var lastPaymentAmount, bytesPaid common.Fixed64 153 | var err error 154 | claimInterval := time.Duration(te.config.ClaimInterval) * time.Second 155 | onErr := nkn.NewOnError(1, nil) 156 | lastPaymentTime := time.Now() 157 | isClosed := false 158 | if connMetadata != nil { 159 | k = string(append(connMetadata.PublicKey, connMetadata.Nonce...)) 160 | te.Common.reverseBytesEntryToExit[k] = bytesEntryToExit 161 | te.Common.reverseBytesExitToEntry[k] = bytesExitToEntry 162 | } 163 | 164 | getTotalCost := func() (common.Fixed64, common.Fixed64) { 165 | cost, totalBytes := common.Fixed64(0), common.Fixed64(0) 166 | for i := range bytesEntryToExit { 167 | entryToExit := common.Fixed64(atomic.LoadUint64(&bytesEntryToExit[i])) 168 | exitToEntry := common.Fixed64(atomic.LoadUint64(&bytesExitToEntry[i])) 169 | if entryToExit == 0 && exitToEntry == 0 { 170 | continue 171 | } 172 | service, err := te.getService(byte(i)) 173 | if err != nil { 174 | continue 175 | } 176 | serviceInfo := te.config.Services[service.Name] 177 | entryToExitPrice, exitToEntryPrice, err := ParsePrice(serviceInfo.Price) 178 | if err != nil { 179 | continue 180 | } 181 | cost += entryToExitPrice*entryToExit/TrafficUnit + exitToEntryPrice*exitToEntry/TrafficUnit 182 | totalBytes += entryToExit + exitToEntry 183 | } 184 | return cost, totalBytes 185 | } 186 | 187 | if !te.config.Reverse { 188 | npc, err = te.Client.NewNanoPayClaimer(te.config.BeneficiaryAddr, int32(claimInterval/time.Millisecond), int32(nanoPayClaimerLinger/time.Millisecond), te.config.MinFlushAmount, onErr) 189 | if err != nil { 190 | log.Fatalln(err) 191 | } 192 | 193 | defer npc.Close() 194 | 195 | go checkNanoPayClaim(session, npc, onErr, &isClosed) 196 | 197 | go checkPayment(session, &lastPaymentTime, &lastPaymentAmount, &bytesPaid, &isClosed, getTotalCost) 198 | } 199 | 200 | for { 201 | stream, err := session.AcceptStream() 202 | if err != nil { 203 | log.Println("Couldn't accept stream:", err) 204 | session.Close() 205 | break 206 | } 207 | 208 | go func() { 209 | err := func() error { 210 | streamMetadata, err := readStreamMetadata(stream) 211 | if err != nil { 212 | return fmt.Errorf("read stream metadata error: %v", err) 213 | } 214 | 215 | if streamMetadata.IsPayment { 216 | return handlePaymentStream(stream, npc, &lastPaymentTime, &lastPaymentAmount, &bytesPaid, getTotalCost) 217 | } 218 | 219 | serviceID := byte(streamMetadata.ServiceId) 220 | portID := int(streamMetadata.PortId) 221 | 222 | service, err := te.getService(serviceID) 223 | if err != nil { 224 | return err 225 | } 226 | tcpPortsCount := len(service.TCP) 227 | udpPortsCount := len(service.UDP) 228 | var protocol string 229 | var port int 230 | if portID < tcpPortsCount { 231 | protocol = tcp4 232 | port = int(service.TCP[portID]) 233 | } else if portID-tcpPortsCount < udpPortsCount { 234 | protocol = udp4 235 | portID -= tcpPortsCount 236 | port = int(service.UDP[portID]) 237 | } else { 238 | return fmt.Errorf("invalid portId: %d", portID) 239 | } 240 | 241 | serviceInfo := te.config.Services[service.Name] 242 | host := serviceInfo.Address + ":" + strconv.Itoa(port) 243 | 244 | conn, err := net.DialTimeout(protocol, host, time.Duration(te.config.DialTimeout)*time.Second) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | if te.config.Reverse { 250 | go te.pipe(conn, stream, &te.reverseBytesEntryToExit) 251 | go te.pipe(stream, conn, &te.reverseBytesExitToEntry) 252 | } else { 253 | go te.pipe(conn, stream, &te.Common.reverseBytesEntryToExit[k][serviceID]) 254 | go te.pipe(stream, conn, &te.Common.reverseBytesExitToEntry[k][serviceID]) 255 | } 256 | 257 | return nil 258 | }() 259 | if err != nil { 260 | log.Println(err) 261 | Close(stream) 262 | } 263 | }() 264 | } 265 | 266 | Close(session) 267 | isClosed = true 268 | } 269 | 270 | func (te *TunaExit) listenTCP(port int) error { 271 | listener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: port}) 272 | if err != nil { 273 | log.Println("Couldn't bind listener:", err) 274 | return err 275 | } 276 | te.tcpListener = listener 277 | 278 | go func() { 279 | for { 280 | conn, err := listener.Accept() 281 | if te.IsClosed() { 282 | return 283 | } 284 | if err != nil { 285 | if strings.Contains(err.Error(), "use of closed network connection") { 286 | te.Close() 287 | return 288 | } 289 | log.Println("Couldn't accept client connection:", err) 290 | time.Sleep(time.Second) 291 | continue 292 | } 293 | 294 | go func() { 295 | err := func() error { 296 | defer Close(conn) 297 | 298 | encryptedConn, connMetadata, err := te.wrapConn(conn, nil, nil) 299 | if err != nil { 300 | return fmt.Errorf("wrap conn error: %v", err) 301 | } 302 | 303 | defer Close(encryptedConn) 304 | 305 | if connMetadata.IsMeasurement { 306 | err = util.BandwidthMeasurementServer(encryptedConn, int(connMetadata.MeasurementBytesDownlink), maxMeasureBandwidthTimeout) 307 | if err != nil { 308 | return fmt.Errorf("bandwidth measurement server error: %v", err) 309 | } 310 | return nil 311 | } 312 | 313 | session, err := smux.Server(encryptedConn, nil) 314 | if err != nil { 315 | return fmt.Errorf("create session error: %v", err) 316 | } 317 | 318 | te.handleSession(session, connMetadata) 319 | 320 | return nil 321 | }() 322 | if err != nil { 323 | log.Println(err) 324 | } 325 | }() 326 | } 327 | }() 328 | 329 | return nil 330 | } 331 | 332 | func (te *TunaExit) getService(serviceID byte) (*Service, error) { 333 | if int(serviceID) >= len(te.services) { 334 | return nil, errors.New("Wrong serviceId: " + strconv.Itoa(int(serviceID))) 335 | } 336 | return &te.services[serviceID], nil 337 | } 338 | 339 | func (te *TunaExit) getServiceConn(connID []byte, serviceID byte, portID byte) (*net.UDPConn, error) { 340 | connKey := strconv.Itoa(int(ConnIDToPort(connID))) 341 | var conn *net.UDPConn 342 | var x interface{} 343 | var ok bool 344 | if x, ok = te.serviceConn.Get(connKey); !ok { 345 | service, err := te.getService(serviceID) 346 | if err != nil { 347 | log.Println(err) 348 | return nil, err 349 | } 350 | if int(portID) >= len(service.UDP) { 351 | return nil, fmt.Errorf("UDP portID %v out of range", portID) 352 | } 353 | port := service.UDP[portID] 354 | conn, err = net.DialUDP(udp4, nil, &net.UDPAddr{Port: int(port)}) 355 | if err != nil { 356 | log.Println("Couldn't connect to local UDP port", port, "with error:", err) 357 | return nil, err 358 | } 359 | 360 | if service.UDPBufferSize == 0 { 361 | service.UDPBufferSize = DefaultUDPBufferSize 362 | } 363 | if te.IsServer { 364 | service.UDPBufferSize = MaxUDPBufferSize 365 | } 366 | conn.SetWriteBuffer(service.UDPBufferSize) 367 | conn.SetReadBuffer(service.UDPBufferSize) 368 | 369 | te.serviceConn.Set(connKey, conn, cache.DefaultExpiration) 370 | 371 | prefix := []byte{connID[0], connID[1], serviceID, portID} 372 | go func() { 373 | defer te.serviceConn.Delete(connKey) 374 | serviceBuffer := make([]byte, service.UDPBufferSize) 375 | 376 | for { 377 | n, _, err := conn.ReadFromUDP(serviceBuffer) 378 | if err != nil { 379 | log.Println("Couldn't receive data from service:", err) 380 | Close(conn) 381 | break 382 | } 383 | 384 | te.udpWriteChan <- append(prefix, serviceBuffer[:n]...) 385 | } 386 | }() 387 | } else { 388 | conn = x.(*net.UDPConn) 389 | } 390 | 391 | return conn, nil 392 | } 393 | 394 | func (te *TunaExit) listenUDP(port int) error { 395 | conn, err := net.ListenUDP(udp4, &net.UDPAddr{Port: port}) 396 | if err != nil { 397 | log.Println("Couldn't bind listener:", err) 398 | return err 399 | } 400 | encConn := NewEncryptUDPConn(conn) 401 | udpConn, err := te.wrapUDPConn(encConn, nil, nil, nil) 402 | if err != nil { 403 | log.Println("wrap udp conn err:", err) 404 | return err 405 | } 406 | te.startUDPReaderWriter(udpConn, nil, nil, nil) 407 | te.udpConn = udpConn 408 | te.readUDP() 409 | return nil 410 | } 411 | 412 | func (te *TunaExit) readUDP() { 413 | go func() { 414 | for { 415 | if te.IsClosed() { 416 | return 417 | } 418 | serverReadChan, err := te.GetServerUDPReadChan(false) 419 | if err != nil { 420 | log.Println("Couldn't get server connection:", err) 421 | continue 422 | } 423 | data := <-serverReadChan 424 | if len(data) < PrefixLen { 425 | log.Println("empty udp packet received") 426 | te.Close() 427 | return 428 | } 429 | 430 | serviceConn, err := te.getServiceConn(data[0:2], data[2], data[3]) 431 | if err != nil { 432 | log.Println("get service conn error:", err) 433 | continue 434 | } 435 | _, _, err = serviceConn.WriteMsgUDP(data[PrefixLen:], nil, nil) 436 | if err != nil { 437 | log.Println("Couldn't send data to service:", err) 438 | } 439 | } 440 | }() 441 | } 442 | 443 | func (te *TunaExit) updateAllMetadata(ip string, tcpPort, udpPort uint32) error { 444 | for serviceName, serviceInfo := range te.config.Services { 445 | serviceID, err := te.getServiceID(serviceName) 446 | if err != nil { 447 | return err 448 | } 449 | UpdateMetadata( 450 | serviceName, 451 | serviceID, 452 | nil, 453 | nil, 454 | ip, 455 | tcpPort, 456 | udpPort, 457 | serviceInfo.Price, 458 | te.config.BeneficiaryAddr, 459 | te.config.SubscriptionPrefix, 460 | uint32(te.config.SubscriptionDuration), 461 | te.config.SubscriptionFee, 462 | te.config.SubscriptionReplaceTxPool, 463 | te.Client, 464 | te.closeChan, 465 | ) 466 | } 467 | return nil 468 | } 469 | 470 | func (te *TunaExit) Start() error { 471 | ip, err := ipify.GetIp() 472 | if err != nil { 473 | return fmt.Errorf("couldn't get IP: %v", err) 474 | } 475 | 476 | err = te.listenTCP(int(te.config.ListenTCP)) 477 | if err != nil { 478 | return err 479 | } 480 | 481 | err = te.listenUDP(int(te.config.ListenUDP)) 482 | if err != nil { 483 | return err 484 | } 485 | 486 | return te.updateAllMetadata(ip, uint32(te.config.ListenTCP), uint32(te.config.ListenUDP)) 487 | } 488 | 489 | func (te *TunaExit) StartReverse(shouldReconnect bool) error { 490 | defer te.Close() 491 | 492 | geoCloseChan := make(chan struct{}) 493 | defer close(geoCloseChan) 494 | if len(te.ServiceInfo.IPFilter.GetProviders()) > 0 { 495 | go te.ServiceInfo.IPFilter.StartUpdateDataFile(geoCloseChan) 496 | } 497 | 498 | serviceID := byte(0) 499 | service, err := te.getService(serviceID) 500 | if err != nil { 501 | return err 502 | } 503 | 504 | var paymentStream *smux.Stream 505 | var recipient string 506 | getPaymentStreamRecipient := func() (*smux.Stream, string, error) { 507 | return paymentStream, recipient, nil 508 | } 509 | 510 | var tcpConn net.Conn 511 | var payOnce sync.Once 512 | for { 513 | err = te.Common.CreateServerConn(true) 514 | if err != nil { 515 | if errors.Is(err, ErrClosed) { 516 | return nil 517 | } 518 | log.Println("Couldn't connect to reverse entry:", err) 519 | if errors.Is(err, nkn.ErrInsufficientBalance) { 520 | return err 521 | } 522 | time.Sleep(1 * time.Second) 523 | continue 524 | } 525 | if te.udpConn != nil { 526 | te.startUDPReaderWriter(te.udpConn, nil, &te.reverseBytesEntryToExit, &te.reverseBytesExitToEntry) 527 | } 528 | 529 | var udpConn UDPConn 530 | if len(service.UDP) > 0 { 531 | udpConn, err = te.Common.GetServerUDPConn(false) 532 | if err != nil { 533 | log.Println(err) 534 | time.Sleep(1 * time.Second) 535 | continue 536 | } 537 | go sendPingMsg(udpConn, te.udpCloseChan) 538 | } 539 | 540 | var tcpPorts []uint32 541 | var udpPorts []uint32 542 | if te.config.ReverseRandomPorts { 543 | tcpPorts = make([]uint32, len(service.TCP)) 544 | udpPorts = make([]uint32, len(service.UDP)) 545 | } else { 546 | tcpPorts = service.TCP 547 | udpPorts = service.UDP 548 | } 549 | 550 | serviceMetadata := CreateRawMetadata( 551 | serviceID, 552 | tcpPorts, 553 | udpPorts, 554 | "", 555 | 0, 556 | 0, 557 | "", 558 | te.config.BeneficiaryAddr, 559 | ) 560 | 561 | tcpConn, err = te.Common.GetServerTCPConn(false) 562 | if err != nil { 563 | log.Println(err) 564 | time.Sleep(1 * time.Second) 565 | continue 566 | } 567 | 568 | session, err := smux.Client(tcpConn, nil) 569 | if err != nil { 570 | log.Println(err) 571 | time.Sleep(1 * time.Second) 572 | continue 573 | } 574 | 575 | stream, err := session.OpenStream() 576 | if err != nil { 577 | log.Println("Couldn't open stream to reverse entry:", err) 578 | time.Sleep(1 * time.Second) 579 | continue 580 | } 581 | 582 | err = WriteVarBytes(stream, serviceMetadata) 583 | if err != nil { 584 | log.Println("Couldn't send metadata to reverse entry:", err) 585 | time.Sleep(1 * time.Second) 586 | continue 587 | } 588 | 589 | buf, err := ReadVarBytes(stream, maxServiceMetadataSize) 590 | if err != nil { 591 | log.Println("Couldn't read reverse metadata:", err) 592 | time.Sleep(1 * time.Second) 593 | continue 594 | } 595 | 596 | reverseMetadata, err := ReadMetadata(string(buf)) 597 | if err != nil { 598 | log.Println("Couldn't unmarshal metadata:", err) 599 | time.Sleep(1 * time.Second) 600 | continue 601 | } 602 | 603 | reverseIP := tcpConn.RemoteAddr().(*net.TCPAddr).IP 604 | reverseTCP := reverseMetadata.ServiceTcp 605 | if len(reverseTCP) > 0 { 606 | go func() { 607 | conn, err := net.DialTimeout(tcp4, fmt.Sprintf("%s:%d", reverseIP.String(), reverseTCP[0]), defaultReverseTestTimeout) 608 | if err == nil { 609 | time.Sleep(defaultReverseTestTimeout) 610 | conn.Close() 611 | } 612 | }() 613 | 614 | session.SetDeadline(time.Now().Add(defaultReverseTestTimeout)) 615 | 616 | stream, err := session.AcceptStream() 617 | if err != nil { 618 | log.Println("Couldn't accept stream test conn from reverse entry:", err) 619 | time.Sleep(1 * time.Second) 620 | continue 621 | } 622 | 623 | stream.Close() 624 | 625 | session.SetDeadline(time.Time{}) 626 | } 627 | 628 | ps, err := openPaymentStream(session) 629 | if err != nil { 630 | log.Println("Couldn't open payment stream:", err) 631 | time.Sleep(1 * time.Second) 632 | continue 633 | } 634 | 635 | paymentStream, recipient = ps, te.GetPaymentReceiver() 636 | 637 | te.RLock() 638 | if te.isClosed { 639 | te.RUnlock() 640 | return nil 641 | } 642 | te.reverseIP = reverseIP 643 | te.reverseTCP = reverseTCP 644 | te.reverseUDP = reverseMetadata.ServiceUdp 645 | te.OnConnect.receive() 646 | te.RUnlock() 647 | 648 | if udpConn != nil { 649 | te.readUDP() 650 | } 651 | 652 | payOnce.Do(func() { 653 | go te.startPayment( 654 | &te.reverseBytesEntryToExit, &te.reverseBytesExitToEntry, 655 | &te.reverseBytesEntryToExitPaid, &te.reverseBytesExitToEntryPaid, 656 | te.config.ReverseNanoPayFee, 657 | te.config.MinReverseNanoPayFee, 658 | te.config.ReverseNanoPayFeeRatio, 659 | getPaymentStreamRecipient, 660 | ) 661 | }) 662 | 663 | te.handleSession(session, nil) 664 | 665 | Close(tcpConn) 666 | Close(udpConn) 667 | te.CloseUDPConn() 668 | 669 | if !shouldReconnect { 670 | break 671 | } 672 | 673 | select { 674 | case _, ok := <-te.closeChan: 675 | if !ok { 676 | return nil 677 | } 678 | default: 679 | } 680 | } 681 | 682 | return nil 683 | } 684 | 685 | func (te *TunaExit) GetReverseIP() net.IP { 686 | return te.reverseIP 687 | } 688 | 689 | func (te *TunaExit) GetReverseTCPPorts() []uint32 { 690 | return te.reverseTCP 691 | } 692 | 693 | func (te *TunaExit) GetReverseUDPPorts() []uint32 { 694 | return te.reverseUDP 695 | } 696 | 697 | func (te *TunaExit) Close() { 698 | te.WaitSessions() 699 | 700 | te.Lock() 701 | defer te.Unlock() 702 | 703 | if te.isClosed { 704 | return 705 | } 706 | 707 | te.isClosed = true 708 | close(te.closeChan) 709 | close(te.udpCloseChan) 710 | Close(te.tcpListener) 711 | Close(te.udpConn) 712 | Close(te.tcpConn) 713 | 714 | te.CloseUDPConn() 715 | te.OnConnect.close() 716 | } 717 | 718 | func (te *TunaExit) CloseUDPConn() { 719 | items := te.serviceConn.Items() 720 | for k, v := range items { 721 | conn := v.Object.(*net.UDPConn) 722 | te.serviceConn.Delete(k) 723 | Close(conn) 724 | } 725 | } 726 | 727 | func (te *TunaExit) IsClosed() bool { 728 | te.RLock() 729 | defer te.RUnlock() 730 | return te.isClosed 731 | } 732 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | package tuna 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/nknorg/nkn-sdk-go" 16 | "github.com/nknorg/nkn/v2/common" 17 | "github.com/nknorg/tuna/pb" 18 | "github.com/nknorg/tuna/util" 19 | "github.com/patrickmn/go-cache" 20 | "github.com/rdegges/go-ipify" 21 | "github.com/xtaci/smux" 22 | ) 23 | 24 | type TunaEntry struct { 25 | // It's important to keep these uint64 field on top to avoid panic on arm32 26 | // architecture: https://github.com/golang/go/issues/23345 27 | bytesEntryToExit uint64 28 | bytesEntryToExitPaid uint64 29 | bytesExitToEntry uint64 30 | bytesExitToEntryPaid uint64 31 | reverseBytesEntryToExit uint64 32 | reverseBytesExitToEntry uint64 33 | 34 | *Common 35 | config *EntryConfiguration 36 | tcpListeners map[byte]*net.TCPListener 37 | serviceConn map[byte]*net.UDPConn 38 | clientAddr *cache.Cache 39 | session *smux.Session 40 | paymentStream *smux.Stream 41 | reverseBeneficiary common.Uint160 42 | sessionLock sync.Mutex 43 | tcpPorts []uint32 44 | udpPorts []uint32 45 | } 46 | 47 | func NewTunaEntry(service Service, serviceInfo ServiceInfo, wallet *nkn.Wallet, client *nkn.MultiClient, config *EntryConfiguration) (*TunaEntry, error) { 48 | config, err := MergedEntryConfig(config) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if !config.Reverse { 53 | _, err = common.StringToFixed64(config.NanoPayFee) 54 | if err != nil { 55 | log.Fatalln("Parse NanoPayFee error:", err) 56 | } 57 | _, err = common.StringToFixed64(config.MinNanoPayFee) 58 | if err != nil { 59 | log.Fatalln("Parse MinNanoPayFee error:", err) 60 | } 61 | } 62 | 63 | c, err := NewCommon( 64 | &service, 65 | &serviceInfo, 66 | wallet, 67 | client, 68 | config.SeedRPCServerAddr, 69 | config.DialTimeout, 70 | config.SubscriptionPrefix, 71 | config.Reverse, 72 | config.Reverse, 73 | config.GeoDBPath, 74 | config.DownloadGeoDB, 75 | config.GetSubscribersBatchSize, 76 | config.MeasureBandwidth, 77 | config.MeasureBandwidthTimeout, 78 | config.MeasureBandwidthWorkersTimeout, 79 | config.MeasurementBytesDownLink, 80 | config.MeasureStoragePath, 81 | config.MaxMeasureWorkerPoolSize, 82 | config.TcpDialContext, 83 | config.HttpDialContext, 84 | config.WsDialContext, 85 | config.SortMeasuredNodes, 86 | nil, 87 | config.MinBalance, 88 | ) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | te := &TunaEntry{ 94 | Common: c, 95 | config: config, 96 | tcpListeners: make(map[byte]*net.TCPListener), 97 | serviceConn: make(map[byte]*net.UDPConn), 98 | clientAddr: cache.New(time.Duration(config.UDPTimeout)*time.Second, time.Second), 99 | } 100 | return te, nil 101 | } 102 | 103 | func (te *TunaEntry) Start(shouldReconnect bool) error { 104 | defer te.Close() 105 | 106 | listenIP := net.ParseIP(te.ServiceInfo.ListenIP) 107 | if listenIP == nil { 108 | listenIP = net.ParseIP(defaultServiceListenIP) 109 | } 110 | 111 | tcpPorts, err := te.listenTCP(listenIP, te.Service.TCP) 112 | if err != nil { 113 | return err 114 | } 115 | if len(tcpPorts) > 0 { 116 | log.Printf("Serving %s on localhost tcp port %v", te.Service.Name, tcpPorts) 117 | te.tcpPorts = tcpPorts 118 | } 119 | 120 | udpPorts, err := te.listenUDP(listenIP, te.Service.UDP) 121 | if err != nil { 122 | return err 123 | } 124 | if len(udpPorts) > 0 { 125 | log.Printf("Serving %s on localhost udp port %v", te.Service.Name, udpPorts) 126 | te.udpPorts = udpPorts 127 | } 128 | 129 | for { 130 | if te.IsClosed() { 131 | return nil 132 | } 133 | 134 | err := te.CreateServerConn(true) 135 | if err != nil { 136 | log.Println("Couldn't connect to node:", err) 137 | if errors.Is(err, nkn.ErrInsufficientBalance) { 138 | return err 139 | } 140 | time.Sleep(1 * time.Second) 141 | continue 142 | } 143 | if te.udpConn != nil { 144 | te.startUDPReaderWriter(te.udpConn, nil, &te.bytesExitToEntry, &te.bytesEntryToExit) 145 | go sendPingMsg(te.udpConn, te.udpCloseChan) 146 | } 147 | go func() { 148 | for { 149 | session, err := te.getSession() 150 | if err != nil { 151 | return 152 | } 153 | 154 | _, err = session.AcceptStream() 155 | if err != nil { 156 | log.Println("Close connection:", err) 157 | session.Close() 158 | if !shouldReconnect { 159 | te.Close() 160 | return 161 | } 162 | } 163 | } 164 | }() 165 | 166 | getPaymentStreamRecipient := func() (*smux.Stream, string, error) { 167 | ps, err := te.getPaymentStream() 168 | return ps, te.GetPaymentReceiver(), err 169 | } 170 | 171 | go te.startPayment( 172 | &te.bytesEntryToExit, &te.bytesExitToEntry, 173 | &te.bytesEntryToExitPaid, &te.bytesExitToEntryPaid, 174 | te.config.NanoPayFee, 175 | te.config.MinNanoPayFee, 176 | te.config.NanoPayFeeRatio, 177 | getPaymentStreamRecipient, 178 | ) 179 | 180 | break 181 | } 182 | 183 | geoCloseChan := make(chan struct{}) 184 | defer close(geoCloseChan) 185 | if te.ServiceInfo.IPFilter != nil && len(te.ServiceInfo.IPFilter.GetProviders()) > 0 { 186 | go te.ServiceInfo.IPFilter.StartUpdateDataFile(geoCloseChan) 187 | } 188 | 189 | <-te.closeChan 190 | 191 | return nil 192 | } 193 | 194 | func (te *TunaEntry) StartReverse(stream *smux.Stream, connMetadata *pb.ConnectionMetadata) error { 195 | defer te.Close() 196 | 197 | metadata := te.GetMetadata() 198 | listenIP := net.ParseIP(te.ServiceInfo.ListenIP) 199 | if listenIP == nil { 200 | listenIP = net.ParseIP(defaultServiceListenIP) 201 | } 202 | tcpPorts, err := te.listenTCP(listenIP, metadata.ServiceTcp) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | var udpPorts []uint32 208 | if len(metadata.ServiceUdp) > 0 { 209 | if len(te.Service.UDP) > 0 || metadata.ServiceUdp[0] == 0 { 210 | metadata.ServiceUdp = tcpPorts // same ports with tcp if udp ports not specific 211 | } 212 | udpPorts, err = te.listenUDP(listenIP, metadata.ServiceUdp) 213 | if err != nil { 214 | return err 215 | } 216 | } 217 | 218 | serviceMetadata := CreateRawMetadata(0, tcpPorts, udpPorts, "", 0, 0, "", te.config.ReverseBeneficiaryAddr) 219 | err = WriteVarBytes(stream, serviceMetadata) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | session, err := te.getSession() 225 | if err != nil { 226 | return err 227 | } 228 | 229 | var bytesPaid, lastPaymentAmount common.Fixed64 230 | lastPaymentTime := time.Now() 231 | claimInterval := time.Duration(te.config.ReverseClaimInterval) * time.Second 232 | onErr := nkn.NewOnError(1, nil) 233 | isClosed := false 234 | 235 | entryToExitPrice, exitToEntryPrice, err := ParsePrice(te.config.ReversePrice) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | npc, err := te.Client.NewNanoPayClaimer(te.config.ReverseBeneficiaryAddr, int32(claimInterval/time.Millisecond), int32(nanoPayClaimerLinger/time.Millisecond), te.config.ReverseMinFlushAmount, onErr) 241 | if err != nil { 242 | return err 243 | } 244 | 245 | defer npc.Close() 246 | k := string(append(connMetadata.PublicKey, connMetadata.Nonce...)) 247 | 248 | getTotalCost := func() (common.Fixed64, common.Fixed64) { 249 | cost, totalBytes := common.Fixed64(0), common.Fixed64(0) 250 | for i := range te.Common.reverseBytesEntryToExit[k] { 251 | entryToExit := common.Fixed64(atomic.LoadUint64(&te.Common.reverseBytesEntryToExit[k][i])) 252 | exitToEntry := common.Fixed64(atomic.LoadUint64(&te.Common.reverseBytesExitToEntry[k][i])) 253 | if entryToExit == 0 && exitToEntry == 0 { 254 | continue 255 | } 256 | cost += entryToExitPrice*entryToExit/TrafficUnit + exitToEntryPrice*exitToEntry/TrafficUnit 257 | totalBytes += entryToExit + exitToEntry 258 | } 259 | tcpBytesEntryToExit := common.Fixed64(atomic.LoadUint64(&te.reverseBytesEntryToExit)) 260 | tcpBytesExitToEntry := common.Fixed64(atomic.LoadUint64(&te.reverseBytesExitToEntry)) 261 | totalBytes += tcpBytesEntryToExit + tcpBytesExitToEntry 262 | cost += entryToExitPrice*tcpBytesEntryToExit/TrafficUnit + exitToEntryPrice*tcpBytesExitToEntry/TrafficUnit 263 | return cost, totalBytes 264 | } 265 | 266 | go checkNanoPayClaim(session, npc, onErr, &isClosed) 267 | 268 | go checkPayment(session, &lastPaymentTime, &lastPaymentAmount, &bytesPaid, &isClosed, getTotalCost) 269 | 270 | for { 271 | if te.IsClosed() { 272 | return nil 273 | } 274 | stream, err := session.AcceptStream() 275 | if err != nil { 276 | log.Println("Couldn't accept stream:", err) 277 | session.Close() 278 | break 279 | } 280 | 281 | go func(stream *smux.Stream) { 282 | err := func() error { 283 | streamMetadata, err := readStreamMetadata(stream) 284 | if err != nil { 285 | return fmt.Errorf("read stream metadata error: %v", err) 286 | } 287 | 288 | if streamMetadata.IsPayment { 289 | return handlePaymentStream(stream, npc, &lastPaymentTime, &lastPaymentAmount, &bytesPaid, getTotalCost) 290 | } 291 | return nil 292 | }() 293 | if err != nil { 294 | log.Println(err) 295 | Close(stream) 296 | } 297 | }(stream) 298 | } 299 | 300 | return nil 301 | } 302 | 303 | func (te *TunaEntry) GetTCPPorts() []uint32 { 304 | return te.tcpPorts 305 | } 306 | 307 | func (te *TunaEntry) GetUDPPorts() []uint32 { 308 | return te.udpPorts 309 | } 310 | 311 | func (te *TunaEntry) Close() { 312 | te.WaitSessions() 313 | 314 | te.Lock() 315 | defer te.Unlock() 316 | 317 | if te.isClosed { 318 | return 319 | } 320 | 321 | te.isClosed = true 322 | close(te.closeChan) 323 | close(te.udpCloseChan) 324 | for _, listener := range te.tcpListeners { 325 | Close(listener) 326 | } 327 | for _, conn := range te.serviceConn { 328 | Close(conn) 329 | } 330 | if te.session != nil { 331 | te.session.Close() 332 | } 333 | te.OnConnect.close() 334 | } 335 | 336 | func (te *TunaEntry) IsClosed() bool { 337 | te.RLock() 338 | defer te.RUnlock() 339 | return te.isClosed 340 | } 341 | 342 | func (te *TunaEntry) createSession(force bool) (*smux.Session, *smux.Stream, error) { 343 | conn, err := te.GetServerTCPConn(force) 344 | if err != nil { 345 | return nil, nil, err 346 | } 347 | 348 | session, err := smux.Client(conn, nil) 349 | if err != nil { 350 | return nil, nil, err 351 | } 352 | 353 | paymentStream, err := openPaymentStream(session) 354 | if err != nil { 355 | return nil, nil, err 356 | } 357 | 358 | return session, paymentStream, nil 359 | } 360 | 361 | func (te *TunaEntry) getSession() (*smux.Session, error) { 362 | te.sessionLock.Lock() 363 | defer te.sessionLock.Unlock() 364 | 365 | if te.session == nil || te.session.IsClosed() { 366 | if te.Reverse { 367 | return nil, errors.New("reverse connection to exit is dead") 368 | } 369 | 370 | session, paymentStream, err := te.createSession(false) 371 | if err != nil { 372 | session, paymentStream, err = te.createSession(true) 373 | if err != nil { 374 | return nil, err 375 | } 376 | } 377 | 378 | te.session = session 379 | te.paymentStream = paymentStream 380 | } 381 | 382 | return te.session, nil 383 | } 384 | 385 | func (te *TunaEntry) getPaymentStream() (*smux.Stream, error) { 386 | _, err := te.getSession() 387 | if err != nil { 388 | return nil, err 389 | } 390 | paymentStream := te.paymentStream 391 | if paymentStream == nil { 392 | return nil, errors.New("nil payment stream") 393 | } 394 | return paymentStream, nil 395 | } 396 | 397 | func (te *TunaEntry) openServiceStream(portID byte) (*smux.Stream, error) { 398 | session, err := te.getSession() 399 | if err != nil { 400 | return nil, err 401 | } 402 | 403 | stream, err := session.OpenStream() 404 | if err != nil { 405 | session.Close() 406 | return nil, err 407 | } 408 | 409 | streamMetadata := &pb.StreamMetadata{ 410 | ServiceId: te.GetMetadata().ServiceId, 411 | PortId: uint32(portID), 412 | IsPayment: false, 413 | } 414 | 415 | err = writeStreamMetadata(stream, streamMetadata) 416 | if err != nil { 417 | stream.Close() 418 | return nil, err 419 | } 420 | 421 | return stream, nil 422 | } 423 | 424 | func (te *TunaEntry) listenTCP(ip net.IP, ports []uint32) ([]uint32, error) { 425 | assignedPorts := make([]uint32, 0, len(ports)) 426 | for i, _port := range ports { 427 | listener, err := net.ListenTCP(tcp4, &net.TCPAddr{IP: ip, Port: int(_port)}) 428 | if err != nil { 429 | log.Println("Couldn't bind listener:", err) 430 | return nil, err 431 | } 432 | port := listener.Addr().(*net.TCPAddr).Port 433 | portID := byte(i) 434 | assignedPorts = append(assignedPorts, uint32(port)) 435 | 436 | te.tcpListeners[portID] = listener 437 | 438 | go func() { 439 | for { 440 | conn, err := listener.Accept() 441 | if c, ok := conn.(*net.TCPConn); ok { 442 | err := c.SetLinger(5) 443 | if err != nil { 444 | log.Println("Couldn't set linger:", err) 445 | continue 446 | } 447 | } 448 | if err != nil { 449 | if te.IsClosed() { 450 | return 451 | } 452 | if strings.Contains(err.Error(), "use of closed network connection") { 453 | te.Close() 454 | return 455 | } 456 | log.Println("Couldn't accept connection:", err) 457 | time.Sleep(time.Second) 458 | continue 459 | } 460 | 461 | go func() { 462 | if te.IsClosed() { 463 | return 464 | } 465 | stream, err := te.openServiceStream(portID) 466 | if err != nil { 467 | log.Println("Couldn't open stream:", err) 468 | Close(conn) 469 | return 470 | } 471 | 472 | if te.config.Reverse { 473 | go te.pipe(stream, conn, &te.reverseBytesEntryToExit) 474 | go te.pipe(conn, stream, &te.reverseBytesExitToEntry) 475 | } else { 476 | go te.pipe(stream, conn, &te.bytesEntryToExit) 477 | go te.pipe(conn, stream, &te.bytesExitToEntry) 478 | } 479 | }() 480 | } 481 | }() 482 | } 483 | 484 | return assignedPorts, nil 485 | } 486 | 487 | func (te *TunaEntry) listenUDP(ip net.IP, ports []uint32) ([]uint32, error) { 488 | assignedPorts := make([]uint32, 0, len(ports)) 489 | if len(ports) == 0 { 490 | return assignedPorts, nil 491 | } 492 | 493 | go func() { 494 | for { 495 | if te.IsClosed() { 496 | return 497 | } 498 | 499 | serverReadChan, err := te.GetServerUDPReadChan(false) 500 | if err != nil { 501 | log.Println("Couldn't get server connection:", err) 502 | continue 503 | } 504 | 505 | data := <-serverReadChan 506 | 507 | if len(data) < PrefixLen { 508 | log.Println("empty udp packet received") 509 | te.Close() 510 | return 511 | } 512 | portID := data[3] 513 | port := ConnIDToPort(data) 514 | connID := strconv.Itoa(int(port)) 515 | 516 | var serviceConn *net.UDPConn 517 | var ok bool 518 | if serviceConn, ok = te.serviceConn[portID]; !ok { 519 | log.Println("Couldn't get service conn for portId:", portID) 520 | continue 521 | } 522 | 523 | var x interface{} 524 | if x, ok = te.clientAddr.Get(connID); !ok { 525 | log.Println("Couldn't get client address for:", connID) 526 | continue 527 | } 528 | clientAddr := x.(*net.UDPAddr) 529 | 530 | _, _, err = serviceConn.WriteMsgUDP(data[PrefixLen:], nil, clientAddr) 531 | if err != nil { 532 | log.Println("Couldn't send data to client:", err) 533 | } 534 | } 535 | }() 536 | 537 | for i, _port := range ports { 538 | localConn, err := net.ListenUDP(udp4, &net.UDPAddr{IP: ip, Port: int(_port)}) 539 | if err != nil { 540 | log.Println("Couldn't bind listener:", err) 541 | return nil, err 542 | } 543 | 544 | bs := te.Service.UDPBufferSize 545 | if te.Reverse { 546 | bs = MaxUDPBufferSize 547 | } 548 | localConn.SetWriteBuffer(bs) 549 | localConn.SetReadBuffer(bs) 550 | 551 | port := localConn.LocalAddr().(*net.UDPAddr).Port 552 | portID := byte(i) 553 | assignedPorts = append(assignedPorts, uint32(port)) 554 | 555 | te.serviceConn[portID] = localConn 556 | 557 | go func() { 558 | localBuffer := make([]byte, bs) 559 | for { 560 | if te.IsClosed() { 561 | return 562 | } 563 | 564 | n, addr, err := localConn.ReadFromUDP(localBuffer) 565 | if err != nil { 566 | log.Println("Couldn't receive data from local:", err) 567 | te.Close() 568 | return 569 | } 570 | 571 | connKey := strconv.Itoa(addr.Port) 572 | te.clientAddr.Set(connKey, addr, cache.DefaultExpiration) 573 | 574 | serverWriteChan, err := te.GetServerUDPWriteChan(false) 575 | if err != nil { 576 | log.Println("Couldn't get remote connection:", err) 577 | continue 578 | } 579 | connID := PortToConnID(uint16(addr.Port)) 580 | serviceID := te.GetMetadata().ServiceId 581 | serverWriteChan <- append([]byte{connID[0], connID[1], byte(serviceID), portID}, localBuffer[:n]...) 582 | } 583 | }() 584 | } 585 | 586 | return assignedPorts, nil 587 | } 588 | 589 | func StartReverse(config *EntryConfiguration, wallet *nkn.Wallet) error { 590 | config, err := MergedEntryConfig(config) 591 | if err != nil { 592 | return err 593 | } 594 | 595 | var serviceListenIP string 596 | if net.ParseIP(config.ReverseServiceListenIP) == nil { 597 | serviceListenIP = defaultReverseServiceListenIP 598 | } else { 599 | serviceListenIP = config.ReverseServiceListenIP 600 | } 601 | 602 | ip, err := ipify.GetIp() 603 | if err != nil { 604 | return fmt.Errorf("couldn't get IP: %v", err) 605 | } 606 | 607 | listener, err := net.ListenTCP(tcp4, &net.TCPAddr{Port: int(config.ReverseTCP)}) 608 | if err != nil { 609 | return err 610 | } 611 | 612 | uConn, err := net.ListenUDP(udp4, &net.UDPAddr{Port: int(config.ReverseUDP)}) 613 | if err != nil { 614 | return err 615 | } 616 | encConn := NewEncryptUDPConn(uConn) 617 | var encKeys, udpEntrys, tcpEntrys, tcpReady, udpReady, addrToKey, keyToAddr sync.Map 618 | go func() { 619 | if encConn.IsClosed() { 620 | return 621 | } 622 | buffer := make([]byte, MaxUDPBufferSize) 623 | for { 624 | n, from, encrypted, err := encConn.ReadFromUDPEncrypted(buffer) 625 | if err != nil { 626 | log.Println("Couldn't receive exit's data:", err) 627 | continue 628 | } 629 | if bytes.Equal(buffer[:PrefixLen], []byte{PrefixLen - 1: 0}) && n > PrefixLen { 630 | connMetadata, err := parseUDPConnMetadata(buffer[PrefixLen:n]) 631 | if err != nil { 632 | log.Println("Couldn't read udp metadata from client:", err) 633 | continue 634 | } 635 | if connMetadata.IsPing || encrypted { 636 | continue 637 | } 638 | k := string(append(connMetadata.PublicKey, connMetadata.Nonce...)) 639 | readyChan, _ := tcpReady.LoadOrStore(k, make(chan struct{})) 640 | <-readyChan.(chan struct{}) 641 | 642 | encryptKey, ok := encKeys.Load(k) 643 | if !ok { 644 | log.Println("no encrypt key found") 645 | continue 646 | } 647 | key := encryptKey.(*[encryptKeySize]byte) 648 | err = encConn.AddCodec(from, key, connMetadata.EncryptionAlgo, false) 649 | if err != nil { 650 | log.Println(err) 651 | return 652 | } 653 | 654 | te, ok := tcpEntrys.Load(k) 655 | if !ok { 656 | log.Println("no encrypt key found from tcp conn") 657 | continue 658 | } 659 | t := te.(*TunaEntry) 660 | t.Common.reverseBytesEntryToExit[k] = make([]uint64, 256) 661 | t.Common.reverseBytesExitToEntry[k] = make([]uint64, 256) 662 | udpEntrys.Store(from.String(), te) 663 | addrToKey.Store(from.String(), k) 664 | keyToAddr.Store(k, from.String()) 665 | 666 | if c, ok := udpReady.Load(k); ok { 667 | closeChan(c.(chan struct{})) 668 | } 669 | 670 | continue 671 | } 672 | if !encrypted { 673 | log.Println("Unencrypted udp packet received") 674 | continue 675 | } 676 | entry, ok := udpEntrys.Load(from.String()) 677 | if !ok { 678 | log.Println("no entry found for udp data") 679 | continue 680 | } 681 | te := entry.(*TunaEntry) 682 | udpReadchan, err := te.GetServerUDPReadChan(false) 683 | if err != nil { 684 | log.Println("Couldn't get udp read chan:", err) 685 | continue 686 | } 687 | if n > 0 { 688 | k, ok := addrToKey.Load(from.String()) 689 | if !ok { 690 | log.Println("no key found for udp data") 691 | continue 692 | } 693 | b := make([]byte, n) 694 | copy(b, buffer[:n]) 695 | udpReadchan <- b 696 | atomic.AddUint64(&te.Common.reverseBytesEntryToExit[k.(string)][b[2]], uint64(n)) 697 | } 698 | } 699 | }() 700 | 701 | clientConfig := &nkn.ClientConfig{ 702 | HttpDialContext: config.HttpDialContext, 703 | WsDialContext: config.WsDialContext, 704 | } 705 | if len(config.SeedRPCServerAddr) > 0 { 706 | clientConfig.SeedRPCServerAddr = nkn.NewStringArray(config.SeedRPCServerAddr...) 707 | } 708 | client, err := nkn.NewMultiClient(wallet.Account(), randomIdentifier(), numRPCClients, false, clientConfig) 709 | if err != nil { 710 | return err 711 | } 712 | 713 | go func() { 714 | for { 715 | tcpConn, err := listener.Accept() 716 | if c, ok := tcpConn.(*net.TCPConn); ok { 717 | err := c.SetLinger(5) 718 | if err != nil { 719 | log.Println("Couldn't set linger:", err) 720 | continue 721 | } 722 | } 723 | if err != nil { 724 | if strings.Contains(err.Error(), "use of closed network connection") { 725 | return 726 | } 727 | log.Println("Couldn't accept client connection:", err) 728 | time.Sleep(time.Second) 729 | continue 730 | } 731 | 732 | go func() { 733 | err := func() error { 734 | te, err := NewTunaEntry(Service{}, ServiceInfo{ListenIP: serviceListenIP}, wallet, client, config) 735 | if err != nil { 736 | return fmt.Errorf("create tuna entry error: %v", err) 737 | } 738 | encryptedConn, connMetadata, err := te.wrapConn(tcpConn, nil, nil) 739 | if err != nil { 740 | te.Close() 741 | return fmt.Errorf("wrap conn error: %v", err) 742 | } 743 | 744 | connKey := string(append(connMetadata.PublicKey, connMetadata.Nonce...)) 745 | tcpEntrys.Store(connKey, te) 746 | k, _ := te.encryptKeys.Load(connKey) 747 | encKeys.Store(connKey, k) 748 | 749 | rc, _ := tcpReady.LoadOrStore(connKey, make(chan struct{})) 750 | readyChan := rc.(chan struct{}) 751 | closeChan(readyChan) 752 | 753 | udpReady.Store(connKey, make(chan struct{})) 754 | 755 | defer func() { 756 | Close(encryptedConn) 757 | te, ok := tcpEntrys.Load(k) 758 | if ok { 759 | t := te.(*TunaEntry) 760 | t.Close() 761 | } 762 | tcpEntrys.Delete(connKey) 763 | encKeys.Delete(connKey) 764 | }() 765 | 766 | if connMetadata.IsMeasurement { 767 | err = util.BandwidthMeasurementServer(encryptedConn, int(connMetadata.MeasurementBytesDownlink), maxMeasureBandwidthTimeout) 768 | if err != nil { 769 | return fmt.Errorf("bandwidth measurement server error: %v", err) 770 | } 771 | return nil 772 | } 773 | 774 | te.session, err = smux.Server(encryptedConn, nil) 775 | if err != nil { 776 | return fmt.Errorf("create session error: %v", err) 777 | } 778 | 779 | stream, err := te.session.AcceptStream() 780 | if err != nil { 781 | te.Close() 782 | return fmt.Errorf("couldn't accept stream: %v", err) 783 | } 784 | 785 | buf, err := ReadVarBytes(stream, maxServiceMetadataSize) 786 | if err != nil { 787 | return fmt.Errorf("couldn't read service metadata: %v", err) 788 | } 789 | 790 | metadata, err := ReadMetadata(string(buf)) 791 | if err != nil { 792 | return fmt.Errorf("couldn't decode service metadata: %v", err) 793 | } 794 | 795 | te.SetMetadata(metadata) 796 | 797 | te.SetServerTCPConn(encryptedConn) 798 | 799 | if len(metadata.ServiceUdp) > 0 { 800 | go func() { 801 | tr, ok := tcpReady.Load(connKey) 802 | if !ok { 803 | return 804 | } 805 | <-tr.(chan struct{}) 806 | 807 | ur, ok := udpReady.Load(connKey) 808 | if !ok { 809 | return 810 | } 811 | <-ur.(chan struct{}) 812 | 813 | addr, ok := keyToAddr.Load(connKey) 814 | if !ok { 815 | return 816 | } 817 | clientAddr := addr.(string) 818 | 819 | ip, portStr, err := net.SplitHostPort(clientAddr) 820 | if err != nil { 821 | log.Printf("parse host error: %v\n", err) 822 | return 823 | } 824 | port, err := strconv.Atoi(portStr) 825 | if err != nil { 826 | log.Printf("parse port error: %v\n", err) 827 | return 828 | } 829 | 830 | udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: port} 831 | for { 832 | if te.isClosed { 833 | return 834 | } 835 | select { 836 | case data := <-te.udpWriteChan: 837 | n, _, err := encConn.WriteMsgUDP(data, nil, &udpAddr) 838 | if err != nil { 839 | log.Println("couldn't send udp data to server:", err) 840 | continue 841 | } 842 | key, ok := addrToKey.Load(udpAddr.String()) 843 | if !ok || len(data) < 2 { 844 | log.Println("no key found from this udp addr:", udpAddr.String()) 845 | continue 846 | } 847 | atomic.AddUint64(&te.Common.reverseBytesExitToEntry[key.(string)][data[2]], uint64(n)) 848 | case <-te.udpCloseChan: 849 | return 850 | } 851 | } 852 | }() 853 | } 854 | 855 | err = te.StartReverse(stream, connMetadata) 856 | if err != nil { 857 | te.Close() 858 | return fmt.Errorf("start reverse error: %v", err) 859 | } 860 | 861 | return nil 862 | }() 863 | if err != nil { 864 | tcpConn.Close() 865 | log.Println(err) 866 | } 867 | }() 868 | } 869 | }() 870 | 871 | for _, rsn := range strings.Split(config.ReverseServiceName, ",") { 872 | UpdateMetadata( 873 | strings.Trim(rsn, " "), 874 | 0, 875 | nil, 876 | nil, 877 | ip, 878 | uint32(config.ReverseTCP), 879 | uint32(config.ReverseUDP), 880 | config.ReversePrice, 881 | config.ReverseBeneficiaryAddr, 882 | config.ReverseSubscriptionPrefix, 883 | uint32(config.ReverseSubscriptionDuration), 884 | config.ReverseSubscriptionFee, 885 | config.ReverseSubscriptionReplaceTxPool, 886 | client, 887 | make(chan struct{}), 888 | ) 889 | } 890 | 891 | return nil 892 | } 893 | --------------------------------------------------------------------------------