├── pkg ├── meepo │ ├── method.go │ ├── copy.go │ ├── version.go │ ├── packet │ │ ├── error.go │ │ ├── constant.go │ │ ├── header_test.go │ │ ├── broadcast_header.go │ │ ├── broadcast_packet.go │ │ ├── header.go │ │ └── packet.go │ ├── auth │ │ ├── authentication.go │ │ └── authorization.go │ ├── list_transports.go │ ├── list_teleportations.go │ ├── util.go │ ├── authentication.go │ ├── ping.go │ ├── transport.go │ ├── authorization.go │ ├── close_transport.go │ ├── teleport.go │ ├── error.go │ ├── close_teleportation.go │ ├── wire.go │ ├── option.go │ ├── teleportation.go │ ├── acl_test.go │ ├── new_transport.go │ ├── crypt.go │ └── acl.go ├── api │ ├── errors.go │ ├── http │ │ ├── healthz.go │ │ ├── shutdown.go │ │ ├── whoami.go │ │ ├── option.go │ │ ├── error.go │ │ ├── version.go │ │ ├── list_transports.go │ │ ├── close_transport.go │ │ ├── close_teleportation.go │ │ ├── ping.go │ │ ├── list_teleportations.go │ │ ├── router.go │ │ ├── new_transport.go │ │ ├── api.go │ │ ├── teleport.go │ │ └── new_teleportation.go │ ├── encoding │ │ ├── version.go │ │ ├── datachannel.go │ │ ├── transport.go │ │ └── teleportation.go │ ├── option.go │ └── api.go ├── util │ ├── sync │ │ ├── mutex.go │ │ ├── error.go │ │ └── channel.go │ ├── daemon │ │ ├── daemon_unix.go │ │ └── daemon_windows.go │ ├── random │ │ └── random.go │ ├── fs │ │ └── fs.go │ ├── base36 │ │ └── base36.go │ ├── version │ │ └── version.go │ ├── msgpack │ │ └── msgpack.go │ ├── crypt │ │ ├── curve25519.go │ │ └── ed25519.go │ ├── conn │ │ └── rw.go │ └── group │ │ └── group.go ├── sdk │ ├── http │ │ ├── shutdown.go │ │ ├── ping.go │ │ ├── close_transport.go │ │ ├── whoami.go │ │ ├── version.go │ │ ├── close_teleportation.go │ │ ├── list_transports.go │ │ ├── list_teleportations.go │ │ ├── new_transport.go │ │ ├── teleport.go │ │ ├── new_teleportation.go │ │ └── sdk.go │ ├── error.go │ ├── base.go │ └── sdk.go ├── transport │ ├── error.go │ ├── webrtc │ │ ├── error.go │ │ ├── util.go │ │ ├── option.go │ │ └── datachannel.go │ ├── datachannel.go │ ├── option.go │ ├── transport.go │ └── loopback │ │ └── datachannel.go ├── signaling │ ├── error.go │ ├── redis │ │ ├── error.go │ │ ├── option.go │ │ └── signaling_test.go │ ├── option.go │ ├── chain │ │ ├── option.go │ │ └── signaling.go │ └── signaling.go ├── ofn │ └── ofn.go └── teleportation │ ├── teleportation.go │ ├── option.go │ ├── sink.go │ └── source.go ├── donations ├── btc.png └── eth.png ├── bin └── meepod.wrapper ├── cmd ├── config │ ├── log_config.go │ ├── path_windows.go │ ├── path_darwin.go │ ├── path_linux.go │ ├── acl_config.go │ ├── proxy_config.go │ ├── error.go │ ├── signaling_config.go │ ├── api_config.go │ ├── transport_config.go │ ├── auth_config.go │ ├── config_test.go │ ├── unmarshal.go │ ├── meepo_config.go │ └── config.go ├── transport.go ├── teleportation.go ├── config.go ├── root.go ├── whoami.go ├── shutdown.go ├── ping.go ├── transport_new.go ├── transport_close.go ├── config_get.go ├── teleportation_close.go ├── transport_list.go ├── util.go ├── config_set.go ├── teleportation_list.go ├── version.go ├── config_init.go ├── teleport.go ├── teleportation_new.go ├── keygen.go ├── ncat.go ├── ssh.go └── serve.go ├── main.go ├── hack └── summon │ ├── monitor.sh │ ├── launch.sh │ ├── summon.sh │ └── start.sh ├── .gitignore ├── LICENSE ├── go.mod ├── .github └── workflows │ └── goreleaser.yaml └── .goreleaser.yml /pkg/meepo/method.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | type Method string 4 | -------------------------------------------------------------------------------- /donations/btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeerXu/meepo/HEAD/donations/btc.png -------------------------------------------------------------------------------- /donations/eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeerXu/meepo/HEAD/donations/eth.png -------------------------------------------------------------------------------- /bin/meepod.wrapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $SNAP/meepo serve --daemon=false 4 | -------------------------------------------------------------------------------- /pkg/meepo/copy.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | type Copier interface { 4 | Copy() interface{} 5 | } 6 | -------------------------------------------------------------------------------- /cmd/config/log_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type LogConfig struct { 4 | Level string `yaml:"level"` 5 | } 6 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/PeerXu/meepo/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /cmd/config/path_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package config 4 | 5 | func GetDefaultConfigPath() string { 6 | return "meepo.yaml" 7 | } 8 | -------------------------------------------------------------------------------- /cmd/config/path_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package config 4 | 5 | func GetDefaultConfigPath() string { 6 | return "~/.meepo/config.yaml" 7 | } 8 | -------------------------------------------------------------------------------- /cmd/config/path_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package config 4 | 5 | func GetDefaultConfigPath() string { 6 | return "/etc/meepo/meepo.yaml" 7 | } 8 | -------------------------------------------------------------------------------- /pkg/api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | func UnsupportedServer(name string) error { 6 | return fmt.Errorf("Unsupported server: %s", name) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/util/sync/mutex.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import "sync" 4 | 5 | type Locker = sync.Locker 6 | 7 | func NewLock() Locker { 8 | return &sync.Mutex{} 9 | } 10 | -------------------------------------------------------------------------------- /cmd/config/acl_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type AclConfig struct { 4 | Allows []string `yaml:"allows,omitempty"` 5 | Blocks []string `yaml:"blocks,omitempty"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/meepo/version.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import "github.com/PeerXu/meepo/pkg/util/version" 4 | 5 | func (mp *Meepo) Version() *version.V { 6 | return version.Get() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/meepo/packet/error.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ErrInvalidPacket = fmt.Errorf("invalid packet") 7 | ErrPacketIsNil = fmt.Errorf("packet is nil") 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/util/sync/error.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ChannelExistError = fmt.Errorf("Channel exist") 7 | ChannelNotExistError = fmt.Errorf("Channel not exist") 8 | ) 9 | -------------------------------------------------------------------------------- /hack/summon/monitor.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ "x${MEEPO_ID}" == "x" ]; then 4 | echo "require MEEPO_ID" 5 | exit 1 6 | fi 7 | 8 | sudo docker exec -it -w /root meepo_${MEEPO_ID} /bin/sh 9 | -------------------------------------------------------------------------------- /pkg/util/daemon/daemon_unix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin 2 | 3 | package daemon 4 | 5 | import "github.com/VividCortex/godaemon" 6 | 7 | func Daemon() { 8 | godaemon.MakeDaemon(&godaemon.DaemonAttr{}) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/api/http/healthz.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (*HttpServer) Healthz(c *gin.Context) { 10 | c.String(http.StatusOK, "ok") 11 | } 12 | -------------------------------------------------------------------------------- /pkg/sdk/http/shutdown.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func (t *MeepoSDK) Shutdown() error { 8 | return t.doRequest("/v1/actions/shutdown", nil, nil, http.StatusNoContent) 9 | } 10 | -------------------------------------------------------------------------------- /cmd/config/proxy_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ProxyConfig struct { 4 | Socks5 *Socks5Config `yaml:"socks5"` 5 | } 6 | 7 | type Socks5Config struct { 8 | Host string `yaml:"host"` 9 | Port int32 `yaml:"port"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/util/random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var ( 9 | Random *rand.Rand 10 | ) 11 | 12 | func init() { 13 | Random = rand.New(rand.NewSource(time.Now().UnixNano())) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/util/fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | func EnsureDirectoryExist(p string) (err error) { 9 | if err = os.MkdirAll(path.Dir(p), 0755); err != nil { 10 | return 11 | } 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /pkg/meepo/auth/authentication.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "github.com/PeerXu/meepo/pkg/ofn" 4 | 5 | type AuthenticateOption = ofn.OFN 6 | 7 | type Authentication interface { 8 | Authenticate(sub string, opts ...AuthenticateOption) error 9 | } 10 | -------------------------------------------------------------------------------- /pkg/meepo/auth/authorization.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "github.com/PeerXu/meepo/pkg/ofn" 4 | 5 | type AuthorizeOption = ofn.OFN 6 | 7 | type Authorization interface { 8 | Authorize(sub, obj, act string, opts ...AuthorizeOption) error 9 | } 10 | -------------------------------------------------------------------------------- /pkg/meepo/packet/constant.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | type Type string 4 | 5 | const ( 6 | Request Type = "request" 7 | Response Type = "response" 8 | BroadcastRequest Type = "broadcastRequest" 9 | BroadcastResponse Type = "broadcastResponse" 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/transport/error.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "fmt" 4 | 5 | var ( 6 | DataChannelNotFoundError = fmt.Errorf("DataChannel not found") 7 | ) 8 | 9 | func UnsupportedTransportError(name string) error { 10 | return fmt.Errorf("Unsupported transport: %s", name) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/api/encoding/version.go: -------------------------------------------------------------------------------- 1 | package encoding_api 2 | 3 | type Version struct { 4 | Version string `json:"version"` 5 | GoVersion string `json:"goVersion"` 6 | GitHash string `json:"gitHash"` 7 | Built string `json:"built"` 8 | Platform string `json:"platform"` 9 | } 10 | -------------------------------------------------------------------------------- /pkg/util/daemon/daemon_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package daemon 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | // TODO: Support daemon mode on Windows. 11 | func Daemon() { 12 | fmt.Fprintf(os.Stderr, "Windows not support daemon now, ignore daemon flag\n") 13 | } 14 | -------------------------------------------------------------------------------- /cmd/transport.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var ( 6 | transportCmd = &cobra.Command{ 7 | Use: "transport", 8 | Aliases: []string{"t"}, 9 | Short: "Meepo transport subcommand", 10 | } 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(transportCmd) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/api/option.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/meepo" 5 | "github.com/PeerXu/meepo/pkg/ofn" 6 | ) 7 | 8 | type NewServerOption = ofn.OFN 9 | 10 | func WithMeepo(meepo *meepo.Meepo) NewServerOption { 11 | return func(o ofn.Option) { 12 | o["meepo"] = meepo 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pkg/util/base36/base36.go: -------------------------------------------------------------------------------- 1 | package base36 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/martinlindhe/base36" 7 | ) 8 | 9 | func Encode(b []byte) string { 10 | return strings.ToLower(base36.EncodeBytes(b)) 11 | } 12 | 13 | func Decode(b string) []byte { 14 | return base36.DecodeToBytes(strings.ToUpper(b)) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/teleportation.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var ( 6 | teleportationCmd = &cobra.Command{ 7 | Use: "teleportation", 8 | Aliases: []string{"tp"}, 9 | Short: "Meepo teleportation subcommand", 10 | } 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(teleportationCmd) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/signaling/error.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import "fmt" 4 | 5 | var ( 6 | WireTimeoutError = fmt.Errorf("Wire timeout") 7 | NextHopUnreachableError = fmt.Errorf("Next hop unreachable") 8 | ) 9 | 10 | func UnsupportedSignalingEngine(name string) error { 11 | return fmt.Errorf("Unsupported signaling engine: %s", name) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/sdk/http/ping.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | ) 8 | 9 | func (t *MeepoSDK) Ping(peerID string) error { 10 | req := &http_api.PingRequest{ 11 | PeerID: peerID, 12 | } 13 | 14 | return t.doRequest("/v1/actions/ping", req, nil, http.StatusNoContent) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/api/http/shutdown.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func (s *HttpServer) Shutdown(c *gin.Context) { 12 | go func() { 13 | time.Sleep(1 * time.Second) 14 | s.Stop(context.TODO()) 15 | }() 16 | c.Writer.WriteHeader(http.StatusNoContent) 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | release/ 17 | dist/ -------------------------------------------------------------------------------- /pkg/api/http/whoami.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type WhoamiResponse struct { 10 | ID string `json:"id"` 11 | } 12 | 13 | func (s *HttpServer) Whaomi(c *gin.Context) { 14 | res := &WhoamiResponse{ 15 | ID: s.meepo.GetID(), 16 | } 17 | 18 | c.JSON(http.StatusOK, res) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/ofn/ofn.go: -------------------------------------------------------------------------------- 1 | package ofn 2 | 3 | import "github.com/stretchr/objx" 4 | 5 | type Option = objx.Map 6 | 7 | type OFN = func(objx.Map) 8 | 9 | func NewOption(ms ...map[string]interface{}) Option { 10 | var m map[string]interface{} 11 | if len(ms) > 0 { 12 | m = ms[0] 13 | } else { 14 | m = map[string]interface{}{} 15 | } 16 | return objx.New(m) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/sdk/error.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "fmt" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | ) 8 | 9 | var ExtractError = http_api.ExtractError 10 | var UnimplementedError = fmt.Errorf("Unimplemented") 11 | 12 | func UnsupportedMeepoSDKDriverError(name string) error { 13 | return fmt.Errorf("Unsupported MeepoSDK driver: %s", name) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/transport/webrtc/error.go: -------------------------------------------------------------------------------- 1 | package webrtc_transport 2 | 3 | import "fmt" 4 | 5 | var ( 6 | GatherTimeoutError = fmt.Errorf("gather timeout") 7 | WaitDataChannelOpenedTimeoutError = fmt.Errorf("wait data channel opened timeout") 8 | ) 9 | 10 | func UnsupportedRoleError(name string) error { 11 | return fmt.Errorf("Unsupported role %s", name) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/sdk/http/close_transport.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | ) 8 | 9 | func (t *MeepoSDK) CloseTransport(peerID string) error { 10 | req := &http_api.CloseTransportRequest{ 11 | PeerID: peerID, 12 | } 13 | 14 | return t.doRequest("/v1/actions/close_transport", req, nil, http.StatusNoContent) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/signaling/redis/error.go: -------------------------------------------------------------------------------- 1 | package redis_signaling 2 | 3 | import "fmt" 4 | 5 | var ( 6 | NotAvailableRedisClientError = fmt.Errorf("Not available redis client") 7 | SessionChannelClosedError = fmt.Errorf("Session channel closed") 8 | SessionChannelNotExistError = fmt.Errorf("Session channel not exist") 9 | SessionChannelExistError = fmt.Errorf("Session channel exist") 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/api/http/option.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/api" 5 | "github.com/PeerXu/meepo/pkg/ofn" 6 | ) 7 | 8 | func WithHost(host string) api.NewServerOption { 9 | return func(o ofn.Option) { 10 | o["host"] = host 11 | } 12 | } 13 | 14 | func WithPort(port int32) api.NewServerOption { 15 | return func(o ofn.Option) { 16 | o["port"] = port 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/sdk/http/whoami.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | ) 8 | 9 | func (t *MeepoSDK) Whoami() (string, error) { 10 | var res http_api.WhoamiResponse 11 | var err error 12 | 13 | if err = t.doRequest("/v1/actions/whoami", nil, &res, http.StatusOK); err != nil { 14 | return "", err 15 | } 16 | 17 | return res.ID, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/sdk/http/version.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | "github.com/PeerXu/meepo/pkg/sdk" 8 | ) 9 | 10 | func (t *MeepoSDK) Version() (*sdk.Version, error) { 11 | var res http_api.VersionResponse 12 | 13 | if err := t.doRequest("/v1/actions/version", nil, &res, http.StatusOK); err != nil { 14 | return nil, err 15 | } 16 | 17 | return &res, nil 18 | } 19 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/PeerXu/meepo/cmd/config" 7 | ) 8 | 9 | var ( 10 | configCmd = &cobra.Command{ 11 | Use: "config", 12 | Short: "Meepo config subcommand", 13 | } 14 | ) 15 | 16 | func init() { 17 | rootCmd.AddCommand(configCmd) 18 | 19 | configCmd.PersistentFlags().StringP("config", "c", config.GetDefaultConfigPath(), "Location of meepo config file") 20 | } 21 | -------------------------------------------------------------------------------- /pkg/signaling/option.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/PeerXu/meepo/pkg/ofn" 7 | ) 8 | 9 | type NewEngineOption = ofn.OFN 10 | 11 | func WithID(id string) NewEngineOption { 12 | return func(o ofn.Option) { 13 | o["id"] = id 14 | } 15 | } 16 | 17 | func WithLogger(logger logrus.FieldLogger) NewEngineOption { 18 | return func(o ofn.Option) { 19 | o["logger"] = logger 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var ( 8 | rootCmd = &cobra.Command{ 9 | Use: "meepo", 10 | SilenceUsage: true, 11 | } 12 | ) 13 | 14 | func Execute() { 15 | rootCmd.Execute() 16 | } 17 | 18 | func init() { 19 | rootCmd.PersistentFlags().String("log-level", "info", "Logging level") 20 | rootCmd.PersistentFlags().StringP("host", "H", "http://127.0.0.1:12345", "Daemon API base url") 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/http/error.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | func ParseError(err error) map[string]interface{} { 10 | return map[string]interface{}{ 11 | "error": err.Error(), 12 | } 13 | } 14 | 15 | func ExtractError(buf []byte) error { 16 | var e struct { 17 | Error string `json:"error"` 18 | } 19 | 20 | json.NewDecoder(bytes.NewReader(buf)).Decode(&e) 21 | 22 | return fmt.Errorf(e.Error) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/sdk/http/close_teleportation.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | ) 8 | 9 | func (t *MeepoSDK) CloseTeleportation(name string) error { 10 | req := &http_api.CloseTeleportationRequest{ 11 | Name: name, 12 | } 13 | 14 | if err := t.doRequest("/v1/actions/close_teleportation", req, nil, http.StatusNoContent); err != nil { 15 | return err 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/sdk/http/list_transports.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | "github.com/PeerXu/meepo/pkg/sdk" 8 | ) 9 | 10 | func (t *MeepoSDK) ListTransports() ([]*sdk.Transport, error) { 11 | var res http_api.ListTransportsResponse 12 | 13 | if err := t.doRequest("/v1/actions/list_transports", nil, &res, http.StatusOK); err != nil { 14 | return nil, err 15 | } 16 | 17 | return res.Transports, nil 18 | } 19 | -------------------------------------------------------------------------------- /cmd/config/error.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type UnsupportedError struct { 6 | Namespace string 7 | Name string 8 | } 9 | 10 | func (e UnsupportedError) Error() string { 11 | return fmt.Sprintf("Unsupported namespace: %v, name: %v", e.Namespace, e.Name) 12 | } 13 | 14 | type UnsupportedConfigKeyError struct { 15 | Key string 16 | } 17 | 18 | func (e UnsupportedConfigKeyError) Error() string { 19 | return fmt.Sprintf("Unsupported config key: %v", e.Key) 20 | } 21 | -------------------------------------------------------------------------------- /hack/summon/launch.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ "x${MEEPO_CLUSTER_SIZE}" == "x" ]; then 4 | echo "require MEEPO_CLUSTER_SIZE" 5 | exit 1 6 | fi 7 | 8 | tmux new -d -s meepo 9 | for idx in $(seq $((MEEPO_CLUSTER_SIZE-1))); do 10 | tmux splitw -t 0 11 | tmux selectl -t 0 tiled 12 | done 13 | 14 | for idx in $(seq 0 $((MEEPO_CLUSTER_SIZE-1))); do 15 | tmux selectp -t ${idx} 16 | tmux send -t meepo "./start.sh meepo_${idx}" C-j 17 | done 18 | 19 | tmux attach -t meepo 20 | -------------------------------------------------------------------------------- /pkg/meepo/list_transports.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/transport" 5 | ) 6 | 7 | func (mp *Meepo) ListTransports() ([]transport.Transport, error) { 8 | logger := mp.getLogger().WithField("#method", "ListTransports") 9 | 10 | tps, err := mp.listTransports() 11 | if err != nil { 12 | logger.WithError(err).Errorf("failed to list transports") 13 | return nil, err 14 | } 15 | 16 | logger.Debugf("list transports") 17 | 18 | return tps, nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/sdk/http/list_teleportations.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | "github.com/PeerXu/meepo/pkg/sdk" 8 | ) 9 | 10 | func (t *MeepoSDK) ListTeleportations() ([]*sdk.Teleportation, error) { 11 | var res http_api.ListTeleportationsResponse 12 | 13 | if err := t.doRequest("/v1/actions/list_teleportations", nil, &res, http.StatusOK); err != nil { 14 | return nil, err 15 | } 16 | 17 | return res.Teleportations, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/meepo/list_teleportations.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import "github.com/PeerXu/meepo/pkg/teleportation" 4 | 5 | func (mp *Meepo) ListTeleportations() ([]teleportation.Teleportation, error) { 6 | logger := mp.getLogger().WithField("#method", "ListTeleportations") 7 | 8 | tss, err := mp.listTeleportations() 9 | if err != nil { 10 | logger.WithError(err).Errorf("failed to list teleportations") 11 | return nil, err 12 | } 13 | 14 | logger.Debugf("list teleportations") 15 | 16 | return tss, nil 17 | } 18 | -------------------------------------------------------------------------------- /cmd/config/signaling_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type SignalingConfig struct { 4 | Name string `yaml:"name"` 5 | } 6 | 7 | type RedisSignalingConfig struct { 8 | Name string `yaml:"name"` 9 | URL string `yaml:"url"` 10 | } 11 | 12 | func init() { 13 | RegisterUnmarshalConfigFunc("meepo.signaling", "redis", func(u func(interface{}) error) (interface{}, error) { 14 | var t struct{ Signaling *RedisSignalingConfig } 15 | if err := u(&t); err != nil { 16 | return nil, err 17 | } 18 | return t.Signaling, nil 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/config/api_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ApiConfig struct { 4 | Name string `yaml:"name"` 5 | } 6 | 7 | type HttpApiConfig struct { 8 | Name string `yaml:"name"` 9 | Host string `yaml:"host"` 10 | Port int32 `yaml:"port"` 11 | } 12 | 13 | func init() { 14 | RegisterUnmarshalConfigFunc("meepo.api", "http", func(u func(interface{}) error) (interface{}, error) { 15 | var t struct { 16 | Api *HttpApiConfig 17 | } 18 | 19 | if err := u(&t); err != nil { 20 | return nil, err 21 | } 22 | 23 | return t.Api, nil 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/api/http/version.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | encoding_api "github.com/PeerXu/meepo/pkg/api/encoding" 9 | ) 10 | 11 | type VersionResponse = encoding_api.Version 12 | 13 | func (s *HttpServer) Version(c *gin.Context) { 14 | v := s.meepo.Version() 15 | 16 | res := &VersionResponse{ 17 | Version: v.Version, 18 | GoVersion: v.GoVersion, 19 | GitHash: v.GitHash, 20 | Built: v.Built, 21 | Platform: v.Platform, 22 | } 23 | 24 | c.JSON(http.StatusOK, res) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/sdk/http/new_transport.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_api "github.com/PeerXu/meepo/pkg/api/http" 7 | "github.com/PeerXu/meepo/pkg/sdk" 8 | ) 9 | 10 | func (t *MeepoSDK) NewTransport(peerID string) (*sdk.Transport, error) { 11 | var res http_api.NewTransportResponse 12 | req := &http_api.NewTransportRequest{ 13 | PeerID: peerID, 14 | } 15 | 16 | if err := t.doRequest("/v1/actions/new_transport", req, &res, http.StatusCreated); err != nil { 17 | return nil, err 18 | } 19 | 20 | return res.Transport, nil 21 | } 22 | -------------------------------------------------------------------------------- /cmd/config/transport_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type TransportConfig struct { 4 | Name string `yaml:"name"` 5 | } 6 | 7 | type WebrtcTransportConfig struct { 8 | Name string `yaml:"name"` 9 | ICEServers []string `yaml:"iceServers"` 10 | } 11 | 12 | func init() { 13 | RegisterUnmarshalConfigFunc("meepo.transport", "webrtc", func(u func(interface{}) error) (interface{}, error) { 14 | var t struct{ Transport *WebrtcTransportConfig } 15 | if err := u(&t); err != nil { 16 | return nil, err 17 | } 18 | return t.Transport, nil 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/util/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | Version string 10 | GoVersion string 11 | GitHash string 12 | Built string 13 | ) 14 | 15 | type V struct { 16 | Version string 17 | GoVersion string 18 | GitHash string 19 | Built string 20 | Platform string 21 | } 22 | 23 | func Get() *V { 24 | return &V{ 25 | Version: Version, 26 | GoVersion: GoVersion, 27 | GitHash: GitHash, 28 | Built: Built, 29 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hack/summon/summon.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | export MEEPO=${MEEPO:-"/bin/meepo"} 4 | 5 | if [ ! -f "/root/config.template.yaml" ]; then 6 | echo "require config.template.yaml" 7 | exit 1 8 | fi 9 | 10 | mkdir -p /etc/meepo 11 | cp /root/config.template.yaml /etc/meepo/meepo.yaml 12 | 13 | if [ "x${MEEPO_SIGNALING_URL}" != "x" ]; then 14 | ${MEEPO} config set signaling.url="${MEEPO_SIGNALING_URL}" 15 | fi 16 | if [ "x${MEEPO_AS_SIGNALING}" != "x" ]; then 17 | ${MEEPO} config set asSignaling="${MEEPO_AS_SIGNALING}" 18 | fi 19 | 20 | ${MEEPO} serve --daemon=false --log-level=trace 21 | -------------------------------------------------------------------------------- /cmd/whoami.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | whoamiCmd = &cobra.Command{ 11 | Use: "whoami", 12 | Short: "Get Meepo ID", 13 | RunE: meepoWhoami, 14 | } 15 | ) 16 | 17 | func meepoWhoami(cmd *cobra.Command, args []string) error { 18 | var err error 19 | 20 | sdk, err := NewHTTPSDK(cmd) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | id, err := sdk.Whoami() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | fmt.Println(id) 31 | 32 | return nil 33 | } 34 | 35 | func init() { 36 | rootCmd.AddCommand(whoamiCmd) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/api/encoding/datachannel.go: -------------------------------------------------------------------------------- 1 | package encoding_api 2 | 3 | import "github.com/PeerXu/meepo/pkg/transport" 4 | 5 | type DataChannel struct { 6 | Label string `json:"label"` 7 | State string `json:"state"` 8 | } 9 | 10 | func ConvertDataChannel(x transport.DataChannel) *DataChannel { 11 | y := &DataChannel{ 12 | Label: x.Label(), 13 | State: x.State().String(), 14 | } 15 | 16 | return y 17 | } 18 | 19 | func ConvertDataChannels(xs []transport.DataChannel) []*DataChannel { 20 | var ys []*DataChannel 21 | 22 | for _, x := range xs { 23 | ys = append(ys, ConvertDataChannel(x)) 24 | } 25 | 26 | return ys 27 | } 28 | -------------------------------------------------------------------------------- /pkg/teleportation/teleportation.go: -------------------------------------------------------------------------------- 1 | package teleportation 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/PeerXu/meepo/pkg/transport" 7 | ) 8 | 9 | type Portal int 10 | 11 | const ( 12 | PortalSource Portal = iota + 1 13 | PortalSink 14 | ) 15 | 16 | var ( 17 | PortalStr = []string{ 18 | PortalSource: "source", 19 | PortalSink: "sink", 20 | } 21 | ) 22 | 23 | func (t Portal) String() string { 24 | return PortalStr[t] 25 | } 26 | 27 | type Teleportation interface { 28 | Name() string 29 | Source() net.Addr 30 | Sink() net.Addr 31 | Portal() Portal 32 | Transport() transport.Transport 33 | DataChannels() []transport.DataChannel 34 | Close() error 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/http/list_transports.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | encoding_api "github.com/PeerXu/meepo/pkg/api/encoding" 9 | ) 10 | 11 | type ListTransportsResponse struct { 12 | Transports []*encoding_api.Transport `json:"transports"` 13 | } 14 | 15 | func (s *HttpServer) ListTransports(c *gin.Context) { 16 | transports, err := s.meepo.ListTransports() 17 | if err != nil { 18 | c.JSON(http.StatusInternalServerError, ParseError(err)) 19 | return 20 | } 21 | 22 | res := ListTransportsResponse{ 23 | Transports: encoding_api.ConvertTransports(transports), 24 | } 25 | 26 | c.JSON(http.StatusOK, &res) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | type Server interface { 10 | Start(context.Context) error 11 | Stop(context.Context) error 12 | Wait() error 13 | } 14 | 15 | type NewServerFunc func(...NewServerOption) (Server, error) 16 | 17 | var newServerFuncs sync.Map 18 | 19 | func RegisterNewServerFunc(name string, fn NewServerFunc) { 20 | newServerFuncs.Store(name, fn) 21 | } 22 | 23 | func NewServer(name string, opts ...NewServerOption) (Server, error) { 24 | fn, ok := newServerFuncs.Load(name) 25 | if !ok { 26 | return nil, fmt.Errorf("Unsupported server: %s", name) 27 | } 28 | 29 | return fn.(NewServerFunc)(opts...) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/api/http/close_transport.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type CloseTransportRequest struct { 10 | PeerID string `json:"peerID" binding:"required"` 11 | } 12 | 13 | func (s *HttpServer) CloseTransport(c *gin.Context) { 14 | var err error 15 | var req CloseTransportRequest 16 | 17 | if err = c.ShouldBindJSON(&req); err != nil { 18 | c.JSON(http.StatusBadRequest, ParseError(err)) 19 | return 20 | } 21 | 22 | if err = s.meepo.CloseTransport(req.PeerID); err != nil { 23 | c.JSON(http.StatusInternalServerError, ParseError(err)) 24 | return 25 | } 26 | 27 | c.Writer.WriteHeader(http.StatusNoContent) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/signaling/chain/option.go: -------------------------------------------------------------------------------- 1 | package chain_signaling 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/ofn" 5 | "github.com/PeerXu/meepo/pkg/signaling" 6 | ) 7 | 8 | func DefaultEngineOption() ofn.Option { 9 | return ofn.NewOption(map[string]interface{}{}) 10 | } 11 | 12 | func WithEngine(engines ...signaling.Engine) signaling.NewEngineOption { 13 | return func(o ofn.Option) { 14 | var enginesSlice []signaling.Engine 15 | var ok bool 16 | 17 | enginesSlice, ok = o.Get("engines").Inter().([]signaling.Engine) 18 | if ok { 19 | enginesSlice = append(enginesSlice, engines...) 20 | } else { 21 | enginesSlice = engines 22 | } 23 | 24 | o["engines"] = enginesSlice 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/api/http/close_teleportation.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type CloseTeleportationRequest struct { 10 | Name string `json:"name" binding:"required"` 11 | } 12 | 13 | func (s *HttpServer) CloseTeleportation(c *gin.Context) { 14 | var err error 15 | var req CloseTeleportationRequest 16 | 17 | if err = c.ShouldBindJSON(&req); err != nil { 18 | c.JSON(http.StatusBadRequest, ParseError(err)) 19 | return 20 | } 21 | 22 | if err = s.meepo.CloseTeleportation(req.Name); err != nil { 23 | c.JSON(http.StatusInternalServerError, ParseError(err)) 24 | return 25 | } 26 | 27 | c.Writer.WriteHeader(http.StatusNoContent) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/api/http/ping.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type PingRequest struct { 10 | PeerID string `json:"peerID" binding:"required"` 11 | Payload string `json:"payload,omitempty"` 12 | } 13 | 14 | func (s *HttpServer) Ping(c *gin.Context) { 15 | var err error 16 | var req PingRequest 17 | 18 | if err = c.ShouldBindJSON(&req); err != nil { 19 | c.JSON(http.StatusBadRequest, ParseError(err)) 20 | return 21 | } 22 | 23 | if err = s.meepo.Ping(req.PeerID, req.Payload); err != nil { 24 | c.JSON(http.StatusInternalServerError, ParseError(err)) 25 | return 26 | } 27 | 28 | c.Writer.WriteHeader(http.StatusNoContent) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/shutdown.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | shutdownCmd = &cobra.Command{ 11 | Use: "shutdown", 12 | Short: "Shutdown Meepo", 13 | Example: "meepo shutdown", 14 | Aliases: []string{"deny"}, 15 | RunE: meepoShutdown, 16 | } 17 | ) 18 | 19 | func meepoShutdown(cmd *cobra.Command, args []string) error { 20 | var err error 21 | 22 | sdk, err := NewHTTPSDK(cmd) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if err = sdk.Shutdown(); err != nil { 28 | return err 29 | } 30 | 31 | fmt.Println("Meepo shutting down") 32 | 33 | return nil 34 | } 35 | 36 | func init() { 37 | rootCmd.AddCommand(shutdownCmd) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/api/http/list_teleportations.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | encoding_api "github.com/PeerXu/meepo/pkg/api/encoding" 9 | ) 10 | 11 | type ListTeleportationsResponse struct { 12 | Teleportations []*encoding_api.Teleportation `json:"teleportations"` 13 | } 14 | 15 | func (s *HttpServer) ListTeleportations(c *gin.Context) { 16 | teleportations, err := s.meepo.ListTeleportations() 17 | if err != nil { 18 | c.JSON(http.StatusInternalServerError, ParseError(err)) 19 | return 20 | } 21 | 22 | res := ListTeleportationsResponse{ 23 | Teleportations: encoding_api.ConvertTeleportations(teleportations), 24 | } 25 | 26 | c.JSON(http.StatusOK, &res) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/util/msgpack/msgpack.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | ) 8 | 9 | type Marshaler = msgpack.Marshaler 10 | type Unmarshaler = msgpack.Unmarshaler 11 | 12 | func Marshal(v interface{}) ([]byte, error) { 13 | enc := msgpack.GetEncoder() 14 | 15 | var buf bytes.Buffer 16 | enc.Reset(&buf) 17 | enc.SetSortMapKeys(true) 18 | enc.UseCompactInts(true) 19 | enc.UseCompactFloats(true) 20 | 21 | err := enc.Encode(v) 22 | b := buf.Bytes() 23 | 24 | msgpack.PutEncoder(enc) 25 | 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return b, nil 31 | } 32 | 33 | func Unmarshal(b []byte, v interface{}) error { 34 | return msgpack.Unmarshal(b, v) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/ping.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | pingCmd = &cobra.Command{ 11 | Use: "ping ", 12 | Short: "Send ping request to peer meepo", 13 | RunE: meepoPing, 14 | Args: cobra.ExactArgs(1), 15 | } 16 | ) 17 | 18 | func meepoPing(cmd *cobra.Command, args []string) error { 19 | var err error 20 | 21 | if len(args) == 0 { 22 | return fmt.Errorf("Require id") 23 | } 24 | id := args[0] 25 | 26 | sdk, err := NewHTTPSDK(cmd) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if err = sdk.Ping(id); err != nil { 32 | return err 33 | } 34 | 35 | fmt.Println("Pong") 36 | 37 | return nil 38 | } 39 | 40 | func init() { 41 | rootCmd.AddCommand(pingCmd) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/transport_new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | transportNewCmd = &cobra.Command{ 11 | Use: "new ", 12 | Short: "New transport", 13 | Aliases: []string{"n"}, 14 | RunE: meepoTransportNew, 15 | Args: cobra.ExactArgs(1), 16 | } 17 | ) 18 | 19 | func meepoTransportNew(cmd *cobra.Command, args []string) error { 20 | var err error 21 | 22 | id := args[0] 23 | 24 | sdk, err := NewHTTPSDK(cmd) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | _, err = sdk.NewTransport(id) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | fmt.Println("Transport creating") 35 | 36 | return nil 37 | } 38 | 39 | func init() { 40 | transportCmd.AddCommand(transportNewCmd) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/transport_close.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | transportCloseCmd = &cobra.Command{ 11 | Use: "close ", 12 | Short: "Close transport", 13 | Aliases: []string{"c"}, 14 | RunE: meepoTransportClose, 15 | Args: cobra.ExactArgs(1), 16 | } 17 | ) 18 | 19 | func meepoTransportClose(cmd *cobra.Command, args []string) error { 20 | var err error 21 | 22 | id := args[0] 23 | 24 | sdk, err := NewHTTPSDK(cmd) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if err = sdk.CloseTransport(id); err != nil { 30 | return err 31 | } 32 | 33 | fmt.Println("Transport closing") 34 | 35 | return nil 36 | } 37 | 38 | func init() { 39 | transportCmd.AddCommand(transportCloseCmd) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/config_get.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/PeerXu/meepo/cmd/config" 9 | ) 10 | 11 | var ( 12 | configGetCmd = &cobra.Command{ 13 | Use: "get ", 14 | Short: "Get Meepo config setting", 15 | RunE: meepoConfigGet, 16 | Args: cobra.ExactArgs(1), 17 | } 18 | ) 19 | 20 | func meepoConfigGet(cmd *cobra.Command, args []string) error { 21 | fs := cmd.Flags() 22 | cp, _ := fs.GetString("config") 23 | key := args[0] 24 | 25 | cfg, _, err := config.Load(cp) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | val, err := cfg.Get(key) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | fmt.Print(val) 36 | 37 | return nil 38 | } 39 | 40 | func init() { 41 | configCmd.AddCommand(configGetCmd) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/transport/datachannel.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type DataChannelState int 8 | 9 | const ( 10 | DataChannelStateConnecting DataChannelState = iota + 1 11 | DataChannelStateOpen 12 | DataChannelStateClosing 13 | DataChannelStateClosed 14 | ) 15 | 16 | var ( 17 | DataChannelStateStr = []string{ 18 | DataChannelStateConnecting: "connecting", 19 | DataChannelStateOpen: "open", 20 | DataChannelStateClosing: "closing", 21 | DataChannelStateClosed: "closed", 22 | } 23 | ) 24 | 25 | func (t DataChannelState) String() string { 26 | return DataChannelStateStr[t] 27 | } 28 | 29 | type DataChannel interface { 30 | Transport() Transport 31 | Label() string 32 | State() DataChannelState 33 | OnOpen(func()) 34 | io.ReadWriteCloser 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/encoding/transport.go: -------------------------------------------------------------------------------- 1 | package encoding_api 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/transport" 5 | ) 6 | 7 | type Transport struct { 8 | PeerID string `json:"peerID"` 9 | State string `json:"state"` 10 | DataChannels []*DataChannel `json:"dataChannels,omitempty"` 11 | } 12 | 13 | func ConvertTransport(x transport.Transport) *Transport { 14 | dcs, _ := x.DataChannels() 15 | 16 | y := &Transport{ 17 | PeerID: x.PeerID(), 18 | State: x.TransportState().String(), 19 | DataChannels: ConvertDataChannels(dcs), 20 | } 21 | 22 | return y 23 | } 24 | 25 | func ConvertTransports(xs []transport.Transport) []*Transport { 26 | var ys []*Transport 27 | 28 | for _, x := range xs { 29 | ys = append(ys, ConvertTransport(x)) 30 | } 31 | 32 | return ys 33 | } 34 | -------------------------------------------------------------------------------- /pkg/transport/option.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/PeerXu/meepo/pkg/ofn" 7 | ) 8 | 9 | type NewTransportOption = ofn.OFN 10 | 11 | func WithID(id string) NewTransportOption { 12 | return func(o ofn.Option) { 13 | o["id"] = id 14 | } 15 | } 16 | 17 | func WithPeerID(id string) NewTransportOption { 18 | return func(o ofn.Option) { 19 | o["peerID"] = id 20 | } 21 | } 22 | 23 | func WithLogger(logger logrus.FieldLogger) NewTransportOption { 24 | return func(o ofn.Option) { 25 | o["logger"] = logger 26 | } 27 | } 28 | 29 | type OnDataChannelCreateHandler func(DataChannel) 30 | 31 | type CreateDataChannelOption = ofn.OFN 32 | 33 | func WithOrdered(ordered bool) CreateDataChannelOption { 34 | return func(o ofn.Option) { 35 | o["ordered"] = ordered 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/config/auth_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type AuthConfig struct { 4 | Name string `yaml:"name"` 5 | } 6 | 7 | type DummyAuthConfig struct { 8 | Name string `yaml:"name"` 9 | } 10 | 11 | type SecretAuthConfig struct { 12 | Name string `yaml:"name"` 13 | Secret string `yaml:"secret"` 14 | } 15 | 16 | func init() { 17 | RegisterUnmarshalConfigFunc("meepo.auth", "dummy", func(u func(interface{}) error) (interface{}, error) { 18 | var t struct{ Auth *DummyAuthConfig } 19 | if err := u(&t); err != nil { 20 | return nil, err 21 | } 22 | return t.Auth, nil 23 | }) 24 | RegisterUnmarshalConfigFunc("meepo.auth", "secret", func(u func(interface{}) error) (interface{}, error) { 25 | var t struct{ Auth *SecretAuthConfig } 26 | if err := u(&t); err != nil { 27 | return nil, err 28 | } 29 | return t.Auth, nil 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/transport/webrtc/util.go: -------------------------------------------------------------------------------- 1 | package webrtc_transport 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/pion/webrtc/v3" 9 | ) 10 | 11 | func unmarshalICEServer(x string) webrtc.ICEServer { 12 | var y webrtc.ICEServer 13 | 14 | if !strings.Contains(x, "://") { 15 | x = strings.Replace(x, ":", "://", 1) 16 | } 17 | 18 | u, _ := url.Parse(x) 19 | y.URLs = append(y.URLs, fmt.Sprintf("%s:%s", u.Scheme, u.Host)) 20 | if u.User != nil { 21 | y.CredentialType = webrtc.ICECredentialTypePassword 22 | y.Username = u.User.Username() 23 | y.Credential, _ = u.User.Password() 24 | } 25 | 26 | return y 27 | } 28 | 29 | func unmarshalICEServers(xs []string) []webrtc.ICEServer { 30 | var ys []webrtc.ICEServer 31 | 32 | for _, x := range xs { 33 | ys = append(ys, unmarshalICEServer(x)) 34 | } 35 | 36 | return ys 37 | } 38 | -------------------------------------------------------------------------------- /pkg/api/http/router.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func (s *HttpServer) getRouter() *gin.Engine { 8 | router := gin.Default() 9 | 10 | router.GET("/healthz", s.Healthz) 11 | 12 | v1Action := router.Group("/v1/actions") 13 | v1Action.POST("/version", s.Version) 14 | v1Action.POST("/ping", s.Ping) 15 | v1Action.POST("/whoami", s.Whaomi) 16 | v1Action.POST("/shutdown", s.Shutdown) 17 | v1Action.POST("/teleport", s.Teleport) 18 | 19 | v1Action.POST("/new_transport", s.NewTransport) 20 | v1Action.POST("/close_transport", s.CloseTransport) 21 | v1Action.POST("/list_transports", s.ListTransports) 22 | 23 | v1Action.POST("/new_teleportation", s.NewTeleportation) 24 | v1Action.POST("/close_teleportation", s.CloseTeleportation) 25 | v1Action.POST("/list_teleportations", s.ListTeleportations) 26 | 27 | return router 28 | } 29 | -------------------------------------------------------------------------------- /pkg/signaling/signaling.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type UserData = map[string]interface{} 8 | 9 | type Descriptor struct { 10 | ID string 11 | UserData UserData 12 | } 13 | 14 | type WireHandler func(*Descriptor) (*Descriptor, error) 15 | 16 | type Engine interface { 17 | Wire(dst, src *Descriptor) (*Descriptor, error) 18 | OnWire(handler WireHandler) 19 | Close() error 20 | } 21 | 22 | type NewEngineFunc func(...NewEngineOption) (Engine, error) 23 | 24 | var newEngineFuncs sync.Map 25 | 26 | func RegisterNewEngineFunc(name string, fn NewEngineFunc) { 27 | newEngineFuncs.Store(name, fn) 28 | } 29 | 30 | func NewEngine(name string, opts ...NewEngineOption) (Engine, error) { 31 | fn, ok := newEngineFuncs.Load(name) 32 | if !ok { 33 | return nil, UnsupportedSignalingEngine(name) 34 | } 35 | 36 | return fn.(NewEngineFunc)(opts...) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/teleportation_close.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | teleportationCloseCmd = &cobra.Command{ 11 | Use: "close ", 12 | Short: "Close teleportation", 13 | Aliases: []string{"c"}, 14 | RunE: meepoTeleportationClose, 15 | Args: cobra.ExactArgs(1), 16 | } 17 | ) 18 | 19 | func meepoTeleportationClose(cmd *cobra.Command, args []string) error { 20 | var err error 21 | 22 | name := args[0] 23 | 24 | sdk, err := NewHTTPSDK(cmd) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if err = sdk.CloseTeleportation(name); err != nil { 30 | return err 31 | } 32 | 33 | fmt.Printf("Teleportation is closing\n") 34 | 35 | return nil 36 | } 37 | 38 | func init() { 39 | teleportationCmd.AddCommand(teleportationCloseCmd) 40 | 41 | teleportationCloseCmd.PersistentFlags().String("name", "", "Teleportation name") 42 | } 43 | -------------------------------------------------------------------------------- /hack/summon/start.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | DOCKER=${DOCKER:-"$(which docker)"} 4 | 5 | MEEPO=${MEEPO:-"$(which meepo)"} 6 | MEEPO_CONFIG=${MEEPO_CONFIG:-"$(readlink -f ~/.meepo/config.yaml)"} 7 | CONTAINER_NAME=$1 8 | 9 | DOCKER_RUN_OPTS="" 10 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS --rm" 11 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS -it" 12 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS --name ${CONTAINER_NAME}" 13 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS --entrypoint /root/summon.sh" 14 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS -v ${MEEPO}:/bin/meepo" 15 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS -v ${MEEPO_CONFIG}:/root/config.template.yaml" 16 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS -v `pwd`/summon.sh:/root/summon.sh" 17 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS -e MEEPO_AS_SIGNALING=true" 18 | if [ "x${MEEPO_SIGNALING_URL}" != "x" ]; then 19 | DOCKER_RUN_OPTS="$DOCKER_RUN_OPTS -e MEEPO_SIGNALING_URL=${MEEPO_SIGNALING_URL}" 20 | fi 21 | 22 | sudo ${DOCKER} run \ 23 | ${DOCKER_RUN_OPTS} \ 24 | alpine 25 | -------------------------------------------------------------------------------- /pkg/api/http/new_transport.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | encoding_api "github.com/PeerXu/meepo/pkg/api/encoding" 9 | "github.com/PeerXu/meepo/pkg/transport" 10 | ) 11 | 12 | type NewTransportRequest struct { 13 | PeerID string `json:"peerID" binding:"required"` 14 | } 15 | 16 | type NewTransportResponse struct { 17 | Transport *encoding_api.Transport `json:"transport"` 18 | } 19 | 20 | func (s *HttpServer) NewTransport(c *gin.Context) { 21 | var err error 22 | var req NewTransportRequest 23 | var tp transport.Transport 24 | 25 | if err = c.ShouldBindJSON(&req); err != nil { 26 | c.JSON(http.StatusBadRequest, ParseError(err)) 27 | return 28 | } 29 | 30 | if tp, err = s.meepo.NewTransport(req.PeerID); err != nil { 31 | c.JSON(http.StatusInternalServerError, ParseError(err)) 32 | return 33 | } 34 | 35 | c.JSON(http.StatusCreated, encoding_api.ConvertTransport(tp)) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/util/crypt/curve25519.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/teserakt-io/golang-ed25519/extra25519" 7 | "golang.org/x/crypto/curve25519" 8 | ) 9 | 10 | func ed25519PublicKeyToCurve25519(pubk ed25519.PublicKey) [32]byte { 11 | var curve [32]byte 12 | var pubk32 [32]byte 13 | copy(pubk32[:], pubk[:32]) 14 | extra25519.PublicKeyToCurve25519(&curve, &pubk32) 15 | return curve 16 | } 17 | 18 | func ed25519PrivateKeyToCurve25519(prik ed25519.PrivateKey) [32]byte { 19 | var curve [32]byte 20 | var prik64 [64]byte 21 | copy(prik64[:], prik[:64]) 22 | extra25519.PrivateKeyToCurve25519(&curve, &prik64) 23 | return curve 24 | } 25 | 26 | func CalcSharedSecret(pubk ed25519.PublicKey, prik ed25519.PrivateKey) [32]byte { 27 | var secret [32]byte 28 | curvePubk := ed25519PublicKeyToCurve25519(pubk) 29 | curvePrik := ed25519PrivateKeyToCurve25519(prik) 30 | curve25519.ScalarMult(&secret, &curvePrik, &curvePubk) 31 | return secret 32 | } 33 | -------------------------------------------------------------------------------- /cmd/transport_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/olekukonko/tablewriter" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | transportListCmd = &cobra.Command{ 12 | Use: "list", 13 | Short: "List transports", 14 | Aliases: []string{"ls", "l"}, 15 | RunE: meepoTransportList, 16 | } 17 | ) 18 | 19 | func meepoTransportList(cmd *cobra.Command, args []string) error { 20 | var err error 21 | 22 | sdk, err := NewHTTPSDK(cmd) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | tps, err := sdk.ListTransports() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | table := tablewriter.NewWriter(os.Stdout) 33 | table.SetHeader([]string{ 34 | "Peer", 35 | "State", 36 | }) 37 | 38 | for _, tp := range tps { 39 | table.Append([]string{ 40 | tp.PeerID, 41 | tp.State, 42 | }) 43 | } 44 | 45 | table.Render() 46 | 47 | return nil 48 | } 49 | 50 | func init() { 51 | transportCmd.AddCommand(transportListCmd) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/util/crypt/ed25519.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "fmt" 7 | "io/ioutil" 8 | 9 | "github.com/mitchellh/go-homedir" 10 | "golang.org/x/crypto/ssh" 11 | ) 12 | 13 | func Ed25519GenerateKey() (pubk ed25519.PublicKey, prik ed25519.PrivateKey) { 14 | pubk, prik, _ = ed25519.GenerateKey(rand.Reader) 15 | return 16 | } 17 | 18 | func LoadEd25519Key(filename string) (pubk ed25519.PublicKey, prik ed25519.PrivateKey, err error) { 19 | filename, err = homedir.Expand(filename) 20 | if err != nil { 21 | return 22 | } 23 | 24 | buf, err := ioutil.ReadFile(filename) 25 | if err != nil { 26 | return 27 | } 28 | 29 | v, err := ssh.ParseRawPrivateKey(buf) 30 | if err != nil { 31 | return 32 | } 33 | 34 | switch v.(type) { 35 | case *ed25519.PrivateKey: 36 | prik = *v.(*ed25519.PrivateKey) 37 | pubk = prik.Public().(ed25519.PublicKey) 38 | return 39 | default: 40 | err = fmt.Errorf("expect openssh ed25519 private key") 41 | return 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/sdk/http/teleport.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | http_api "github.com/PeerXu/meepo/pkg/api/http" 8 | "github.com/PeerXu/meepo/pkg/sdk" 9 | ) 10 | 11 | func (t *MeepoSDK) Teleport(peerID string, remote net.Addr, opt *sdk.TeleportOption) (net.Addr, error) { 12 | req := &http_api.TeleportRequest{ 13 | ID: peerID, 14 | RemoteNetwork: remote.Network(), 15 | RemoteAddress: remote.String(), 16 | } 17 | 18 | if opt.Local != nil { 19 | req.LocalNetwork = opt.Local.Network() 20 | req.LocalAddress = opt.Local.String() 21 | } 22 | 23 | if opt.Name != "" { 24 | req.Name = opt.Name 25 | } 26 | 27 | if opt.Secret != "" { 28 | req.Secret = opt.Secret 29 | } 30 | 31 | var res http_api.TeleportResponse 32 | 33 | if err := t.doRequest("/v1/actions/teleport", req, &res, http.StatusOK); err != nil { 34 | return nil, err 35 | } 36 | 37 | local, err := net.ResolveTCPAddr(res.LocalNetwork, res.LocalAddress) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return local, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/sdk/http/new_teleportation.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | http_api "github.com/PeerXu/meepo/pkg/api/http" 8 | "github.com/PeerXu/meepo/pkg/sdk" 9 | ) 10 | 11 | func (t *MeepoSDK) NewTeleportation(peerID string, remote net.Addr, opt *sdk.NewTeleportationOption) (*sdk.Teleportation, error) { 12 | req := &http_api.NewTeleportationRequest{ 13 | PeerID: peerID, 14 | RemoteNetwork: remote.Network(), 15 | RemoteAddress: remote.String(), 16 | } 17 | if opt == nil { 18 | opt = &sdk.NewTeleportationOption{} 19 | } 20 | 21 | if opt.Name != "" { 22 | req.Name = opt.Name 23 | } 24 | 25 | if source := opt.Source; source != nil && source.String() != "" { 26 | req.LocalNetwork = source.Network() 27 | req.LocalAddress = source.String() 28 | } 29 | 30 | if opt.Secret != "" { 31 | req.Secret = opt.Secret 32 | } 33 | 34 | var res http_api.NewTeleportationResponse 35 | 36 | if err := t.doRequest("/v1/actions/new_teleportation", req, &res, http.StatusCreated); err != nil { 37 | return nil, err 38 | } 39 | 40 | return res.Teleportation, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/signaling/redis/option.go: -------------------------------------------------------------------------------- 1 | package redis_signaling 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/PeerXu/meepo/pkg/ofn" 7 | "github.com/PeerXu/meepo/pkg/signaling" 8 | ) 9 | 10 | func DefaultEngineOption() ofn.Option { 11 | return ofn.NewOption(map[string]interface{}{ 12 | "url": "redis://127.0.0.1:6379/0", 13 | "waitWiredEventTimeout": 13 * time.Second, 14 | "resolvePeriod": 61 * time.Second, 15 | "healthCheckPeriod": 57 * time.Second, 16 | }) 17 | } 18 | 19 | func WithURL(url string) signaling.NewEngineOption { 20 | return func(o ofn.Option) { 21 | o["url"] = url 22 | } 23 | } 24 | 25 | func WithWaitWiredEventTimeout(d time.Duration) signaling.NewEngineOption { 26 | return func(o ofn.Option) { 27 | o["waitWiredEventTimeout"] = d 28 | } 29 | } 30 | 31 | func WithResolvePeriod(d time.Duration) signaling.NewEngineOption { 32 | return func(o ofn.Option) { 33 | o["resolvePeriod"] = d 34 | } 35 | } 36 | 37 | func WithHealthCheckPeriod(d time.Duration) signaling.NewEngineOption { 38 | return func(o ofn.Option) { 39 | o["healthCheckPeriod"] = d 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Peer Xu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PeerXu/meepo 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/VividCortex/godaemon v1.0.0 7 | github.com/gin-gonic/gin v1.7.7 8 | github.com/go-redis/redis/v8 v8.11.4 9 | github.com/go-resty/resty/v2 v2.7.0 10 | github.com/hashicorp/golang-lru v0.5.4 11 | github.com/martinlindhe/base36 v1.1.0 12 | github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a 13 | github.com/mitchellh/go-homedir v1.1.0 14 | github.com/olekukonko/tablewriter v0.0.5 15 | github.com/pion/datachannel v1.5.2 16 | github.com/pion/webrtc/v3 v3.1.13 17 | github.com/sirupsen/logrus v1.8.1 18 | github.com/spf13/cast v1.4.1 19 | github.com/spf13/cobra v1.3.0 20 | github.com/spf13/viper v1.10.1 21 | github.com/stretchr/objx v0.3.0 22 | github.com/stretchr/testify v1.7.0 23 | github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 24 | github.com/things-go/go-socks5 v0.0.2 25 | github.com/vmihailenco/msgpack/v5 v5.3.5 26 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 27 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f 28 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 29 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 30 | ) 31 | -------------------------------------------------------------------------------- /cmd/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | "gopkg.in/yaml.v3" 8 | 9 | "github.com/PeerXu/meepo/cmd/config" 10 | ) 11 | 12 | type ConfigTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (s *ConfigTestSuite) TestEncodeDecodeConfig() { 17 | in := config.NewDefaultConfig() 18 | 19 | buf, err := yaml.Marshal(in) 20 | s.Require().Nil(err) 21 | 22 | var out config.Config 23 | err = yaml.Unmarshal(buf, &out) 24 | s.Require().Nil(err) 25 | 26 | { 27 | ai, ok := out.Meepo.AuthI.(*config.DummyAuthConfig) 28 | s.Require().True(ok) 29 | s.Equal("dummy", ai.Name) 30 | } 31 | { 32 | ti, ok := out.Meepo.TransportI.(*config.WebrtcTransportConfig) 33 | s.Require().True(ok) 34 | s.Equal("webrtc", ti.Name) 35 | } 36 | { 37 | si, ok := out.Meepo.SignalingI.(*config.RedisSignalingConfig) 38 | s.Require().True(ok) 39 | s.Equal("redis", si.Name) 40 | } 41 | { 42 | ai, ok := out.Meepo.ApiI.(*config.HttpApiConfig) 43 | s.Require().True(ok) 44 | s.Equal("http", ai.Name) 45 | } 46 | } 47 | 48 | func TestConfigTestSuite(t *testing.T) { 49 | suite.Run(t, new(ConfigTestSuite)) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "io/ioutil" 6 | "net" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/PeerXu/meepo/pkg/sdk" 12 | http_sdk "github.com/PeerXu/meepo/pkg/sdk/http" 13 | ) 14 | 15 | func NewHTTPSDK(cmd *cobra.Command) (sdk.MeepoSDK, error) { 16 | fs := cmd.Flags() 17 | host, _ := fs.GetString("host") 18 | 19 | sdk, err := sdk.NewMeepoSDK("http", http_sdk.WithHost(host)) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return sdk, nil 25 | } 26 | 27 | func MustResolveTCPAddr(network string, address string) net.Addr { 28 | addr, _ := net.ResolveTCPAddr(network, address) 29 | return addr 30 | } 31 | 32 | func ParseValue(val string) (string, error) { 33 | if strings.HasPrefix(val, "base64:") { 34 | buf, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(val, "base64:")) 35 | if err != nil { 36 | return "", err 37 | } 38 | return string(buf), nil 39 | } 40 | 41 | if strings.HasPrefix(val, "file://") { 42 | buf, err := ioutil.ReadFile(strings.TrimPrefix(val, "file://")) 43 | if err != nil { 44 | return "", err 45 | } 46 | return string(buf), nil 47 | } 48 | 49 | return val, nil 50 | } 51 | -------------------------------------------------------------------------------- /cmd/config_set.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/PeerXu/meepo/cmd/config" 10 | ) 11 | 12 | var ( 13 | configSetCmd = &cobra.Command{ 14 | Use: "set = [= ...]", 15 | Short: "Set Meepo config setting", 16 | RunE: meepoConfigSet, 17 | } 18 | ) 19 | 20 | func meepoConfigSet(cmd *cobra.Command, args []string) error { 21 | fs := cmd.Flags() 22 | 23 | cp, _ := fs.GetString("config") 24 | 25 | if len(args) == 0 { 26 | return fmt.Errorf("Require config(key=value)") 27 | } 28 | 29 | cfg, _, err := config.Load(cp) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | for _, arg := range args { 35 | ss := strings.SplitN(arg, "=", 2) 36 | if len(ss) != 2 { 37 | return fmt.Errorf("Require config(key=value)") 38 | } 39 | 40 | key, val := ss[0], ss[1] 41 | if val, err = ParseValue(val); err != nil { 42 | return err 43 | } 44 | 45 | if err = cfg.Set(key, val); err != nil { 46 | return err 47 | } 48 | } 49 | 50 | if err = cfg.Dump(cp); err != nil { 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func init() { 58 | configCmd.AddCommand(configSetCmd) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/meepo/util.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func checkAddrIsListenable(addr net.Addr) (net.Addr, error) { 8 | lis, err := net.Listen(addr.Network(), addr.String()) 9 | if err != nil { 10 | return nil, err 11 | } 12 | defer lis.Close() 13 | 14 | tcpAddr, _ := net.ResolveTCPAddr(lis.Addr().Network(), lis.Addr().String()) 15 | 16 | return tcpAddr, nil 17 | } 18 | 19 | // TODO(Peer): More robustness 20 | func getListenableAddr() net.Addr { 21 | for { 22 | addr, _ := net.ResolveTCPAddr("tcp", "localhost:0") 23 | if addr, err := checkAddrIsListenable(addr); err == nil { 24 | return addr 25 | } 26 | } 27 | } 28 | 29 | func (mp *Meepo) resolveTeleportationSourceAddr(network, address string) (net.Addr, error) { 30 | switch network { 31 | case "socks5": 32 | return SOCKS5ADDR, nil 33 | case "tcp": 34 | return net.ResolveTCPAddr(network, address) 35 | default: 36 | return nil, ErrUnsupportedNetworkType 37 | } 38 | } 39 | 40 | func (mp *Meepo) resolveTeleportationSinkAddr(network, address string) (net.Addr, error) { 41 | switch network { 42 | case "tcp": 43 | return net.ResolveTCPAddr(network, address) 44 | default: 45 | return nil, ErrUnsupportedNetworkType 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/transport/webrtc/option.go: -------------------------------------------------------------------------------- 1 | package webrtc_transport 2 | 3 | import ( 4 | "github.com/pion/webrtc/v3" 5 | 6 | "github.com/PeerXu/meepo/pkg/ofn" 7 | "github.com/PeerXu/meepo/pkg/transport" 8 | ) 9 | 10 | func AsAnswerer() transport.NewTransportOption { 11 | return func(o ofn.Option) { 12 | o["role"] = "answerer" 13 | } 14 | } 15 | 16 | func AsOfferer() transport.NewTransportOption { 17 | return func(o ofn.Option) { 18 | o["role"] = "offerer" 19 | } 20 | } 21 | 22 | func WithWebrtcAPI(api *webrtc.API) transport.NewTransportOption { 23 | return func(o ofn.Option) { 24 | o["webrtcAPI"] = api 25 | } 26 | } 27 | 28 | func WithICEServers(iceServers []string) transport.NewTransportOption { 29 | return func(o ofn.Option) { 30 | o["iceServers"] = iceServers 31 | } 32 | } 33 | 34 | func WithOffer(offer *webrtc.SessionDescription) transport.NewTransportOption { 35 | return func(o ofn.Option) { 36 | o["offer"] = offer 37 | } 38 | } 39 | 40 | func WithOfferHook(offerHook OfferHook) transport.NewTransportOption { 41 | return func(o ofn.Option) { 42 | o["offerHook"] = offerHook 43 | } 44 | } 45 | 46 | func WithAnswerHook(answerHook AnswerHook) transport.NewTransportOption { 47 | return func(o ofn.Option) { 48 | o["answerHook"] = answerHook 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/teleportation_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | teleportationListCmd = &cobra.Command{ 13 | Use: "list", 14 | Short: "List teleportations", 15 | Aliases: []string{"ls", "l"}, 16 | RunE: meepoTeleportationList, 17 | } 18 | ) 19 | 20 | func meepoTeleportationList(cmd *cobra.Command, args []string) error { 21 | var err error 22 | 23 | sdk, err := NewHTTPSDK(cmd) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | tps, err := sdk.ListTeleportations() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | table := tablewriter.NewWriter(os.Stdout) 34 | table.SetHeader([]string{ 35 | "Name", 36 | "Transport", 37 | "Portal", 38 | "Source", 39 | "Sink", 40 | "Channels", 41 | }) 42 | 43 | for _, tp := range tps { 44 | table.Append([]string{ 45 | tp.Name, 46 | tp.Transport.PeerID, 47 | tp.Portal, 48 | fmt.Sprintf("%s:%s", tp.Source.Network, tp.Source.Address), 49 | fmt.Sprintf("%s:%s", tp.Sink.Network, tp.Sink.Address), 50 | fmt.Sprintf("%d", len(tp.DataChannels)), 51 | }) 52 | } 53 | 54 | table.Render() 55 | 56 | return nil 57 | } 58 | 59 | func init() { 60 | teleportationCmd.AddCommand(teleportationListCmd) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/config/unmarshal.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func joinNamespaceName(namespace, name string) string { 11 | return namespace + "." + name 12 | } 13 | 14 | func WrapKeyYaml(key, text string) string { 15 | var sb strings.Builder 16 | ss := strings.Split(text, "\n") 17 | sb.WriteString(key + ":\n") 18 | for _, s := range ss { 19 | if s != "" { 20 | sb.WriteString(" " + s + "\n") 21 | } 22 | } 23 | return sb.String() 24 | } 25 | 26 | func UnmarshalConfig(namespace, name, text string) (interface{}, error) { 27 | return unmarshalConfig(namespace, name, yaml.NewDecoder(strings.NewReader(text)).Decode) 28 | } 29 | 30 | func unmarshalConfig(namespace, name string, unmarshal func(interface{}) error) (interface{}, error) { 31 | fn, ok := unmarshalConfigFuncs.Load(joinNamespaceName(namespace, name)) 32 | if !ok { 33 | return nil, UnsupportedError{Namespace: namespace, Name: name} 34 | } 35 | 36 | return fn.(UnmarshalConfigFunc)(unmarshal) 37 | } 38 | 39 | type UnmarshalConfigFunc = func(unmarshal func(interface{}) error) (interface{}, error) 40 | 41 | var unmarshalConfigFuncs sync.Map 42 | 43 | func RegisterUnmarshalConfigFunc(namespace, name string, fn UnmarshalConfigFunc) { 44 | unmarshalConfigFuncs.Store(joinNamespaceName(namespace, name), fn) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/api/encoding/teleportation.go: -------------------------------------------------------------------------------- 1 | package encoding_api 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/PeerXu/meepo/pkg/teleportation" 7 | ) 8 | 9 | type Addr struct { 10 | Network string `json:"network"` 11 | Address string `json:"address"` 12 | } 13 | 14 | type Teleportation struct { 15 | Name string `json:"name"` 16 | Source *Addr `json:"source"` 17 | Sink *Addr `json:"sink"` 18 | Portal string `json:"portal"` 19 | Transport *Transport `json:"transport"` 20 | DataChannels []*DataChannel `json:"dataChannels"` 21 | } 22 | 23 | func ConvertAddr(x net.Addr) *Addr { 24 | return &Addr{ 25 | Network: x.Network(), 26 | Address: x.String(), 27 | } 28 | } 29 | 30 | func ConvertTeleportation(x teleportation.Teleportation) *Teleportation { 31 | return &Teleportation{ 32 | Name: x.Name(), 33 | Source: ConvertAddr(x.Source()), 34 | Sink: ConvertAddr(x.Sink()), 35 | Portal: x.Portal().String(), 36 | Transport: &Transport{ 37 | PeerID: x.Transport().PeerID(), 38 | State: x.Transport().TransportState().String(), 39 | }, 40 | DataChannels: ConvertDataChannels(x.DataChannels()), 41 | } 42 | } 43 | 44 | func ConvertTeleportations(xs []teleportation.Teleportation) []*Teleportation { 45 | var ys []*Teleportation 46 | 47 | for _, x := range xs { 48 | ys = append(ys, ConvertTeleportation(x)) 49 | } 50 | 51 | return ys 52 | } 53 | -------------------------------------------------------------------------------- /pkg/util/conn/rw.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | type rwConn struct { 12 | reader io.Reader 13 | writer io.Writer 14 | local net.Addr 15 | remote net.Addr 16 | } 17 | 18 | func (c *rwConn) Read(p []byte) (int, error) { 19 | return c.reader.Read(p) 20 | } 21 | 22 | func (c *rwConn) Write(p []byte) (int, error) { 23 | return c.writer.Write(p) 24 | } 25 | 26 | func (c *rwConn) Close() error { 27 | var eg errgroup.Group 28 | 29 | rdCloser, ok := c.reader.(io.Closer) 30 | if ok { 31 | eg.Go(rdCloser.Close) 32 | } 33 | 34 | wrCloser, ok := c.writer.(io.Closer) 35 | if ok { 36 | eg.Go(wrCloser.Close) 37 | } 38 | 39 | return eg.Wait() 40 | } 41 | 42 | func (c *rwConn) LocalAddr() net.Addr { 43 | return c.local 44 | } 45 | 46 | func (c *rwConn) RemoteAddr() net.Addr { 47 | return c.remote 48 | } 49 | 50 | func (c *rwConn) SetDeadline(t time.Time) error { 51 | panic("unimplemented") 52 | } 53 | 54 | func (c *rwConn) SetReadDeadline(t time.Time) error { 55 | panic("unimplemented") 56 | } 57 | 58 | func (c *rwConn) SetWriteDeadline(t time.Time) error { 59 | panic("unimplemented") 60 | } 61 | 62 | func NewRWConn(reader io.Reader, writer io.Writer, local net.Addr, remote net.Addr) *rwConn { 63 | return &rwConn{ 64 | reader: reader, 65 | writer: writer, 66 | local: local, 67 | remote: remote, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/PeerXu/meepo/pkg/sdk" 9 | "github.com/PeerXu/meepo/pkg/util/version" 10 | ) 11 | 12 | var ( 13 | versionCmd = &cobra.Command{ 14 | Use: "version", 15 | Short: "Print version information", 16 | RunE: meepoVersion, 17 | } 18 | ) 19 | 20 | func printClientVersion(v *version.V) { 21 | fmt.Printf("Meepo Client:\n") 22 | fmt.Printf(" Version:\t\t%v\n", v.Version) 23 | fmt.Printf(" GoVersion:\t\t%v\n", v.GoVersion) 24 | fmt.Printf(" GitHash:\t\t%v\n", v.GitHash) 25 | fmt.Printf(" Bulit:\t\t%v\n", v.Built) 26 | fmt.Printf(" Platform:\t\t%v\n", v.Platform) 27 | } 28 | 29 | func printServerVersion(v *sdk.Version) { 30 | fmt.Printf("Meepo Server:\n") 31 | fmt.Printf(" Version:\t\t%v\n", v.Version) 32 | fmt.Printf(" GoVersion:\t\t%v\n", v.GoVersion) 33 | fmt.Printf(" GitHash:\t\t%v\n", v.GitHash) 34 | fmt.Printf(" Bulit:\t\t%v\n", v.Built) 35 | fmt.Printf(" Platform:\t\t%v\n", v.Platform) 36 | } 37 | 38 | func meepoVersion(cmd *cobra.Command, args []string) error { 39 | printClientVersion(version.Get()) 40 | fmt.Println() 41 | 42 | sdk, err := NewHTTPSDK(cmd) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | sv, err := sdk.Version() 48 | if err != nil { 49 | return err 50 | } 51 | printServerVersion(sv) 52 | 53 | return nil 54 | } 55 | 56 | func init() { 57 | rootCmd.AddCommand(versionCmd) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/config_init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/PeerXu/meepo/cmd/config" 11 | ) 12 | 13 | var ( 14 | configInitCmd = &cobra.Command{ 15 | Use: "init [--overwrite] [= ...]", 16 | Short: "Initial config file", 17 | RunE: meepoConfigInit, 18 | } 19 | ) 20 | 21 | func meepoConfigInit(cmd *cobra.Command, args []string) error { 22 | var loaded bool 23 | var err error 24 | 25 | fs := cmd.Flags() 26 | cp, _ := fs.GetString("config") 27 | overwrite, _ := fs.GetBool("overwrite") 28 | 29 | if _, loaded, err = config.Load(cp); err != nil { 30 | if !os.IsNotExist(err) { 31 | return err 32 | } 33 | } 34 | 35 | if !overwrite && loaded { 36 | return fmt.Errorf("Config already existed") 37 | } 38 | 39 | cfg := config.NewDefaultConfig() 40 | 41 | for _, arg := range args { 42 | ss := strings.SplitN(arg, "=", 2) 43 | if len(ss) != 2 { 44 | return fmt.Errorf("Require config(key=value)") 45 | } 46 | 47 | key, val := ss[0], ss[1] 48 | if val, err = ParseValue(val); err != nil { 49 | return err 50 | } 51 | 52 | if err = cfg.Set(key, val); err != nil { 53 | return err 54 | } 55 | } 56 | 57 | if err = cfg.Dump(cp); err != nil { 58 | return err 59 | } 60 | 61 | fmt.Println("Meepo config initialized") 62 | 63 | return nil 64 | } 65 | 66 | func init() { 67 | configCmd.AddCommand(configInitCmd) 68 | 69 | configInitCmd.PersistentFlags().Bool("overwrite", false, "Overwrite exists config file") 70 | } 71 | -------------------------------------------------------------------------------- /pkg/api/http/api.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/spf13/cast" 10 | "golang.org/x/sync/errgroup" 11 | 12 | "github.com/PeerXu/meepo/pkg/api" 13 | "github.com/PeerXu/meepo/pkg/meepo" 14 | "github.com/PeerXu/meepo/pkg/ofn" 15 | ) 16 | 17 | func NewHttpServerOption() ofn.Option { 18 | return ofn.NewOption(map[string]interface{}{ 19 | "host": "127.0.0.1", 20 | "port": 12345, 21 | }) 22 | } 23 | 24 | type HttpServer struct { 25 | opt ofn.Option 26 | 27 | httpd *http.Server 28 | eg errgroup.Group 29 | 30 | meepo *meepo.Meepo 31 | } 32 | 33 | func (s *HttpServer) Start(ctx context.Context) error { 34 | host := cast.ToString(s.opt.Get("host").Inter()) 35 | port := cast.ToString(s.opt.Get("port").Inter()) 36 | 37 | s.httpd = &http.Server{ 38 | Addr: net.JoinHostPort(host, port), 39 | } 40 | s.httpd.Handler = s.getRouter() 41 | 42 | s.eg.Go(s.httpd.ListenAndServe) 43 | 44 | return nil 45 | } 46 | 47 | func (s *HttpServer) Stop(ctx context.Context) error { 48 | return s.httpd.Shutdown(ctx) 49 | } 50 | 51 | func (s *HttpServer) Wait() error { 52 | return s.eg.Wait() 53 | } 54 | 55 | func NewHttpServer(opts ...api.NewServerOption) (api.Server, error) { 56 | o := NewHttpServerOption() 57 | for _, opt := range opts { 58 | opt(o) 59 | } 60 | 61 | mp, ok := o.Get("meepo").Inter().(*meepo.Meepo) 62 | if !ok { 63 | return nil, fmt.Errorf("require meepo") 64 | } 65 | 66 | return &HttpServer{ 67 | opt: o, 68 | meepo: mp, 69 | }, nil 70 | } 71 | 72 | func init() { 73 | api.RegisterNewServerFunc("http", NewHttpServer) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/util/sync/channel.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | type ChannelLocker interface { 4 | Acquire(id int32) error 5 | Release(id int32) error 6 | Get(id int32) (chan interface{}, error) 7 | GetWithUnlock(id int32) (chan interface{}, func(), error) 8 | } 9 | 10 | type channelLocker struct { 11 | chs map[int32]chan interface{} 12 | mtx Locker 13 | } 14 | 15 | func (t *channelLocker) Acquire(id int32) error { 16 | t.mtx.Lock() 17 | defer t.mtx.Unlock() 18 | 19 | ch := make(chan interface{}) 20 | if _, ok := t.chs[id]; ok { 21 | defer close(ch) 22 | return ChannelExistError 23 | } 24 | 25 | t.chs[id] = ch 26 | 27 | return nil 28 | } 29 | 30 | func (t *channelLocker) Release(id int32) error { 31 | t.mtx.Lock() 32 | defer t.mtx.Unlock() 33 | 34 | ch, err := t.getNL(id) 35 | if err != nil { 36 | return err 37 | } 38 | defer close(ch) 39 | 40 | delete(t.chs, id) 41 | 42 | return nil 43 | } 44 | 45 | func (t *channelLocker) Get(id int32) (chan interface{}, error) { 46 | return t.getNL(id) 47 | } 48 | 49 | func (t *channelLocker) GetWithUnlock(id int32) (ch chan interface{}, unlock func(), err error) { 50 | t.mtx.Lock() 51 | 52 | ch, err = t.getNL(id) 53 | if err != nil { 54 | defer t.mtx.Unlock() 55 | return nil, nil, err 56 | } 57 | 58 | return ch, t.mtx.Unlock, nil 59 | } 60 | 61 | func (t *channelLocker) getNL(id int32) (chan interface{}, error) { 62 | ch, ok := t.chs[id] 63 | if !ok { 64 | return nil, ChannelNotExistError 65 | } 66 | 67 | return ch, nil 68 | } 69 | 70 | func NewChannelLocker() ChannelLocker { 71 | return &channelLocker{ 72 | chs: make(map[int32]chan interface{}), 73 | mtx: NewLock(), 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/meepo/authentication.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "github.com/spf13/cast" 5 | 6 | "github.com/PeerXu/meepo/pkg/meepo/auth" 7 | "github.com/PeerXu/meepo/pkg/meepo/packet" 8 | "github.com/PeerXu/meepo/pkg/ofn" 9 | ) 10 | 11 | func WithPacket(p packet.Packet) ofn.OFN { 12 | return func(o ofn.Option) { 13 | o["packet"] = p 14 | } 15 | } 16 | 17 | func (mp *Meepo) Authenticate(subject string, opts ...auth.AuthenticateOption) (err error) { 18 | logger := mp.getLogger().WithField("#method", "Authenticate") 19 | 20 | o := ofn.NewOption(map[string]interface{}{}) 21 | 22 | for _, opt := range opts { 23 | opt(o) 24 | } 25 | 26 | in, ok := o.Get("packet").Inter().(packet.Packet) 27 | if !ok { 28 | logger.Debugf("require packet") 29 | return ErrUnauthenticated 30 | } 31 | 32 | if in.Header().Source() != subject { 33 | logger.Debugf("require source equal to subject") 34 | return ErrUnauthenticated 35 | } 36 | 37 | if err = mp.verifyPacket(in); err != nil { 38 | logger.WithError(err).Debugf("failed to verify packet") 39 | return ErrUnauthenticated 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func WithSubject(sub string) ofn.OFN { 46 | return func(o ofn.Option) { 47 | o["subject"] = sub 48 | } 49 | } 50 | 51 | type authenticatePacketOption = ofn.OFN 52 | 53 | func (mp *Meepo) authenticatePacket(p packet.Packet, opts ...authenticatePacketOption) error { 54 | o := ofn.NewOption(map[string]interface{}{}) 55 | for _, opt := range opts { 56 | opt(o) 57 | } 58 | 59 | sub := cast.ToString(o.Get("subject").Inter()) 60 | if sub == "" { 61 | sub = p.Header().Source() 62 | } 63 | 64 | return mp.authentication.Authenticate(sub, WithPacket(p)) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/meepo/packet/header_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/PeerXu/meepo/pkg/meepo/packet" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type HeaderTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (s *HeaderTestSuite) TestMarshalAndUnmarshal() { 15 | h := packet.NewHeader(1, "a", "b", packet.Request, "test") 16 | h = h.SetSignature([]byte("abc")) 17 | buf, err := packet.MarshalHeader(h) 18 | s.Require().Nil(err) 19 | 20 | h1, err := packet.UnmarshalHeader(buf) 21 | s.Require().Nil(err) 22 | 23 | s.Equal(h.Session(), h1.Session()) 24 | s.Equal(h.Source(), h1.Source()) 25 | s.Equal(h.Destination(), h1.Destination()) 26 | s.Equal(h.Type(), h1.Type()) 27 | s.Equal(h.Method(), h1.Method()) 28 | s.Equal(h.Signature(), h1.Signature()) 29 | } 30 | 31 | func (s *HeaderTestSuite) TestInvertHeader() { 32 | h := packet.NewHeader(1, "a", "b", packet.Request, "test") 33 | h = h.SetSignature([]byte("abc")) 34 | ih := packet.InvertHeader(h) 35 | 36 | s.Equal(h.Session(), ih.Session()) 37 | s.Equal(h.Destination(), ih.Source()) 38 | s.Equal(h.Source(), ih.Destination()) 39 | s.Equal(packet.Response, ih.Type()) 40 | s.Equal(h.Method(), ih.Method()) 41 | s.Nil(ih.Signature()) 42 | } 43 | 44 | func (s *HeaderTestSuite) TestSetAndUnsetSignature() { 45 | h := packet.NewHeader(1, "a", "b", packet.Request, "test") 46 | hs := h.SetSignature([]byte("ttt")) 47 | hus := h.UnsetSignature() 48 | s.Nil(h.Signature()) 49 | s.Equal([]byte("ttt"), hs.Signature()) 50 | hb, err := packet.MarshalHeader(h) 51 | s.Require().Nil(err) 52 | husb, err := packet.MarshalHeader(hus) 53 | s.Require().Nil(err) 54 | s.Equal(hb, husb) 55 | } 56 | 57 | func TestHeaderTestSuite(t *testing.T) { 58 | suite.Run(t, new(HeaderTestSuite)) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/sdk/base.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import "net" 4 | 5 | type BaseMeepoSDK struct { 6 | BaseTransportSDK 7 | BaseTeleportationSDK 8 | } 9 | 10 | func (BaseMeepoSDK) Version() (*Version, error) { 11 | return nil, UnimplementedError 12 | } 13 | 14 | func (BaseMeepoSDK) Ping() error { 15 | return UnimplementedError 16 | } 17 | 18 | func (BaseMeepoSDK) Shutdown() error { 19 | return UnimplementedError 20 | } 21 | 22 | func (BaseMeepoSDK) Whoami() (string, error) { 23 | return "", UnimplementedError 24 | } 25 | 26 | func (BaseMeepoSDK) Teleport(peerID string, remote net.Addr, opt *TeleportOption) (net.Addr, error) { 27 | return nil, UnimplementedError 28 | } 29 | 30 | type BaseTransportSDK struct{} 31 | 32 | func (BaseTransportSDK) NewTransport(peerID string) (*Transport, error) { 33 | return nil, UnimplementedError 34 | } 35 | 36 | func (BaseTransportSDK) CloseTransport(peerID string) error { 37 | return UnimplementedError 38 | } 39 | 40 | func (BaseTransportSDK) ListTransports() ([]*Transport, error) { 41 | return nil, UnimplementedError 42 | } 43 | 44 | func (BaseTransportSDK) GetTransport(peerID string) (*Transport, error) { 45 | return nil, UnimplementedError 46 | } 47 | 48 | type BaseTeleportationSDK struct{} 49 | 50 | func (BaseTeleportationSDK) NewTeleportation(peerID string, sink net.Addr, opt *NewTeleportationOption) (*Teleportation, error) { 51 | return nil, UnimplementedError 52 | } 53 | 54 | func (BaseTeleportationSDK) CloseTeleportation(name string) error { 55 | return UnimplementedError 56 | } 57 | 58 | func (BaseTeleportationSDK) ListTeleportations() ([]*Teleportation, error) { 59 | return nil, UnimplementedError 60 | } 61 | 62 | func (BaseTeleportationSDK) GetTeleportation(name string) (*Teleportation, error) { 63 | return nil, UnimplementedError 64 | } 65 | -------------------------------------------------------------------------------- /pkg/signaling/chain/signaling.go: -------------------------------------------------------------------------------- 1 | package chain_signaling 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/PeerXu/meepo/pkg/ofn" 10 | "github.com/PeerXu/meepo/pkg/signaling" 11 | ) 12 | 13 | type ChainEngine struct { 14 | opt ofn.Option 15 | logger logrus.FieldLogger 16 | 17 | engines []signaling.Engine 18 | } 19 | 20 | func (e *ChainEngine) Wire(dst, src *signaling.Descriptor) (*signaling.Descriptor, error) { 21 | var res *signaling.Descriptor 22 | var err error 23 | 24 | for _, ng := range e.engines { 25 | if res, err = ng.Wire(dst, src); err == nil { 26 | return res, nil 27 | } 28 | } 29 | 30 | return nil, err 31 | } 32 | 33 | func (e *ChainEngine) OnWire(handler signaling.WireHandler) { 34 | for _, ng := range e.engines { 35 | ng.OnWire(handler) 36 | } 37 | } 38 | 39 | func (e *ChainEngine) Close() error { 40 | var errOnce sync.Once 41 | var err error 42 | 43 | for _, ng := range e.engines { 44 | if er := ng.Close(); er != nil { 45 | errOnce.Do(func() { err = er }) 46 | } 47 | } 48 | 49 | return err 50 | } 51 | 52 | func NewChainEngine(opts ...signaling.NewEngineOption) (signaling.Engine, error) { 53 | o := DefaultEngineOption() 54 | 55 | for _, opt := range opts { 56 | opt(o) 57 | } 58 | 59 | logger, ok := o.Get("logger").Inter().(logrus.FieldLogger) 60 | if !ok { 61 | return nil, fmt.Errorf("Require logger") 62 | } 63 | 64 | engines, ok := o.Get("engines").Inter().([]signaling.Engine) 65 | if !ok { 66 | return nil, fmt.Errorf("Require engines") 67 | } 68 | 69 | ce := &ChainEngine{ 70 | opt: o, 71 | logger: logger, 72 | engines: engines, 73 | } 74 | 75 | return ce, nil 76 | } 77 | 78 | func init() { 79 | signaling.RegisterNewEngineFunc("chain", NewChainEngine) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/meepo/packet/broadcast_header.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import "fmt" 4 | 5 | type BroadcastHeader interface { 6 | Session() int32 7 | Source() string 8 | Destination() string 9 | Type() Type 10 | Method() string 11 | Hop() int32 12 | } 13 | 14 | func NewBroadcastHeader(sess int32, src, dst string, typ Type, meth string, hop int32) BroadcastHeader { 15 | return &broadcastHeader{ 16 | session: sess, 17 | source: src, 18 | destination: dst, 19 | typ: typ, 20 | method: meth, 21 | hop: hop, 22 | } 23 | } 24 | 25 | func InvertBroadcastHeader(in BroadcastHeader) (out BroadcastHeader) { 26 | var typ Type 27 | switch in.Type() { 28 | case BroadcastRequest: 29 | typ = BroadcastResponse 30 | case BroadcastResponse: 31 | typ = BroadcastRequest 32 | default: 33 | panic(fmt.Errorf("unexpected broadcast type: %v", in.Type())) 34 | } 35 | 36 | return &broadcastHeader{ 37 | session: in.Session(), 38 | source: in.Destination(), 39 | destination: in.Source(), 40 | typ: typ, 41 | method: in.Method(), 42 | hop: in.Hop(), 43 | } 44 | } 45 | 46 | type broadcastHeader struct { 47 | session int32 48 | source string 49 | destination string 50 | typ Type 51 | method string 52 | hop int32 53 | } 54 | 55 | func (h *broadcastHeader) Session() int32 { 56 | return h.session 57 | } 58 | 59 | func (h *broadcastHeader) Source() string { 60 | return h.source 61 | } 62 | 63 | func (h *broadcastHeader) Destination() string { 64 | return h.destination 65 | } 66 | 67 | func (h *broadcastHeader) Type() Type { 68 | return h.typ 69 | } 70 | 71 | func (h *broadcastHeader) Method() string { 72 | return h.method 73 | } 74 | 75 | func (h *broadcastHeader) Hop() int32 { 76 | return h.hop 77 | } 78 | -------------------------------------------------------------------------------- /pkg/signaling/redis/signaling_test.go: -------------------------------------------------------------------------------- 1 | package redis_signaling_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/cast" 9 | "github.com/spf13/viper" 10 | "github.com/stretchr/objx" 11 | "github.com/stretchr/testify/suite" 12 | 13 | "github.com/PeerXu/meepo/pkg/ofn" 14 | "github.com/PeerXu/meepo/pkg/signaling" 15 | redis_signaling "github.com/PeerXu/meepo/pkg/signaling/redis" 16 | ) 17 | 18 | func newEngineOptions(id string) []signaling.NewEngineOption { 19 | logger := logrus.New() 20 | logger.SetLevel(logrus.TraceLevel) 21 | 22 | return []signaling.NewEngineOption{ 23 | signaling.WithID(id), 24 | redis_signaling.WithURL(viper.GetString("redis_url")), 25 | signaling.WithLogger(logger), 26 | } 27 | } 28 | 29 | type RedisEngineTestSuite struct { 30 | suite.Suite 31 | } 32 | 33 | func (s *RedisEngineTestSuite) TestRedisEngine() { 34 | ea, err := redis_signaling.NewRedisEngine(newEngineOptions("a")...) 35 | s.Require().Nil(err) 36 | defer ea.Close() 37 | 38 | ea.OnWire(func(in *signaling.Descriptor) (*signaling.Descriptor, error) { 39 | ud := ofn.NewOption(in.UserData) 40 | s.Equal(1, cast.ToInt(ud.Get("a").Inter())) 41 | 42 | return in, nil 43 | }) 44 | 45 | time.Sleep(1 * time.Millisecond) 46 | d, err := ea.Wire( 47 | &signaling.Descriptor{ID: "a"}, 48 | &signaling.Descriptor{ 49 | ID: "a", 50 | UserData: map[string]interface{}{ 51 | "a": 1, 52 | }, 53 | }, 54 | ) 55 | s.Require().Nil(err) 56 | s.Equal(1, cast.ToInt(objx.New(d.UserData).Get("a").Inter())) 57 | } 58 | 59 | func TestRedisEngineTestSuite(t *testing.T) { 60 | suite.Run(t, new(RedisEngineTestSuite)) 61 | } 62 | 63 | func init() { 64 | viper.SetEnvPrefix("mpt") 65 | viper.BindEnv("redis_url") 66 | viper.SetDefault("redis_url", "redis://127.0.0.1:6379/0") 67 | } 68 | -------------------------------------------------------------------------------- /pkg/meepo/ping.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/PeerXu/meepo/pkg/meepo/packet" 9 | "github.com/PeerXu/meepo/pkg/transport" 10 | ) 11 | 12 | const ( 13 | METHOD_PING Method = "ping" 14 | ) 15 | 16 | type ( 17 | PingRequest struct { 18 | Payload string 19 | } 20 | 21 | PongResponse struct { 22 | Payload string 23 | } 24 | ) 25 | 26 | func (mp *Meepo) Ping(id string, payload string) error { 27 | var pong PongResponse 28 | 29 | logger := mp.getLogger().WithFields(logrus.Fields{ 30 | "#method": "Ping", 31 | "peerID": id, 32 | }) 33 | 34 | in := mp.createRequest(id, METHOD_PING, &PingRequest{Payload: payload}) 35 | 36 | out, err := mp.doRequest(in) 37 | if err != nil { 38 | logger.WithError(err).Errorf("failed to do request") 39 | return err 40 | } 41 | 42 | if err = out.Data(&pong); err != nil { 43 | logger.WithError(err).Errorf("failed to unmarshal response data") 44 | return err 45 | } 46 | 47 | if pong.Payload != payload { 48 | err = fmt.Errorf("Unmatched pong payload") 49 | logger.WithError(err).Errorf("failed to ping") 50 | return err 51 | } 52 | 53 | logger.Infof("ping") 54 | 55 | return nil 56 | } 57 | 58 | func (mp *Meepo) onPing(dc transport.DataChannel, in packet.Packet) { 59 | var ping PingRequest 60 | 61 | hdr := in.Header() 62 | logger := mp.getLogger().WithFields(logrus.Fields{ 63 | "#method": "onPing", 64 | "peerID": hdr.Source(), 65 | "session": hdr.Session(), 66 | }) 67 | 68 | if err := in.Data(&ping); err != nil { 69 | logger.WithError(err).Errorf("failed to unmarshal request data") 70 | mp.sendResponse(dc, mp.createResponseWithError(in, err)) 71 | return 72 | } 73 | 74 | out := mp.createResponse(in, &PongResponse{Payload: ping.Payload}) 75 | 76 | mp.sendResponse(dc, out) 77 | 78 | logger.Infof("pong") 79 | } 80 | -------------------------------------------------------------------------------- /pkg/sdk/http/sdk.go: -------------------------------------------------------------------------------- 1 | package http_sdk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/url" 7 | 8 | "github.com/go-resty/resty/v2" 9 | "github.com/spf13/cast" 10 | 11 | "github.com/PeerXu/meepo/pkg/ofn" 12 | "github.com/PeerXu/meepo/pkg/sdk" 13 | ) 14 | 15 | func WithHost(host string) sdk.NewMeepoSDKOption { 16 | return func(o ofn.Option) { 17 | o["host"] = host 18 | } 19 | } 20 | 21 | type MeepoSDK struct { 22 | sdk.BaseMeepoSDK 23 | 24 | opt ofn.Option 25 | client *resty.Client 26 | 27 | host string 28 | } 29 | 30 | func (t *MeepoSDK) joinPath(p string) (string, error) { 31 | u, err := url.Parse(t.host) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | u.Path = p 37 | 38 | return u.String(), nil 39 | } 40 | 41 | func (t *MeepoSDK) doRequest(path string, req interface{}, res interface{}, expectCode int) error { 42 | u, err := t.joinPath(path) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | out, err := t.client.R().SetBody(req).Post(u) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if out.StatusCode() != expectCode { 53 | return sdk.ExtractError(out.Body()) 54 | } 55 | 56 | if res != nil { 57 | if err = json.NewDecoder(bytes.NewReader(out.Body())).Decode(res); err != nil { 58 | return err 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func newNewMeepoSDKOption() ofn.Option { 66 | return ofn.NewOption(map[string]interface{}{ 67 | "host": "http://localhost:12345", 68 | }) 69 | } 70 | 71 | func NewMeepoSDK(opts ...sdk.NewMeepoSDKOption) (sdk.MeepoSDK, error) { 72 | o := newNewMeepoSDKOption() 73 | 74 | for _, opt := range opts { 75 | opt(o) 76 | } 77 | 78 | host := cast.ToString(o.Get("host").Inter()) 79 | 80 | return &MeepoSDK{ 81 | opt: o, 82 | host: host, 83 | client: resty.New(), 84 | }, nil 85 | } 86 | 87 | func init() { 88 | sdk.RegisterNewMeepoSDKFunc("http", NewMeepoSDK) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "sync" 4 | 5 | type TransportState int 6 | 7 | const ( 8 | TransportStateNew TransportState = iota + 1 9 | TransportStateConnecting 10 | TransportStateConnected 11 | TransportStateDisconnected 12 | TransportStateFailed 13 | TransportStateClosed 14 | ) 15 | 16 | var ( 17 | transportStateStr = []string{ 18 | TransportStateNew: "new", 19 | TransportStateConnecting: "connecting", 20 | TransportStateConnected: "connected", 21 | TransportStateDisconnected: "disconnected", 22 | TransportStateFailed: "failed", 23 | TransportStateClosed: "closed", 24 | } 25 | ) 26 | 27 | func (t TransportState) String() string { 28 | return transportStateStr[t] 29 | } 30 | 31 | type HandleID = uint32 32 | type OnTransportStateHandler func(HandleID) 33 | 34 | type Transport interface { 35 | PeerID() string 36 | Err() error 37 | Close() error 38 | OnTransportStateChange(func(TransportState)) 39 | OnTransportState(TransportState, func(hid HandleID)) HandleID 40 | UnsetOnTransportState(s TransportState, hid HandleID) 41 | TransportState() TransportState 42 | 43 | DataChannels() ([]DataChannel, error) 44 | DataChannel(label string) (DataChannel, error) 45 | CreateDataChannel(label string, opts ...CreateDataChannelOption) (DataChannel, error) 46 | OnDataChannelCreate(label string, f func(DataChannel)) 47 | } 48 | 49 | var ( 50 | newTransportFuncs sync.Map 51 | ) 52 | 53 | type NewTransportFunc func(...NewTransportOption) (Transport, error) 54 | 55 | func NewTransport(name string, opts ...NewTransportOption) (Transport, error) { 56 | fn, ok := newTransportFuncs.Load(name) 57 | if !ok { 58 | return nil, UnsupportedTransportError(name) 59 | } 60 | 61 | return fn.(NewTransportFunc)(opts...) 62 | } 63 | 64 | func RegisterNewTransportFunc(name string, fn NewTransportFunc) { 65 | newTransportFuncs.Store(name, fn) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/meepo/transport.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/transport" 5 | msync "github.com/PeerXu/meepo/pkg/util/sync" 6 | ) 7 | 8 | func (mp *Meepo) getTransportLock(peerID string) msync.Locker { 9 | val, _ := mp.transportLocks.LoadOrStore(peerID, msync.NewLock()) 10 | return val.(msync.Locker) 11 | } 12 | 13 | func (mp *Meepo) getTransport(peerID string) (transport.Transport, error) { 14 | mp.transportsMtx.Lock() 15 | defer mp.transportsMtx.Unlock() 16 | 17 | return mp.getTransportNL(peerID) 18 | } 19 | 20 | func (mp *Meepo) getTransportNL(peerID string) (transport.Transport, error) { 21 | transport, ok := mp.transports[peerID] 22 | if !ok { 23 | return nil, ErrTransportNotExist 24 | } 25 | 26 | return transport, nil 27 | } 28 | 29 | func (mp *Meepo) getConnectedTransport(peerID string) (transport.Transport, error) { 30 | tp, err := mp.getTransport(peerID) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if tp.TransportState() != transport.TransportStateConnected { 36 | return nil, ErrTransportNotExist 37 | } 38 | 39 | return tp, nil 40 | } 41 | 42 | func (mp *Meepo) listTransports() ([]transport.Transport, error) { 43 | mp.transportsMtx.Lock() 44 | defer mp.transportsMtx.Unlock() 45 | 46 | return mp.listTransportsNL() 47 | } 48 | 49 | func (mp *Meepo) listTransportsNL() ([]transport.Transport, error) { 50 | var tps []transport.Transport 51 | 52 | for _, tp := range mp.transports { 53 | tps = append(tps, tp) 54 | } 55 | 56 | return tps, nil 57 | } 58 | 59 | func (mp *Meepo) listConnectedTransports() ([]transport.Transport, error) { 60 | tps, err := mp.listTransports() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | var ctps []transport.Transport 66 | for _, tp := range tps { 67 | if tp.TransportState() == transport.TransportStateConnected { 68 | ctps = append(ctps, tp) 69 | } 70 | } 71 | 72 | return ctps, nil 73 | } 74 | -------------------------------------------------------------------------------- /cmd/teleport.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | msdk "github.com/PeerXu/meepo/pkg/sdk" 10 | ) 11 | 12 | var ( 13 | teleportCmd = &cobra.Command{ 14 | Use: "teleport [-n ] [-l ] [-s secret] ", 15 | Short: "New teleportation in easy way", 16 | RunE: meepoTeleport, 17 | Args: cobra.ExactArgs(2), 18 | } 19 | ) 20 | 21 | func meepoTeleport(cmd *cobra.Command, args []string) error { 22 | fs := cmd.Flags() 23 | name, _ := fs.GetString("name") 24 | localAddress, _ := fs.GetString("local-address") 25 | secret, _ := fs.GetString("secret") 26 | peerID := args[0] 27 | remoteAddress := args[1] 28 | localNetwork := "tcp" 29 | remoteNetwork := "tcp" 30 | 31 | sdk, err := NewHTTPSDK(cmd) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | var tpOpt msdk.TeleportOption 37 | 38 | remote, err := net.ResolveTCPAddr(remoteNetwork, remoteAddress) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if localAddress != "" { 44 | local, err := net.ResolveTCPAddr(localNetwork, localAddress) 45 | if err != nil { 46 | return err 47 | } 48 | tpOpt.Local = local 49 | } 50 | 51 | if name != "" { 52 | tpOpt.Name = name 53 | } 54 | 55 | if secret != "" { 56 | tpOpt.Secret = secret 57 | } 58 | 59 | local, err := sdk.Teleport(peerID, remote, &tpOpt) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | fmt.Printf("Teleport SUCCESS\n") 65 | fmt.Printf("Enjoy your teleportation with %s\n", local.String()) 66 | 67 | return nil 68 | } 69 | 70 | func init() { 71 | rootCmd.AddCommand(teleportCmd) 72 | 73 | teleportCmd.PersistentFlags().StringP("name", "n", "", "Transport and teleportation name") 74 | teleportCmd.PersistentFlags().StringP("local-address", "l", "", "Local listen address") 75 | teleportCmd.PersistentFlags().StringP("secret", "s", "", "New teleportation with secret") 76 | } 77 | -------------------------------------------------------------------------------- /pkg/meepo/authorization.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/cast" 9 | "golang.org/x/crypto/bcrypt" 10 | 11 | "github.com/PeerXu/meepo/pkg/meepo/auth" 12 | "github.com/PeerXu/meepo/pkg/ofn" 13 | ) 14 | 15 | func (mp *Meepo) GetAuthorizationName() string { 16 | return cast.ToString(mp.opt.Get("authorizationName").Inter()) 17 | } 18 | 19 | func (mp *Meepo) Authorize(subject, object, action string, opts ...auth.AuthorizeOption) error { 20 | authName := mp.GetAuthorizationName() 21 | switch authName { 22 | case "secret": 23 | return mp.secretAuthorize(subject, object, action, opts...) 24 | case "dummy": 25 | fallthrough 26 | default: 27 | return nil 28 | } 29 | } 30 | 31 | func (mp *Meepo) secretAuthorize(subject, object, action string, opts ...auth.AuthorizeOption) error { 32 | logger := mp.getLogger().WithFields(logrus.Fields{ 33 | "#method": "secretAuthorize", 34 | "subject": subject, 35 | "object": object, 36 | "action": action, 37 | }) 38 | 39 | o := ofn.NewOption(map[string]interface{}{}) 40 | for _, opt := range opts { 41 | opt(o) 42 | } 43 | 44 | switch action { 45 | case string(METHOD_NEW_TELEPORTATION): 46 | hashedSecret, err := base64.StdEncoding.DecodeString(cast.ToString(o.Get("authorizationSecret").Inter())) 47 | if err != nil { 48 | return fmt.Errorf("%w: invalid hashed secret", ErrUnauthorized) 49 | } 50 | 51 | if len(hashedSecret) == 0 { 52 | return fmt.Errorf("%w: require secret", ErrUnauthorized) 53 | } 54 | 55 | secret := cast.ToString(mp.opt.Get("authorizationSecret").Inter()) 56 | 57 | if err = bcrypt.CompareHashAndPassword([]byte(hashedSecret), []byte(secret)); err != nil { 58 | logger.WithError(err).Debugf("incorrect password") 59 | return fmt.Errorf("%w: incorrect password", ErrUnauthorized) 60 | } 61 | default: 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/sdk/sdk.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | encoding_api "github.com/PeerXu/meepo/pkg/api/encoding" 8 | "github.com/PeerXu/meepo/pkg/ofn" 9 | ) 10 | 11 | type Version = encoding_api.Version 12 | type Transport = encoding_api.Transport 13 | type Teleportation = encoding_api.Teleportation 14 | 15 | type TeleportOption struct { 16 | Name string 17 | Local net.Addr 18 | Secret string 19 | } 20 | 21 | type MeepoSDK interface { 22 | TransportSDK 23 | TeleportationSDK 24 | 25 | Version() (*Version, error) 26 | Ping(peerID string) error 27 | Shutdown() error 28 | Whoami() (string, error) 29 | Teleport(peerID string, remote net.Addr, opt *TeleportOption) (net.Addr, error) 30 | } 31 | 32 | type TransportSDK interface { 33 | NewTransport(peerID string) (*Transport, error) 34 | CloseTransport(peerID string) error 35 | ListTransports() ([]*Transport, error) 36 | GetTransport(peerID string) (*Transport, error) 37 | } 38 | 39 | type NewTeleportationOption struct { 40 | Name string 41 | Source net.Addr 42 | Secret string 43 | } 44 | 45 | type TeleportationSDK interface { 46 | NewTeleportation(peerID string, remote net.Addr, opt *NewTeleportationOption) (*Teleportation, error) 47 | CloseTeleportation(name string) error 48 | ListTeleportations() ([]*Teleportation, error) 49 | GetTeleportation(name string) (*Teleportation, error) 50 | } 51 | 52 | type NewMeepoSDKOption = ofn.OFN 53 | 54 | type NewMeepoSDKFunc func(opts ...NewMeepoSDKOption) (MeepoSDK, error) 55 | 56 | var ( 57 | newMeepoSDKFuncs sync.Map 58 | ) 59 | 60 | func RegisterNewMeepoSDKFunc(name string, fn NewMeepoSDKFunc) { 61 | newMeepoSDKFuncs.Store(name, fn) 62 | } 63 | 64 | func NewMeepoSDK(name string, opts ...NewMeepoSDKOption) (MeepoSDK, error) { 65 | fn, ok := newMeepoSDKFuncs.Load(name) 66 | if !ok { 67 | return nil, UnsupportedMeepoSDKDriverError(name) 68 | } 69 | 70 | return fn.(NewMeepoSDKFunc)(opts...) 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | branchs: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.16 23 | - name: Set up Environment 24 | run: | 25 | echo "GOVERSION=$(go version|awk '{print $3}')" >> $GITHUB_ENV 26 | - name: Set up Snapcraft 27 | # HACK: the mkdirs are a hack for https://bugs.launchpad.net/snapcraft/+bug/1889741 28 | run: | 29 | sudo snap install --classic snapcraft 30 | mkdir -p $HOME/.cache/snapcraft/download 31 | mkdir -p $HOME/.cache/snapcraft/stage-packages 32 | - name: Cache Go modules 33 | uses: actions/cache@v2.1.4 34 | with: 35 | path: ~/go/pkg/mod 36 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 37 | restore-keys: | 38 | ${{ runner.os }}-go- 39 | - name: Snapcraft Login 40 | if: success() && startsWith(github.ref, 'refs/tags/') 41 | env: 42 | SNAPCRAFT_TOKEN: ${{ secrets.SNAPCRAFT_TOKEN }} 43 | run: | 44 | snapcraft login --with <(echo "$SNAPCRAFT_TOKEN") 45 | - name: Install GoReleaser 46 | uses: goreleaser/goreleaser-action@v2 47 | with: 48 | install-only: true 49 | - name: Run GoReleaser 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 53 | run: | 54 | if [[ $GITHUB_REF == refs/tags/* ]] 55 | then 56 | goreleaser release --rm-dist 57 | else 58 | goreleaser release --rm-dist --snapshot 59 | fi 60 | -------------------------------------------------------------------------------- /cmd/teleportation_new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | msdk "github.com/PeerXu/meepo/pkg/sdk" 10 | ) 11 | 12 | var ( 13 | teleportationNewCmd = &cobra.Command{ 14 | Use: "new [-n name] [-l local-address] [-s secret] ", 15 | Short: "New teleportation", 16 | Aliases: []string{"n"}, 17 | RunE: meepoTeleportationNew, 18 | Args: cobra.ExactArgs(2), 19 | } 20 | ) 21 | 22 | func meepoTeleportationNew(cmd *cobra.Command, args []string) error { 23 | var err error 24 | 25 | fs := cmd.Flags() 26 | name, _ := fs.GetString("name") 27 | localAddress, _ := fs.GetString("local-address") 28 | secret, _ := fs.GetString("secret") 29 | 30 | id := args[0] 31 | remoteAddress := args[1] 32 | localNetwork := "tcp" 33 | remoteNetwork := "tcp" 34 | 35 | sdk, err := NewHTTPSDK(cmd) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | opt := &msdk.NewTeleportationOption{} 41 | 42 | remote, err := net.ResolveTCPAddr(remoteNetwork, remoteAddress) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if name != "" { 48 | opt.Name = name 49 | } 50 | 51 | if localAddress != "" { 52 | local, err := net.ResolveTCPAddr(localNetwork, localAddress) 53 | if err != nil { 54 | return err 55 | } 56 | opt.Source = local 57 | } 58 | 59 | if secret != "" { 60 | opt.Secret = secret 61 | } 62 | 63 | _, err = sdk.NewTeleportation(id, remote, opt) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | fmt.Printf("Teleportation creating\n") 69 | 70 | return nil 71 | } 72 | 73 | func init() { 74 | teleportationCmd.AddCommand(teleportationNewCmd) 75 | 76 | teleportationNewCmd.PersistentFlags().StringP("name", "n", "", "Teleportation name") 77 | teleportationNewCmd.PersistentFlags().StringP("local-address", "l", "", "Local listen address, if not set, random port will be listen") 78 | teleportationNewCmd.PersistentFlags().StringP("secret", "s", "", "New teleportation with secret") 79 | } 80 | -------------------------------------------------------------------------------- /cmd/keygen.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "encoding/pem" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/mikesmitty/edkey" 12 | "github.com/mitchellh/go-homedir" 13 | "github.com/spf13/cobra" 14 | 15 | mfs "github.com/PeerXu/meepo/pkg/util/fs" 16 | ) 17 | 18 | var ( 19 | keygenCmd = &cobra.Command{ 20 | Use: "keygen [-t ed25519] [-f identity_file]", 21 | Short: "Generate an identity key", 22 | RunE: meepoKeygen, 23 | } 24 | ) 25 | 26 | func meepoKeygen(cmd *cobra.Command, args []string) error { 27 | fs := cmd.Flags() 28 | 29 | filename, _ := fs.GetString("filename") 30 | overwrite, _ := fs.GetBool("overwrite") 31 | typ, _ := fs.GetString("type") 32 | 33 | _, err := os.Stat(filename) 34 | if err == nil { 35 | if !overwrite { 36 | return os.ErrExist 37 | } 38 | } else if !os.IsNotExist(err) { 39 | return err 40 | } 41 | 42 | if filename, err = homedir.Expand(filename); err != nil { 43 | return err 44 | } 45 | 46 | if err = mfs.EnsureDirectoryExist(filename); err != nil { 47 | return err 48 | } 49 | 50 | switch typ { 51 | case "ed25519": 52 | default: 53 | return fmt.Errorf("Unsupported type: %v", typ) 54 | } 55 | 56 | _, prik, err := ed25519.GenerateKey(rand.Reader) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | buf := pem.EncodeToMemory(&pem.Block{ 62 | Type: "OPENSSH PRIVATE KEY", 63 | Bytes: edkey.MarshalED25519PrivateKey(prik), 64 | }) 65 | 66 | if err = ioutil.WriteFile(filename, buf, 0600); err != nil { 67 | return err 68 | } 69 | 70 | fmt.Println("Key generated!") 71 | 72 | return nil 73 | } 74 | 75 | func init() { 76 | rootCmd.AddCommand(keygenCmd) 77 | 78 | keygenCmd.PersistentFlags().StringP("type", "t", "ed25519", "Specifies the type of key to create") 79 | keygenCmd.PersistentFlags().StringP("filename", "f", "meepo.pem", "Specifies the filename of the key file") 80 | keygenCmd.PersistentFlags().Bool("overwrite", false, "Overwrite an exists key file") 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/http/teleport.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/PeerXu/meepo/pkg/meepo" 10 | ) 11 | 12 | type TeleportRequest struct { 13 | ID string `json:"id" binding:"required"` 14 | Name string `json:"name,omitempty"` 15 | RemoteNetwork string `json:"remoteNetwork"` 16 | RemoteAddress string `json:"remoteAddress" binding:"required"` 17 | LocalNetwork string `json:"localNetwork,omitempty"` 18 | LocalAddress string `json:"localAddress,omitempty"` 19 | Secret string `json:"secret,omitempty"` 20 | } 21 | 22 | type TeleportResponse struct { 23 | LocalNetwork string `json:"localNetwork"` 24 | LocalAddress string `json:"localAddress"` 25 | } 26 | 27 | func (s *HttpServer) Teleport(c *gin.Context) { 28 | var err error 29 | var req TeleportRequest 30 | var local net.Addr 31 | 32 | if err = c.ShouldBindJSON(&req); err != nil { 33 | c.JSON(http.StatusBadRequest, ParseError(err)) 34 | return 35 | } 36 | 37 | remote, err := net.ResolveTCPAddr(req.RemoteNetwork, req.RemoteAddress) 38 | if err != nil { 39 | c.JSON(http.StatusBadRequest, ParseError(err)) 40 | return 41 | } 42 | 43 | var opts []meepo.TeleportOption 44 | 45 | if req.LocalAddress != "" { 46 | if local, err = net.ResolveTCPAddr(req.LocalNetwork, req.LocalAddress); err != nil { 47 | c.JSON(http.StatusBadRequest, ParseError(err)) 48 | return 49 | } 50 | 51 | opts = append(opts, meepo.WithLocalAddress(local)) 52 | } 53 | 54 | if req.Name != "" { 55 | opts = append(opts, meepo.WithName(req.Name)) 56 | } 57 | 58 | if req.Secret != "" { 59 | opts = append(opts, meepo.WithSecret(req.Secret)) 60 | } 61 | 62 | if local, err = s.meepo.Teleport(req.ID, remote, opts...); err != nil { 63 | c.JSON(http.StatusInternalServerError, ParseError(err)) 64 | return 65 | } 66 | 67 | res := &TeleportResponse{ 68 | LocalNetwork: local.Network(), 69 | LocalAddress: local.String(), 70 | } 71 | 72 | c.JSON(http.StatusOK, res) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/meepo/close_transport.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/PeerXu/meepo/pkg/meepo/packet" 9 | "github.com/PeerXu/meepo/pkg/transport" 10 | ) 11 | 12 | const ( 13 | METHOD_CLOSE_TRANSPORT Method = "closeTransport" 14 | ) 15 | 16 | func (mp *Meepo) CloseTransport(peerID string) error { 17 | var err error 18 | 19 | logger := mp.getLogger().WithFields(logrus.Fields{ 20 | "#method": "closeTransport", 21 | "peerID": peerID, 22 | }) 23 | 24 | tp, err := mp.getTransport(peerID) 25 | if err != nil { 26 | logger.WithError(err).Errorf("transport not found") 27 | return err 28 | } 29 | 30 | in := mp.createRequest(peerID, METHOD_CLOSE_TRANSPORT, nil) 31 | out, err := mp.doRequest(in) 32 | if err != nil { 33 | logger.WithError(err).Warningf("failed to do request") 34 | } 35 | 36 | if err = out.Err(); err != nil { 37 | logger.WithError(err).Warningf("failed to close transport by peer") 38 | } 39 | 40 | if err = tp.Close(); err != nil { 41 | logger.WithError(err).Errorf("failed to close transport") 42 | return err 43 | } 44 | 45 | logger.Infof("transport closed") 46 | 47 | return nil 48 | } 49 | 50 | func (mp *Meepo) onCloseTransport(dc transport.DataChannel, in packet.Packet) { 51 | hdr := in.Header() 52 | peerID := hdr.Source() 53 | 54 | logger := mp.getLogger().WithFields(logrus.Fields{ 55 | "#method": "onCloseTransport", 56 | "peerID": peerID, 57 | "session": hdr.Session(), 58 | }) 59 | 60 | tp, err := mp.getTransport(peerID) 61 | if err != nil { 62 | logger.WithError(err).Errorf("transport not found") 63 | mp.sendResponse(dc, mp.createResponseWithError(in, err)) 64 | return 65 | } 66 | 67 | go func() { 68 | // HACK: yield cpu avoid too soon to close transport 69 | time.Sleep(0) 70 | 71 | if err = tp.Close(); err != nil { 72 | logger.WithError(err).Warningf("failed to close transport") 73 | return 74 | } 75 | logger.Tracef("transport closed") 76 | }() 77 | 78 | out := mp.createResponse(in, nil) 79 | mp.sendResponse(dc, out) 80 | 81 | logger.Infof("close transport") 82 | } 83 | -------------------------------------------------------------------------------- /pkg/api/http/new_teleportation.go: -------------------------------------------------------------------------------- 1 | package http_api 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | encoding_api "github.com/PeerXu/meepo/pkg/api/encoding" 10 | "github.com/PeerXu/meepo/pkg/meepo" 11 | "github.com/PeerXu/meepo/pkg/teleportation" 12 | ) 13 | 14 | type NewTeleportationRequest struct { 15 | PeerID string `json:"peerID" binding:"required"` 16 | RemoteNetwork string `json:"remoteNetwork" binding:"required"` 17 | RemoteAddress string `json:"remoteAddress" binding:"required"` 18 | Name string `json:"name,omitempty"` 19 | LocalNetwork string `json:"localNetwork,omitempty"` 20 | LocalAddress string `json:"localAddress,omitempty"` 21 | Secret string `json:"secret,omitempty"` 22 | } 23 | 24 | type NewTeleportationResponse struct { 25 | Teleportation *encoding_api.Teleportation `json:"teleportation"` 26 | } 27 | 28 | func (s *HttpServer) NewTeleportation(c *gin.Context) { 29 | var err error 30 | var req NewTeleportationRequest 31 | var tp teleportation.Teleportation 32 | 33 | if err = c.ShouldBindJSON(&req); err != nil { 34 | c.JSON(http.StatusBadRequest, ParseError(err)) 35 | return 36 | } 37 | 38 | remote, err := net.ResolveTCPAddr(req.RemoteNetwork, req.RemoteAddress) 39 | if err != nil { 40 | c.JSON(http.StatusBadRequest, ParseError(err)) 41 | return 42 | } 43 | 44 | var opts []meepo.NewTeleportationOption 45 | if req.Name != "" { 46 | opts = append(opts, meepo.WithName(req.Name)) 47 | } 48 | 49 | if req.LocalAddress != "" { 50 | local, err := net.ResolveTCPAddr(req.LocalNetwork, req.LocalAddress) 51 | if err != nil { 52 | c.JSON(http.StatusBadRequest, ParseError(err)) 53 | return 54 | } 55 | 56 | opts = append(opts, meepo.WithLocalAddress(local)) 57 | } 58 | 59 | if req.Secret != "" { 60 | opts = append(opts, meepo.WithSecret(req.Secret)) 61 | } 62 | 63 | if tp, err = s.meepo.NewTeleportation(req.PeerID, remote, opts...); err != nil { 64 | c.JSON(http.StatusInternalServerError, ParseError(err)) 65 | return 66 | } 67 | 68 | c.JSON(http.StatusCreated, encoding_api.ConvertTeleportation(tp)) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/transport/webrtc/datachannel.go: -------------------------------------------------------------------------------- 1 | package webrtc_transport 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/pion/datachannel" 8 | "github.com/pion/webrtc/v3" 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/PeerXu/meepo/pkg/transport" 12 | ) 13 | 14 | type WebrtcDataChannel struct { 15 | logger logrus.FieldLogger 16 | 17 | dc *webrtc.DataChannel 18 | ddc datachannel.ReadWriteCloser 19 | 20 | detachOnce sync.Once 21 | opened chan struct{} 22 | 23 | transport *WebrtcTransport 24 | } 25 | 26 | func (wdc *WebrtcDataChannel) getLogger() logrus.FieldLogger { 27 | return wdc.logger.WithFields(logrus.Fields{ 28 | "#instance": "WebrtcDataChannel", 29 | "label": wdc.Label(), 30 | }) 31 | } 32 | 33 | func (wdc *WebrtcDataChannel) waitOpened() error { 34 | select { 35 | case <-wdc.opened: 36 | return nil 37 | case <-time.After(5 * time.Second): 38 | return WaitDataChannelOpenedTimeoutError 39 | } 40 | } 41 | 42 | func (wdc *WebrtcDataChannel) Transport() transport.Transport { 43 | return wdc.transport 44 | } 45 | 46 | func (wdc *WebrtcDataChannel) Label() string { 47 | return wdc.dc.Label() 48 | } 49 | 50 | func (wdc *WebrtcDataChannel) State() transport.DataChannelState { 51 | return transport.DataChannelState(wdc.dc.ReadyState()) 52 | } 53 | 54 | func (wdc *WebrtcDataChannel) OnOpen(f func()) { 55 | wdc.dc.OnOpen(func() { 56 | wdc.detachOnce.Do(func() { 57 | wdc.ddc, _ = wdc.dc.Detach() 58 | close(wdc.opened) 59 | }) 60 | f() 61 | }) 62 | } 63 | 64 | func (wdc *WebrtcDataChannel) Read(p []byte) (int, error) { 65 | if err := wdc.waitOpened(); err != nil { 66 | return 0, err 67 | } 68 | 69 | return wdc.ddc.Read(p) 70 | } 71 | 72 | func (wdc *WebrtcDataChannel) Write(p []byte) (int, error) { 73 | if err := wdc.waitOpened(); err != nil { 74 | return 0, err 75 | } 76 | 77 | return wdc.ddc.Write(p) 78 | } 79 | 80 | func (wdc *WebrtcDataChannel) Close() error { 81 | return wdc.dc.Close() 82 | } 83 | 84 | func NewWebrtcDataChannel(logger logrus.FieldLogger, dc *webrtc.DataChannel, tp *WebrtcTransport) *WebrtcDataChannel { 85 | wdc := &WebrtcDataChannel{ 86 | logger: logger, 87 | dc: dc, 88 | opened: make(chan struct{}), 89 | transport: tp, 90 | } 91 | 92 | return wdc 93 | } 94 | -------------------------------------------------------------------------------- /cmd/ncat.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/spf13/cobra" 12 | xproxy "golang.org/x/net/proxy" 13 | ) 14 | 15 | var ( 16 | ncatCmd = &cobra.Command{ 17 | Use: "ncat", 18 | Short: "ncat-like program, support SOCKS5 with authentication", 19 | Example: "ncat [--proxy :] [--proxy-type ] [--proxy-auth ] ", 20 | RunE: meepoNcat, 21 | Args: cobra.ExactArgs(2), 22 | } 23 | ) 24 | 25 | func meepoNcat(cmd *cobra.Command, args []string) error { 26 | var socks5Auth *xproxy.Auth 27 | var err error 28 | 29 | if len(args) != 2 { 30 | return fmt.Errorf("require hostname and port") 31 | } 32 | 33 | fs := cmd.Flags() 34 | hostname := args[0] 35 | port := args[1] 36 | proxy, _ := fs.GetString("proxy") 37 | proxyType, _ := fs.GetString("proxy-type") 38 | proxyAuth, _ := fs.GetString("proxy-auth") 39 | 40 | if proxyType != "socks5" { 41 | return fmt.Errorf("unsupported proxy type: %s", proxyType) 42 | } 43 | 44 | if proxyAuth != "" { 45 | ss := strings.SplitN(proxyAuth, ":", 2) 46 | if len(ss) != 2 { 47 | return fmt.Errorf("invalid proxy authentication") 48 | } 49 | 50 | socks5Auth = &xproxy.Auth{ 51 | User: ss[0], 52 | Password: ss[1], 53 | } 54 | } 55 | 56 | dialer, err := xproxy.SOCKS5("tcp", proxy, socks5Auth, xproxy.Direct) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | conn, err := dialer.Dial("tcp", net.JoinHostPort(hostname, port)) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | done := make(chan struct{}) 67 | var doneOnce sync.Once 68 | go func() { 69 | defer doneOnce.Do(func() { close(done) }) 70 | io.Copy(conn, os.Stdin) 71 | 72 | }() 73 | go func() { 74 | defer doneOnce.Do(func() { close(done) }) 75 | io.Copy(os.Stdout, conn) 76 | }() 77 | 78 | <-done 79 | 80 | return nil 81 | } 82 | 83 | func init() { 84 | rootCmd.AddCommand(ncatCmd) 85 | 86 | ncatCmd.PersistentFlags().String("proxy", "127.0.0.1:12341", "Specify address of host to proxy through") 87 | ncatCmd.PersistentFlags().String("proxy-type", "socks5", "Specify proxy type (\"socks5\")") 88 | ncatCmd.PersistentFlags().String("proxy-auth", "", "Authenticate with SOCKS proxy server") 89 | } 90 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - 386 15 | - amd64 16 | - arm 17 | - arm64 18 | mod_timestamp: '{{ .CommitTimestamp }}' 19 | flags: 20 | - -trimpath 21 | ldflags: 22 | - -s -w 23 | - -X "github.com/PeerXu/meepo/pkg/util/version.Version={{ .Version }}" 24 | - -X "github.com/PeerXu/meepo/pkg/util/version.GoVersion={{ .Env.GOVERSION }}" 25 | - -X "github.com/PeerXu/meepo/pkg/util/version.GitHash={{ .Commit }}" 26 | - -X "github.com/PeerXu/meepo/pkg/util/version.Built={{ .CommitDate }}" 27 | checksum: 28 | name_template: 'checksums.txt' 29 | archives: 30 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 31 | format_overrides: 32 | - goos: windows 33 | format: zip 34 | files: 35 | - none* 36 | release: 37 | # Same as for github 38 | # Note: it can only be one: either github, gitlab or gitea 39 | github: 40 | owner: PeerXu 41 | name: meepo 42 | draft: true 43 | snapcrafts: 44 | - name: meepo 45 | name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 46 | summary: Connect to your service without public IP in p2p channel 47 | description: | 48 | Meepo help you to build your private network over Internet in easy way. 49 | grade: stable 50 | confinement: strict 51 | publish: true 52 | extra_files: 53 | - source: bin/meepod.wrapper 54 | destination: meepod.wrapper 55 | mode: 0755 56 | layout: 57 | /etc/meepo: 58 | bind: $SNAP_DATA/etc 59 | apps: 60 | meepod: 61 | daemon: simple 62 | command: meepod.wrapper 63 | plugs: 64 | - network 65 | - network-bind 66 | meepo: 67 | command: meepo 68 | plugs: 69 | - network 70 | - network-bind 71 | brews: 72 | - description: | 73 | Meepo help you to build your private network over Internet in easy way. 74 | tap: 75 | owner: PeerXu 76 | name: homebrew-tap 77 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 78 | folder: Formula 79 | homepage: https://github.com/PeerXu/meepo 80 | license: MIT 81 | test: | 82 | system "#{bin}/meepo version" 83 | install: | 84 | bin.install "meepo" 85 | -------------------------------------------------------------------------------- /pkg/teleportation/option.go: -------------------------------------------------------------------------------- 1 | package teleportation 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/PeerXu/meepo/pkg/ofn" 9 | "github.com/PeerXu/meepo/pkg/transport" 10 | ) 11 | 12 | type NewTeleportationSourceOption = ofn.OFN 13 | type NewTeleportationSinkOption = ofn.OFN 14 | 15 | func WithLogger(logger logrus.FieldLogger) ofn.OFN { 16 | return func(o ofn.Option) { 17 | o["logger"] = logger 18 | } 19 | } 20 | 21 | func WithName(name string) ofn.OFN { 22 | return func(o ofn.Option) { 23 | o["name"] = name 24 | } 25 | } 26 | 27 | func WithSource(addr net.Addr) ofn.OFN { 28 | return func(o ofn.Option) { 29 | o["source"] = addr 30 | } 31 | } 32 | 33 | func WithSink(addr net.Addr) ofn.OFN { 34 | return func(o ofn.Option) { 35 | o["sink"] = addr 36 | } 37 | } 38 | 39 | func WithTransport(t transport.Transport) ofn.OFN { 40 | return func(o ofn.Option) { 41 | o["transport"] = t 42 | } 43 | } 44 | 45 | type NewDial func(network, address string) (net.Conn, error) 46 | 47 | func WithNewDial(f NewDial) ofn.OFN { 48 | return func(o ofn.Option) { 49 | o["newDial"] = f 50 | } 51 | } 52 | 53 | type DoTeleportFunc func(label string) error 54 | 55 | func WithDoTeleportFunc(f DoTeleportFunc) ofn.OFN { 56 | return func(o ofn.Option) { 57 | o["doTeleportFunc"] = f 58 | } 59 | } 60 | 61 | type OnDoTeleportFunc func() error 62 | 63 | func WithOnDoTeleportFunc(f OnDoTeleportFunc) ofn.OFN { 64 | return func(o ofn.Option) { 65 | o["onDoTeleportFunc"] = f 66 | } 67 | } 68 | 69 | type OnCloseHandler func() 70 | 71 | func WithOnCloseHandler(h OnCloseHandler) ofn.OFN { 72 | return func(o ofn.Option) { 73 | o["onCloseHandler"] = h 74 | } 75 | } 76 | 77 | type OnErrorHandler func(error) 78 | 79 | func WithOnErrorHandler(h OnErrorHandler) ofn.OFN { 80 | return func(o ofn.Option) { 81 | o["onErrorHandler"] = h 82 | } 83 | } 84 | 85 | type DialRequest struct { 86 | Conn net.Conn 87 | Quit chan struct{} 88 | } 89 | 90 | func NewDialRequest(conn net.Conn) *DialRequest { 91 | return &DialRequest{Conn: conn} 92 | } 93 | 94 | func NewDialRequestWithQuit(conn net.Conn, quit chan struct{}) *DialRequest { 95 | return &DialRequest{Conn: conn, Quit: quit} 96 | } 97 | 98 | func SetDialRequestChannel(c chan *DialRequest) ofn.OFN { 99 | return func(o ofn.Option) { 100 | o["dialRequestChannel"] = c 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/meepo/teleport.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | 8 | "github.com/PeerXu/meepo/pkg/ofn" 9 | "github.com/PeerXu/meepo/pkg/teleportation" 10 | "github.com/PeerXu/meepo/pkg/transport" 11 | ) 12 | 13 | func newTeleportOptions() ofn.Option { 14 | return ofn.NewOption(map[string]interface{}{}) 15 | } 16 | 17 | func (mp *Meepo) Teleport(peerID string, remote net.Addr, opts ...TeleportOption) (net.Addr, error) { 18 | var local net.Addr 19 | var name string 20 | var secret string 21 | var tp transport.Transport 22 | var err error 23 | var ok bool 24 | 25 | o := newTeleportOptions() 26 | 27 | for _, opt := range opts { 28 | opt(o) 29 | } 30 | 31 | tp, err = mp.getTransport(peerID) 32 | if err != nil { 33 | if !errors.Is(err, ErrTransportNotExist) { 34 | return nil, err 35 | } 36 | 37 | done := make(chan struct{}) 38 | var doneOnce sync.Once 39 | tp, err = mp.NewTransport(peerID) 40 | if err != nil { 41 | return nil, err 42 | } 43 | fn := func(transport.HandleID) { 44 | doneOnce.Do(func() { close(done) }) 45 | } 46 | h1 := tp.OnTransportState(transport.TransportStateConnected, fn) 47 | defer tp.UnsetOnTransportState(transport.TransportStateConnected, h1) 48 | h2 := tp.OnTransportState(transport.TransportStateFailed, fn) 49 | defer tp.UnsetOnTransportState(transport.TransportStateFailed, h2) 50 | h3 := tp.OnTransportState(transport.TransportStateClosed, fn) 51 | defer tp.UnsetOnTransportState(transport.TransportStateClosed, h3) 52 | 53 | <-done 54 | } 55 | 56 | tss, err := mp.listTeleportationsByPeerID(peerID) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | for _, ts := range tss { 62 | sink := ts.Sink() 63 | if ts.Portal() == teleportation.PortalSource && 64 | sink.Network() == remote.Network() && 65 | sink.String() == remote.String() { 66 | return ts.Source(), nil 67 | } 68 | } 69 | 70 | var ntOpts []NewTeleportationOption 71 | if local, ok = o.Get("local").Inter().(net.Addr); ok { 72 | ntOpts = append(ntOpts, WithLocalAddress(local)) 73 | } 74 | 75 | if name, ok = o.Get("name").Inter().(string); ok { 76 | ntOpts = append(ntOpts, WithName(name)) 77 | } 78 | 79 | if secret, ok = o.Get("secret").Inter().(string); ok { 80 | ntOpts = append(ntOpts, WithSecret(secret)) 81 | } 82 | 83 | ts, err := mp.NewTeleportation(peerID, remote, ntOpts...) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return ts.Source(), nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/meepo/error.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | ErrInvalidPeerID = fmt.Errorf("invalid peer id") 9 | ErrTransportExist = fmt.Errorf("transport exists") 10 | ErrTeleportationNotExist = fmt.Errorf("teleportation not exists") 11 | ErrWaitResponseTimeout = fmt.Errorf("wait response timeout") 12 | ErrNotWirable = fmt.Errorf("not wirable") 13 | ErrUnsupportedSocks5Command = fmt.Errorf("unsupported socks5 command") 14 | ErrNetworkUnreachable = fmt.Errorf("network unreachable") 15 | ErrUnsupportedNetworkType = fmt.Errorf("unsupported network type") 16 | ErrNotBroadcastPacket = fmt.Errorf("not braoadcast packet") 17 | ErrOutOfEdge = fmt.Errorf("out of edge") 18 | ErrTransportNotExist = fmt.Errorf("transport not exists") 19 | ErrUnexpectedMessage = fmt.Errorf("unexpected message") 20 | ErrUnsupportedMethod = fmt.Errorf("unsupported method") 21 | ErrUnexpectedType = fmt.Errorf("unexpected type") 22 | ErrNotFound = fmt.Errorf("not found") 23 | ErrUnauthenticated = fmt.Errorf("unauthenticated") 24 | ErrUnauthorized = fmt.Errorf("unauthorized") 25 | ErrIncorrectSignature = fmt.Errorf("incorrect signature") 26 | ErrIncorrectPassword = fmt.Errorf("incorrect password") 27 | ErrAclNotAllowed = fmt.Errorf("acl: not allowed") 28 | ErrInvalidAclPolicyString = fmt.Errorf("invalid acl policy string") 29 | ) 30 | 31 | func SessionChannelExistError(session int32) error { 32 | return fmt.Errorf("SessionChannel: %d exist", session) 33 | } 34 | 35 | func SessionChannelNotExistError(session int32) error { 36 | return fmt.Errorf("SessionChannel: %d not exist", session) 37 | } 38 | 39 | func SessionChannelClosedError(session int32) error { 40 | return fmt.Errorf("SessionChannel: %d closed", session) 41 | } 42 | 43 | func UnsupportedMessageDecodeDriverError(messageIdentifier string) error { 44 | return fmt.Errorf("Unsupported message decode driver: %s", messageIdentifier) 45 | } 46 | 47 | type sendMessageError struct { 48 | err error 49 | } 50 | 51 | func (t sendMessageError) Error() string { 52 | return t.err.Error() 53 | } 54 | 55 | func SendMessageError(err error) sendMessageError { 56 | return SendMessageError(err) 57 | } 58 | 59 | type errSendPacket struct { 60 | err error 61 | } 62 | 63 | func (t errSendPacket) Error() string { 64 | return t.err.Error() 65 | } 66 | 67 | func ErrSendPacket(err error) errSendPacket { 68 | return ErrSendPacket(err) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/meepo/close_teleportation.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "github.com/PeerXu/meepo/pkg/meepo/packet" 5 | "github.com/PeerXu/meepo/pkg/transport" 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | const ( 10 | METHOD_CLOSE_TELEPORTATION Method = "closeTeleportation" 11 | ) 12 | 13 | type ( 14 | CloseTeleportationRequest struct { 15 | Name string 16 | } 17 | 18 | CloseTeleportationResponse struct{} 19 | ) 20 | 21 | func (mp *Meepo) CloseTeleportation(name string) error { 22 | var err error 23 | 24 | logger := mp.getLogger().WithFields(logrus.Fields{ 25 | "#method": "CloseTeleportation", 26 | "name": name, 27 | }) 28 | 29 | tp, err := mp.GetTeleportation(name, WithSourceFirst()) 30 | if err != nil { 31 | logger.WithError(err).Errorf("failed to get teleportation") 32 | return err 33 | } 34 | 35 | in := mp.createRequest(tp.Transport().PeerID(), METHOD_CLOSE_TELEPORTATION, &CloseTeleportationRequest{Name: name}) 36 | 37 | out, err := mp.doRequest(in) 38 | if err != nil { 39 | logger.WithError(err).Errorf("failed to do request") 40 | return err 41 | } 42 | 43 | if err = out.Err(); err != nil { 44 | logger.WithError(err).Errorf("failed to close teleportation by peer") 45 | return err 46 | } 47 | 48 | go func() { 49 | if err = tp.Close(); err != nil { 50 | logger.WithError(err).Errorf("failed to close teleportation") 51 | return 52 | } 53 | logger.Infof("teleportation closed") 54 | }() 55 | 56 | logger.Debugf("teleportation closing") 57 | 58 | return nil 59 | } 60 | 61 | func (mp *Meepo) onCloseTeleportation(dc transport.DataChannel, in packet.Packet) { 62 | var err error 63 | var req CloseTeleportationRequest 64 | 65 | logger := mp.getLogger().WithFields(logrus.Fields{ 66 | "#method": "onCloseTeleportation", 67 | }) 68 | 69 | if err = in.Data(&req); err != nil { 70 | logger.WithError(err).Errorf("failed to unmarshal request data") 71 | mp.sendResponse(dc, mp.createResponseWithError(in, err)) 72 | return 73 | } 74 | 75 | logger = logger.WithField("name", req.Name) 76 | 77 | ts, err := mp.GetTeleportation(req.Name, WithSinkFirst()) 78 | if err != nil { 79 | logger.WithError(err).Errorf("failed to get teleportation") 80 | mp.sendResponse(dc, mp.createResponseWithError(in, err)) 81 | return 82 | } 83 | 84 | go func() { 85 | if err = ts.Close(); err != nil { 86 | logger.WithError(err).Errorf("failed to close teleportation") 87 | return 88 | } 89 | logger.Infof("teleportation closed") 90 | }() 91 | 92 | mp.sendResponse(dc, mp.createResponse(in, &CloseTeleportationResponse{})) 93 | logger.Debugf("teleportation closing") 94 | } 95 | -------------------------------------------------------------------------------- /pkg/meepo/packet/broadcast_packet.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/PeerXu/meepo/pkg/ofn" 7 | ) 8 | 9 | type BroadcastPacket interface { 10 | Header() BroadcastHeader 11 | Err() error 12 | Packet() Packet 13 | SetPacket(Packet) BroadcastPacket 14 | } 15 | 16 | type broadcastPacket struct { 17 | header BroadcastHeader 18 | packet Packet 19 | } 20 | 21 | func (p *broadcastPacket) Header() BroadcastHeader { 22 | return p.header 23 | } 24 | 25 | func (p *broadcastPacket) Err() error { 26 | if p.packet == nil { 27 | return ErrPacketIsNil 28 | } 29 | 30 | return p.packet.Err() 31 | } 32 | 33 | func (p *broadcastPacket) Packet() Packet { 34 | return p.packet 35 | } 36 | 37 | func (p *broadcastPacket) SetPacket(t Packet) BroadcastPacket { 38 | pp := *p 39 | pp.packet = t 40 | return &pp 41 | } 42 | 43 | type NewBroadcastPacketOption = ofn.OFN 44 | 45 | func WithPacket(p Packet) ofn.OFN { 46 | return func(o ofn.Option) { 47 | o["packet"] = p 48 | } 49 | } 50 | 51 | func NewBroadcastPacket(h BroadcastHeader, opts ...NewBroadcastPacketOption) (BroadcastPacket, error) { 52 | o := ofn.NewOption(map[string]interface{}{}) 53 | for _, opt := range opts { 54 | opt(o) 55 | } 56 | 57 | p, ok := o.Get("packet").Inter().(Packet) 58 | if !ok { 59 | return nil, ErrPacketIsNil 60 | } 61 | 62 | return &broadcastPacket{ 63 | header: h, 64 | packet: p, 65 | }, nil 66 | } 67 | 68 | type _broadcastPacket struct { 69 | Hop int32 70 | Packet string 71 | } 72 | 73 | func PacketToBroadcastPacketE(p Packet) (bp BroadcastPacket, err error) { 74 | var t _broadcastPacket 75 | if err = p.Data(&t); err != nil { 76 | return 77 | } 78 | 79 | pbuf, err := base64.StdEncoding.DecodeString(t.Packet) 80 | if err != nil { 81 | return 82 | } 83 | 84 | np, err := UnmarshalPacket(pbuf) 85 | if err != nil { 86 | return 87 | } 88 | 89 | bhdr := NewBroadcastHeader( 90 | p.Header().Session(), 91 | p.Header().Source(), 92 | p.Header().Destination(), 93 | p.Header().Type(), 94 | p.Header().Method(), 95 | t.Hop, 96 | ) 97 | 98 | return NewBroadcastPacket(bhdr, WithPacket(np)) 99 | } 100 | 101 | func BroadcastPacketToPacketE(bp BroadcastPacket) (p Packet, err error) { 102 | var pbuf []byte 103 | 104 | bhdr := bp.Header() 105 | 106 | pbuf, err = MarshalPacket(bp.Packet()) 107 | if err != nil { 108 | return 109 | } 110 | 111 | hdr := NewHeader(bhdr.Session(), bhdr.Source(), bhdr.Destination(), bhdr.Type(), bhdr.Method()) 112 | p, err = NewPacket(hdr, WithData(&_broadcastPacket{ 113 | Hop: bhdr.Hop(), 114 | Packet: base64.StdEncoding.EncodeToString(pbuf), 115 | })) 116 | 117 | return 118 | } 119 | 120 | func BroadcastPacketToPacket(bp BroadcastPacket) (p Packet) { 121 | p, _ = BroadcastPacketToPacketE(bp) 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /pkg/meepo/packet/header.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/spf13/cast" 7 | 8 | "github.com/PeerXu/meepo/pkg/util/msgpack" 9 | ) 10 | 11 | type Header interface { 12 | Session() int32 13 | Source() string 14 | Destination() string 15 | Type() Type 16 | Method() string 17 | Signature() []byte 18 | 19 | SetSignature([]byte) Header 20 | UnsetSignature() Header 21 | 22 | msgpack.Marshaler 23 | msgpack.Unmarshaler 24 | } 25 | 26 | func NewHeader(sess int32, src, dst string, typ Type, meth string) Header { 27 | return &header{ 28 | session: sess, 29 | source: src, 30 | destination: dst, 31 | typ: typ, 32 | method: meth, 33 | } 34 | } 35 | 36 | type header struct { 37 | session int32 38 | source string 39 | destination string 40 | typ Type 41 | method string 42 | signature []byte 43 | } 44 | 45 | func (h *header) Session() int32 { 46 | return h.session 47 | } 48 | 49 | func (h *header) Source() string { 50 | return h.source 51 | } 52 | 53 | func (h *header) Destination() string { 54 | return h.destination 55 | } 56 | 57 | func (h *header) Type() Type { 58 | return h.typ 59 | } 60 | 61 | func (h *header) Method() string { 62 | return h.method 63 | } 64 | 65 | func (h *header) Signature() []byte { 66 | return h.signature 67 | } 68 | 69 | var _ msgpack.Marshaler = (*header)(nil) 70 | 71 | func (h *header) MarshalMsgpack() ([]byte, error) { 72 | return msgpack.Marshal(map[string]interface{}{ 73 | "session": h.session, 74 | "source": h.source, 75 | "destination": h.destination, 76 | "type": h.typ, 77 | "method": h.method, 78 | "signature": base64.StdEncoding.EncodeToString(h.signature), 79 | }) 80 | } 81 | 82 | var _ msgpack.Unmarshaler = (*header)(nil) 83 | 84 | func (h *header) UnmarshalMsgpack(b []byte) error { 85 | var m map[string]interface{} 86 | if err := msgpack.Unmarshal(b, &m); err != nil { 87 | return err 88 | } 89 | 90 | h.session = cast.ToInt32(m["session"]) 91 | h.source = cast.ToString(m["source"]) 92 | h.destination = cast.ToString(m["destination"]) 93 | h.typ = Type(cast.ToString(m["type"])) 94 | h.method = cast.ToString(m["method"]) 95 | h.signature, _ = base64.StdEncoding.DecodeString(cast.ToString(m["signature"])) 96 | 97 | return nil 98 | } 99 | 100 | func (h *header) SetSignature(b []byte) Header { 101 | hh := *h 102 | hh.signature = b 103 | return &hh 104 | } 105 | 106 | func (h *header) UnsetSignature() Header { 107 | hh := *h 108 | hh.signature = nil 109 | return &hh 110 | } 111 | 112 | func MarshalHeader(h Header) ([]byte, error) { 113 | return msgpack.Marshal(h) 114 | } 115 | 116 | func UnmarshalHeader(b []byte) (Header, error) { 117 | var h header 118 | if err := msgpack.Unmarshal(b, &h); err != nil { 119 | return nil, err 120 | } 121 | return &h, nil 122 | } 123 | 124 | func InvertHeader(in Header) Header { 125 | var typ Type 126 | switch in.Type() { 127 | case Request: 128 | typ = Response 129 | case Response: 130 | typ = Request 131 | case BroadcastRequest: 132 | typ = BroadcastResponse 133 | case BroadcastResponse: 134 | typ = BroadcastRequest 135 | } 136 | 137 | return &header{ 138 | session: in.Session(), 139 | source: in.Destination(), 140 | destination: in.Source(), 141 | typ: typ, 142 | method: in.Method(), 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /cmd/ssh.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | msdk "github.com/PeerXu/meepo/pkg/sdk" 11 | ) 12 | 13 | var ( 14 | sshCmd = &cobra.Command{ 15 | Use: "ssh", 16 | Short: "Run a ssh proxy to other Meepo ssh server", 17 | Example: "eval $(meepo ssh -- -p ... @)", 18 | RunE: meepoSsh, 19 | } 20 | ) 21 | 22 | func splitUsernameHost(s string) (string, string, error) { 23 | if !strings.ContainsRune(s, '@') { 24 | return "", s, nil 25 | } 26 | 27 | ss := strings.SplitN(s, "@", 2) 28 | if len(ss) > 2 { 29 | return "", "", fmt.Errorf("bad destination") 30 | } 31 | 32 | return ss[0], ss[1], nil 33 | } 34 | 35 | // meepo ssh -- -p ... @ 36 | func meepoSsh(cmd *cobra.Command, args []string) error { 37 | var err error 38 | 39 | sdk, err := NewHTTPSDK(cmd) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | fs := cmd.Flags() 45 | binary, _ := fs.GetString("binary") 46 | id, _ := fs.GetString("id") 47 | username, _ := fs.GetString("username") 48 | la, _ := fs.GetString("laddr") 49 | ra, _ := fs.GetString("raddr") 50 | rh, rp, _ := net.SplitHostPort(ra) 51 | strict, _ := fs.GetBool("strict") 52 | secret, _ := fs.GetString("secret") 53 | 54 | if len(args) > 0 { 55 | for i := 0; i < len(args); i++ { 56 | if len(args[i]) >= 2 && args[i][:2] == "-p" { 57 | if len(args[i]) > 3 { 58 | if args[i][2] == '=' { 59 | rp = args[i][3:] 60 | } else { 61 | rp = args[i][2:] 62 | } 63 | } else { 64 | rp = args[i+1] 65 | args[i+1] = "" 66 | } 67 | args[i] = "" 68 | break 69 | } 70 | } 71 | username, id, _ = splitUsernameHost(args[len(args)-1]) 72 | } 73 | ra = net.JoinHostPort(rh, rp) 74 | 75 | remote := MustResolveTCPAddr("", ra) 76 | tpOpt := &msdk.TeleportOption{ 77 | Name: fmt.Sprintf("ssh:%s:%s", id, rp), 78 | } 79 | if la != "" { 80 | tpOpt.Local = MustResolveTCPAddr("", la) 81 | } 82 | if secret != "" { 83 | tpOpt.Secret = secret 84 | } 85 | local, err := sdk.Teleport(id, remote, tpOpt) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | lh, lp, _ := net.SplitHostPort(local.String()) 91 | if username != "" { 92 | username += "@" 93 | } 94 | buf := []string{binary, fmt.Sprintf("-p%s", lp)} 95 | if !strict { 96 | buf = append(buf, "-o 'StrictHostKeyChecking=no'", "-o 'UserKnownHostsFile=/dev/null'") 97 | } 98 | 99 | if len(args) > 1 { 100 | buf = append(buf, args[:len(args)-2]...) 101 | } 102 | buf = append(buf, fmt.Sprintf("%s%s", username, lh)) 103 | sshStr := strings.Join(buf, " ") 104 | 105 | fmt.Printf("%s\n", sshStr) 106 | 107 | return nil 108 | } 109 | 110 | func init() { 111 | rootCmd.AddCommand(sshCmd) 112 | 113 | sshCmd.PersistentFlags().String("binary", "ssh", "Binary of ssh") 114 | sshCmd.PersistentFlags().String("id", "", "Meepo ID") 115 | sshCmd.PersistentFlags().String("username", "", "Username for ssh") 116 | sshCmd.PersistentFlags().String("laddr", "127.0.0.1:0", "Local listen address, if not set, random port will be listen") 117 | sshCmd.PersistentFlags().String("raddr", "127.0.0.1:22", "Remote ssh server address, port overwrite by rest option(-p)") 118 | sshCmd.PersistentFlags().Bool("strict", false, "Check host key and write to known hosts file") 119 | sshCmd.PersistentFlags().String("secret", "", "New teleportation with secret") 120 | } 121 | -------------------------------------------------------------------------------- /pkg/meepo/wire.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/PeerXu/meepo/pkg/meepo/packet" 7 | "github.com/PeerXu/meepo/pkg/signaling" 8 | "github.com/PeerXu/meepo/pkg/transport" 9 | mgroup "github.com/PeerXu/meepo/pkg/util/group" 10 | ) 11 | 12 | const ( 13 | METHOD_WIRE Method = "wire" 14 | ) 15 | 16 | type ( 17 | WireRequest struct { 18 | Descriptor *signaling.Descriptor 19 | } 20 | 21 | WireResponse struct { 22 | Descriptor *signaling.Descriptor 23 | } 24 | ) 25 | 26 | func (mp *Meepo) Wire(peerID string, src *signaling.Descriptor) (*signaling.Descriptor, error) { 27 | var res WireResponse 28 | 29 | in := mp.createRequest(peerID, METHOD_WIRE, &WireRequest{Descriptor: src}) 30 | 31 | logger := mp.getLogger().WithFields(logrus.Fields{ 32 | "#method": "Wire", 33 | "session": in.Header().Session(), 34 | }) 35 | 36 | out, err := mp.doBroadcastRequest(in, &doBroadcastRequestOption{ 37 | NewGroupFunc: mgroup.NewAnyGroupFunc, 38 | }) 39 | if err != nil { 40 | logger.WithError(err).Debugf("failed to do broadcast request") 41 | return nil, err 42 | } 43 | 44 | if err = out.Err(); err != nil { 45 | logger.WithError(err).Debugf("failed to wire") 46 | return nil, err 47 | } 48 | 49 | if err = out.Data(&res); err != nil { 50 | logger.WithError(err).Debugf("failed to unmarshal response data") 51 | return nil, err 52 | } 53 | 54 | logger.Infof("wire") 55 | 56 | return res.Descriptor, nil 57 | } 58 | 59 | func (mp *Meepo) SetWireHandler(h signaling.WireHandler) { 60 | mp.wireHandlerMtx.Lock() 61 | mp.wireHandler = h 62 | mp.wireHandlerMtx.Unlock() 63 | } 64 | 65 | func (mp *Meepo) onWire(dc transport.DataChannel, bin packet.BroadcastPacket) { 66 | var req WireRequest 67 | var err error 68 | 69 | hdr := bin.Header() 70 | logger := mp.getLogger().WithFields(logrus.Fields{ 71 | "#method": "onWire", 72 | "source": hdr.Source(), 73 | "session": hdr.Session(), 74 | }) 75 | 76 | if err = bin.Packet().Data(&req); err != nil { 77 | logger.WithError(err).Debugf("failed to unmarshal broadcast request data") 78 | mp.sendBroadcastResponse(dc, mp.createBroadcastResponseWithError(bin, err)) 79 | return 80 | } 81 | 82 | mp.wireHandlerMtx.Lock() 83 | handler := mp.wireHandler 84 | mp.wireHandlerMtx.Unlock() 85 | 86 | if handler == nil { 87 | err = ErrNotWirable 88 | mp.sendBroadcastResponse(dc, mp.createBroadcastResponseWithError(bin, err)) 89 | logger.WithError(err).Debugf("not wirable") 90 | return 91 | } 92 | 93 | desc, err := handler(req.Descriptor) 94 | if err != nil { 95 | mp.sendBroadcastResponse(dc, mp.createBroadcastResponseWithError(bin, err)) 96 | logger.WithError(err).Debugf("failed to handle wire") 97 | return 98 | } 99 | logger.Tracef("handle wire") 100 | 101 | out := mp.createResponse(bin.Packet(), &WireResponse{Descriptor: desc}) 102 | bout := mp.createBroadcastResponse(bin, out) 103 | mp.sendBroadcastResponse(dc, bout) 104 | 105 | logger.Tracef("done") 106 | } 107 | 108 | type SignalingEngineWrapper struct { 109 | meepo *Meepo 110 | } 111 | 112 | func (e *SignalingEngineWrapper) Wire(dst, src *signaling.Descriptor) (*signaling.Descriptor, error) { 113 | return e.meepo.Wire(dst.ID, src) 114 | } 115 | 116 | func (e *SignalingEngineWrapper) OnWire(h signaling.WireHandler) { 117 | e.meepo.SetWireHandler(h) 118 | } 119 | 120 | func (e *SignalingEngineWrapper) Close() error { 121 | e.meepo.SetWireHandler(nil) 122 | 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /pkg/meepo/packet/packet.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/spf13/cast" 8 | 9 | "github.com/PeerXu/meepo/pkg/ofn" 10 | "github.com/PeerXu/meepo/pkg/util/msgpack" 11 | ) 12 | 13 | type Packet interface { 14 | Header() Header 15 | Err() error 16 | Data(out interface{}) error 17 | SetSignature([]byte) Packet 18 | UnsetSignature() Packet 19 | 20 | msgpack.Marshaler 21 | msgpack.Unmarshaler 22 | } 23 | 24 | type NewPacketOption = ofn.OFN 25 | 26 | func WithError(err error) ofn.OFN { 27 | return func(o ofn.Option) { 28 | o["error"] = err 29 | } 30 | } 31 | 32 | func WithData(v interface{}) ofn.OFN { 33 | return func(o ofn.Option) { 34 | o["data"] = v 35 | } 36 | } 37 | 38 | func NewPacket(h Header, opts ...NewPacketOption) (Packet, error) { 39 | p := &packet{ 40 | header: h, 41 | } 42 | 43 | o := ofn.NewOption(map[string]interface{}{}) 44 | 45 | for _, opt := range opts { 46 | opt(o) 47 | } 48 | 49 | if v := o.Get("error").Inter(); v != nil { 50 | p.err = v.(error) 51 | return p, nil 52 | } 53 | 54 | if v := o.Get("data").Inter(); v != nil { 55 | raw, err := msgpack.Marshal(v) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | p.rawData = raw 61 | return p, nil 62 | } 63 | 64 | return p, nil 65 | } 66 | 67 | type packet struct { 68 | header Header 69 | err error 70 | rawData []byte 71 | } 72 | 73 | func (p *packet) Header() Header { 74 | return p.header 75 | } 76 | 77 | func (p *packet) Err() error { 78 | return p.err 79 | } 80 | 81 | func (p *packet) Data(out interface{}) error { 82 | return msgpack.Unmarshal(p.rawData, &out) 83 | } 84 | 85 | func (p *packet) SetSignature(b []byte) Packet { 86 | pp := *p 87 | pp.header = p.Header().SetSignature(b) 88 | return &pp 89 | } 90 | 91 | func (p *packet) UnsetSignature() Packet { 92 | pp := *p 93 | pp.header = p.Header().UnsetSignature() 94 | return &pp 95 | } 96 | 97 | var _ msgpack.Marshaler = (*packet)(nil) 98 | 99 | func (p *packet) MarshalMsgpack() ([]byte, error) { 100 | bHdr, err := msgpack.Marshal(p.header) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | m := map[string]interface{}{ 106 | "header": bHdr, 107 | } 108 | 109 | if p.err != nil { 110 | m["error"] = p.err.Error() 111 | } else { 112 | m["data"] = base64.StdEncoding.EncodeToString(p.rawData) 113 | } 114 | 115 | return msgpack.Marshal(m) 116 | } 117 | 118 | var _ msgpack.Unmarshaler = (*packet)(nil) 119 | 120 | func (p *packet) UnmarshalMsgpack(b []byte) error { 121 | var m map[string]interface{} 122 | 123 | err := msgpack.Unmarshal(b, &m) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | iHdr, ok := m["header"] 129 | if !ok { 130 | return ErrInvalidPacket 131 | } 132 | 133 | if p.header, err = UnmarshalHeader(iHdr.([]byte)); err != nil { 134 | return err 135 | } 136 | 137 | iErr, ok := m["error"] 138 | if ok { 139 | p.err = fmt.Errorf(iErr.(string)) 140 | return nil 141 | } 142 | 143 | iData, ok := m["data"] 144 | if !ok { 145 | return ErrInvalidPacket 146 | } 147 | 148 | p.rawData, _ = base64.StdEncoding.DecodeString(cast.ToString(iData)) 149 | 150 | return nil 151 | } 152 | 153 | func MarshalPacket(p Packet) ([]byte, error) { 154 | return msgpack.Marshal(p) 155 | } 156 | 157 | func UnmarshalPacket(b []byte) (Packet, error) { 158 | var p packet 159 | if err := msgpack.Unmarshal(b, &p); err != nil { 160 | return nil, err 161 | } 162 | 163 | return &p, nil 164 | } 165 | -------------------------------------------------------------------------------- /pkg/util/group/group.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func DONE() (interface{}, error) { return nil, nil } 8 | 9 | type Group interface { 10 | Go(func() (interface{}, error), ...func(interface{}, error)) 11 | Wait() (interface{}, error) 12 | } 13 | 14 | type NewGroupFunc func() Group 15 | 16 | var ( 17 | NewAllGroupFunc = func() Group { return new(AllGroup) } 18 | NewRaceGroupFunc = func() Group { return new(RaceGroup) } 19 | NewAnyGroupFunc = func() Group { return new(AnyGroup) } 20 | ) 21 | 22 | type AllGroup struct { 23 | initOnce sync.Once 24 | 25 | doneWg sync.WaitGroup 26 | fastDone chan struct{} 27 | 28 | errOnce sync.Once 29 | err error 30 | } 31 | 32 | func (g *AllGroup) Go(f func() (interface{}, error), cb ...func(interface{}, error)) { 33 | g.ensureInit() 34 | 35 | g.doneWg.Add(1) 36 | go func() { 37 | defer g.doneWg.Done() 38 | 39 | if _, err := f(); err != nil { 40 | g.errOnce.Do(func() { 41 | g.err = err 42 | if len(cb) > 0 { 43 | cb[0](nil, err) 44 | } 45 | close(g.fastDone) 46 | }) 47 | } 48 | }() 49 | } 50 | 51 | func (g *AllGroup) Wait() (interface{}, error) { 52 | g.ensureInit() 53 | defer g.ensureRelease() 54 | 55 | allDone := make(chan struct{}) 56 | go func() { 57 | g.doneWg.Wait() 58 | close(allDone) 59 | }() 60 | 61 | select { 62 | case <-allDone: 63 | case <-g.fastDone: 64 | } 65 | 66 | return nil, g.err 67 | } 68 | 69 | func (g *AllGroup) ensureInit() { 70 | g.initOnce.Do(func() { 71 | g.fastDone = make(chan struct{}) 72 | }) 73 | } 74 | 75 | func (g *AllGroup) ensureRelease() { 76 | g.errOnce.Do(func() { close(g.fastDone) }) 77 | } 78 | 79 | type RaceGroup struct { 80 | initOnce sync.Once 81 | 82 | done chan struct{} 83 | 84 | doneOnce sync.Once 85 | ret interface{} 86 | err error 87 | } 88 | 89 | func (g *RaceGroup) ensureInit() { 90 | g.initOnce.Do(func() { 91 | g.done = make(chan struct{}) 92 | }) 93 | 94 | } 95 | 96 | func (g *RaceGroup) Go(f func() (interface{}, error), cb ...func(interface{}, error)) { 97 | g.ensureInit() 98 | 99 | go func() { 100 | ret, err := f() 101 | g.doneOnce.Do(func() { 102 | g.ret = ret 103 | g.err = err 104 | if len(cb) > 0 { 105 | cb[0](ret, err) 106 | } 107 | close(g.done) 108 | }) 109 | }() 110 | } 111 | 112 | func (g *RaceGroup) Wait() (interface{}, error) { 113 | g.ensureInit() 114 | 115 | select { 116 | case <-g.done: 117 | } 118 | 119 | return g.ret, g.err 120 | } 121 | 122 | type AnyGroup struct { 123 | initOnce sync.Once 124 | 125 | doneWg sync.WaitGroup 126 | fastDone chan struct{} 127 | 128 | errOnce sync.Once 129 | err error 130 | 131 | retOnce sync.Once 132 | ret interface{} 133 | } 134 | 135 | func (g *AnyGroup) ensureInit() { 136 | g.initOnce.Do(func() { 137 | g.fastDone = make(chan struct{}) 138 | }) 139 | } 140 | 141 | func (g *AnyGroup) ensureRelease() { 142 | g.retOnce.Do(func() { close(g.fastDone) }) 143 | } 144 | 145 | func (g *AnyGroup) Wait() (interface{}, error) { 146 | g.ensureInit() 147 | defer g.ensureRelease() 148 | 149 | allDone := make(chan struct{}) 150 | go func() { 151 | g.doneWg.Wait() 152 | close(allDone) 153 | }() 154 | 155 | select { 156 | case <-allDone: 157 | return g.ret, g.err 158 | case <-g.fastDone: 159 | return g.ret, nil 160 | } 161 | } 162 | 163 | func (g *AnyGroup) Go(f func() (interface{}, error), cb ...func(interface{}, error)) { 164 | g.ensureInit() 165 | 166 | g.doneWg.Add(1) 167 | go func() { 168 | defer g.doneWg.Done() 169 | 170 | ret, err := f() 171 | if err != nil { 172 | g.errOnce.Do(func() { g.err = err }) 173 | return 174 | } 175 | 176 | g.retOnce.Do(func() { 177 | g.ret = ret 178 | if len(cb) > 0 { 179 | cb[0](ret, nil) 180 | } 181 | close(g.fastDone) 182 | }) 183 | }() 184 | } 185 | -------------------------------------------------------------------------------- /cmd/config/meepo_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type MeepoConfig struct { 4 | IdentityFile string `yaml:"identityFile,omitempty"` 5 | Daemon bool `yaml:"daemon,omitempty"` 6 | AsSignaling bool `yaml:"asSignaling,omitempty"` 7 | Log *LogConfig `yaml:"log,omitempty"` 8 | Proxy *ProxyConfig `yaml:"proxy,omitempty"` 9 | Auth *AuthConfig `yaml:"auth,omitempty"` 10 | AuthI interface{} `yaml:"-"` 11 | Transport *TransportConfig `yaml:"transport,omitempty"` 12 | TransportI interface{} `yaml:"-"` 13 | Signaling *SignalingConfig `yaml:"signaling,omitempty"` 14 | SignalingI interface{} `yaml:"-"` 15 | Api *ApiConfig `yaml:"api,omitempty"` 16 | ApiI interface{} `yaml:"-"` 17 | Acl *AclConfig `yaml:"acl,omitempty"` 18 | } 19 | 20 | func (mc *MeepoConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 21 | var err error 22 | 23 | var fmc struct { 24 | IdentityFile *string `yaml:"identityFile"` 25 | Daemon *bool `yaml:"daemon"` 26 | AsSignaling *bool `yaml:"asSignaling"` 27 | Log *LogConfig `yaml:"log"` 28 | Proxy *ProxyConfig `yaml:"proxy"` 29 | Auth *AuthConfig `yaml:"auth"` 30 | Transport *TransportConfig `yaml:"transport"` 31 | Signaling *SignalingConfig `yaml:"signaling"` 32 | Api *ApiConfig `yaml:"api"` 33 | Acl *AclConfig `yaml:"acl"` 34 | } 35 | 36 | if err = unmarshal(&fmc); err != nil { 37 | return err 38 | } 39 | 40 | if fmc.Auth != nil { 41 | mc.Auth = fmc.Auth 42 | if fmc.Auth != nil { 43 | if mc.AuthI, err = unmarshalConfig("meepo.auth", fmc.Auth.Name, unmarshal); err != nil { 44 | return err 45 | } 46 | } 47 | } 48 | 49 | if fmc.Transport != nil { 50 | mc.Transport = fmc.Transport 51 | if fmc.Transport != nil { 52 | if mc.TransportI, err = unmarshalConfig("meepo.transport", fmc.Transport.Name, unmarshal); err != nil { 53 | return err 54 | } 55 | } 56 | } 57 | 58 | if fmc.Signaling != nil { 59 | mc.Signaling = fmc.Signaling 60 | if fmc.Signaling != nil { 61 | if mc.SignalingI, err = unmarshalConfig("meepo.signaling", fmc.Signaling.Name, unmarshal); err != nil { 62 | return err 63 | } 64 | } 65 | } 66 | 67 | mc.Api = fmc.Api 68 | if fmc.Api != nil { 69 | if mc.ApiI, err = unmarshalConfig("meepo.api", fmc.Api.Name, unmarshal); err != nil { 70 | return err 71 | } 72 | } 73 | 74 | if fmc.Log != nil { 75 | mc.Log = fmc.Log 76 | } 77 | 78 | if fmc.Proxy != nil { 79 | mc.Proxy = fmc.Proxy 80 | } 81 | 82 | if fmc.Acl != nil { 83 | mc.Acl = fmc.Acl 84 | } 85 | 86 | if fmc.IdentityFile != nil { 87 | mc.IdentityFile = *fmc.IdentityFile 88 | } 89 | 90 | if fmc.Daemon != nil { 91 | mc.Daemon = *fmc.Daemon 92 | } 93 | 94 | if fmc.AsSignaling != nil { 95 | mc.AsSignaling = *fmc.AsSignaling 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (mc *MeepoConfig) MarshalYAML() (interface{}, error) { 102 | _mc := struct { 103 | IdentityFile string `yaml:"identityFile"` 104 | Daemon bool `yaml:"daemon"` 105 | AsSignaling bool `yaml:"asSignaling"` 106 | Log *LogConfig `yaml:"log"` 107 | Proxy *ProxyConfig `yaml:"proxy"` 108 | Auth interface{} `yaml:"auth"` 109 | Transport interface{} `yaml:"transport"` 110 | Signaling interface{} `yaml:"signaling"` 111 | Api interface{} `yaml:"api"` 112 | Acl *AclConfig `yaml:"acl"` 113 | }{ 114 | IdentityFile: mc.IdentityFile, 115 | Daemon: mc.Daemon, 116 | AsSignaling: mc.AsSignaling, 117 | Log: mc.Log, 118 | Proxy: mc.Proxy, 119 | Auth: mc.AuthI, 120 | Transport: mc.TransportI, 121 | Signaling: mc.SignalingI, 122 | Api: mc.ApiI, 123 | Acl: mc.Acl, 124 | } 125 | 126 | return &_mc, nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/meepo/option.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "net" 6 | "time" 7 | 8 | "github.com/pion/webrtc/v3" 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/PeerXu/meepo/pkg/ofn" 12 | "github.com/PeerXu/meepo/pkg/signaling" 13 | ) 14 | 15 | func newNewMeepoOption() ofn.Option { 16 | return ofn.NewOption(map[string]interface{}{ 17 | "iceServers": []string{ 18 | "stun:stun.xten.com:3478", 19 | "stun:stun.voipbuster.com:3478", 20 | "stun:stun.sipgate.net:3478", 21 | "stun:stun.ekiga.net:3478", 22 | "stun:stun.ideasip.com:3478", 23 | "stun:stun.schlund.de:3478", 24 | "stun:stun.voiparound.com:3478", 25 | "stun:stun.voipbuster.com:3478", 26 | "stun:stun.voipstunt.com:3478", 27 | "stun:stun.counterpath.com:3478", 28 | "stun:stun.1und1.de:3478", 29 | "stun:stun.gmx.net:3478", 30 | "stun:stun.callwithus.com:3478", 31 | "stun:stun.counterpath.net:3478", 32 | "stun:stun.internetcalls.com:3478", 33 | "stun:numb.viagenie.ca:3478", 34 | }, 35 | "gatherTimeout": 31 * time.Second, 36 | "waitResponseTimeout": 17 * time.Second, 37 | }) 38 | } 39 | 40 | type NewMeepoOption = ofn.OFN 41 | 42 | func WithSignalingEngine(se signaling.Engine) ofn.OFN { 43 | return func(o ofn.Option) { 44 | o["signalingEngine"] = se 45 | } 46 | } 47 | 48 | func WithWebrtcAPI(webrtcAPI *webrtc.API) ofn.OFN { 49 | return func(o ofn.Option) { 50 | o["webrtcAPI"] = webrtcAPI 51 | } 52 | } 53 | 54 | func WithICEServers(iceServers []string) ofn.OFN { 55 | return func(o ofn.Option) { 56 | o["iceServers"] = iceServers 57 | } 58 | } 59 | 60 | func WithED25519KeyPair(pubk ed25519.PublicKey, prik ed25519.PrivateKey) ofn.OFN { 61 | return func(o ofn.Option) { 62 | o["ed25519PublicKey"] = pubk 63 | o["ed25519PrivateKey"] = prik 64 | } 65 | } 66 | 67 | func WithLogger(logger logrus.FieldLogger) ofn.OFN { 68 | return func(o ofn.Option) { 69 | o["logger"] = logger 70 | } 71 | } 72 | 73 | func WithGatherTimeout(d time.Duration) ofn.OFN { 74 | return func(o ofn.Option) { 75 | o["gatherTimeout"] = d 76 | } 77 | } 78 | 79 | func WithAsSignaling(b bool) ofn.OFN { 80 | return func(o ofn.Option) { 81 | o["asSignaling"] = b 82 | } 83 | } 84 | 85 | func WithAuthorizationName(name string) ofn.OFN { 86 | return func(o ofn.Option) { 87 | o["authorizationName"] = name 88 | } 89 | } 90 | 91 | func WithAuthorizationSecret(secret string) ofn.OFN { 92 | return func(o ofn.Option) { 93 | o["authorizationSecret"] = secret 94 | } 95 | } 96 | 97 | func WithAcl(acl Acl) ofn.OFN { 98 | return func(o ofn.Option) { 99 | o["acl"] = acl 100 | } 101 | } 102 | 103 | type TeleportOption = ofn.OFN 104 | 105 | func WithLocalAddress(local net.Addr) ofn.OFN { 106 | return func(o ofn.Option) { 107 | o["local"] = local 108 | } 109 | } 110 | 111 | func WithSecret(secret string) ofn.OFN { 112 | return func(o ofn.Option) { 113 | o["secret"] = secret 114 | } 115 | } 116 | 117 | type NewTeleportationOption = ofn.OFN 118 | 119 | func WithName(name string) ofn.OFN { 120 | return func(o ofn.Option) { 121 | o["name"] = name 122 | } 123 | } 124 | 125 | type GetTeleportationOption = ofn.OFN 126 | 127 | func WithSourceFirst() ofn.OFN { 128 | return func(o ofn.Option) { 129 | o["getFirst"] = "source" 130 | } 131 | } 132 | 133 | func WithSinkFirst() ofn.OFN { 134 | return func(o ofn.Option) { 135 | o["getFirst"] = "sink" 136 | } 137 | } 138 | 139 | // Socks5 Server 140 | 141 | type NewSocks5ServerOption = ofn.OFN 142 | 143 | func WithMeepo(mp *Meepo) ofn.OFN { 144 | return func(o ofn.Option) { 145 | o["meepo"] = mp 146 | } 147 | } 148 | 149 | func WithHost(host string) ofn.OFN { 150 | return func(o ofn.Option) { 151 | o["host"] = host 152 | } 153 | } 154 | 155 | func WithPort(port int32) ofn.OFN { 156 | return func(o ofn.Option) { 157 | o["port"] = port 158 | } 159 | } 160 | 161 | // Close Transport 162 | 163 | type CloseTransportOption = ofn.OFN 164 | 165 | func WithGracePeriod(s string) ofn.OFN { 166 | return func(o ofn.Option) { 167 | o["gracePeriod"] = s 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /pkg/meepo/teleportation.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "github.com/spf13/cast" 5 | 6 | "github.com/PeerXu/meepo/pkg/ofn" 7 | "github.com/PeerXu/meepo/pkg/teleportation" 8 | ) 9 | 10 | type GetTeleportationFunc func(string) (teleportation.Teleportation, bool) 11 | 12 | func (mp *Meepo) GetTeleportation(name string, opts ...GetTeleportationOption) (teleportation.Teleportation, error) { 13 | mp.teleportationsMtx.Lock() 14 | defer mp.teleportationsMtx.Unlock() 15 | 16 | return mp.getTeleportationNL(name, opts...) 17 | } 18 | 19 | func newGetTeleportationOption() ofn.Option { 20 | return ofn.NewOption(map[string]interface{}{}) 21 | } 22 | 23 | func (mp *Meepo) getTeleportationNL(name string, opts ...GetTeleportationOption) (teleportation.Teleportation, error) { 24 | var tp teleportation.Teleportation 25 | var ok bool 26 | 27 | o := newGetTeleportationOption() 28 | for _, opt := range opts { 29 | opt(o) 30 | } 31 | 32 | var fns []GetTeleportationFunc 33 | switch cast.ToString(o.Get("getFirst").Inter()) { 34 | case "sink": 35 | fns = []GetTeleportationFunc{ 36 | func(name string) (teleportation.Teleportation, bool) { return mp.getTeleportationSinkNL(name) }, 37 | func(name string) (teleportation.Teleportation, bool) { return mp.getTeleportationSourceNL(name) }, 38 | } 39 | case "source": 40 | fallthrough 41 | default: 42 | fns = []GetTeleportationFunc{ 43 | func(name string) (teleportation.Teleportation, bool) { return mp.getTeleportationSourceNL(name) }, 44 | func(name string) (teleportation.Teleportation, bool) { return mp.getTeleportationSinkNL(name) }, 45 | } 46 | } 47 | 48 | for _, fn := range fns { 49 | if tp, ok = fn(name); ok { 50 | return tp, nil 51 | } 52 | } 53 | 54 | return nil, ErrTeleportationNotExist 55 | } 56 | 57 | func (mp *Meepo) addTeleportationSource(name string, ts *teleportation.TeleportationSource) { 58 | mp.teleportationsMtx.Lock() 59 | defer mp.teleportationsMtx.Unlock() 60 | mp.addTeleportationSourceNL(name, ts) 61 | } 62 | 63 | func (mp *Meepo) addTeleportationSourceNL(name string, ts *teleportation.TeleportationSource) { 64 | mp.teleportationSources[name] = ts 65 | } 66 | 67 | func (mp *Meepo) removeTeleportationSource(name string) { 68 | mp.teleportationsMtx.Lock() 69 | defer mp.teleportationsMtx.Unlock() 70 | mp.removeTeleportationSourceNL(name) 71 | } 72 | 73 | func (mp *Meepo) removeTeleportationSourceNL(name string) { 74 | delete(mp.teleportationSources, name) 75 | } 76 | 77 | func (mp *Meepo) addTeleportationSink(name string, ts *teleportation.TeleportationSink) { 78 | mp.teleportationsMtx.Lock() 79 | defer mp.teleportationsMtx.Unlock() 80 | mp.addTeleportationSinkNL(name, ts) 81 | } 82 | 83 | func (mp *Meepo) addTeleportationSinkNL(name string, ts *teleportation.TeleportationSink) { 84 | mp.teleportationSinks[name] = ts 85 | } 86 | 87 | func (mp *Meepo) removeTeleportationSink(name string) { 88 | mp.teleportationsMtx.Lock() 89 | defer mp.teleportationsMtx.Unlock() 90 | mp.removeTeleportationSinkNL(name) 91 | } 92 | 93 | func (mp *Meepo) removeTeleportationSinkNL(name string) { 94 | delete(mp.teleportationSinks, name) 95 | } 96 | 97 | func (mp *Meepo) listTeleportations() ([]teleportation.Teleportation, error) { 98 | mp.teleportationsMtx.Lock() 99 | defer mp.teleportationsMtx.Unlock() 100 | 101 | return mp.listTeleportationsNL() 102 | } 103 | 104 | func (mp *Meepo) listTeleportationsNL() ([]teleportation.Teleportation, error) { 105 | var teleportations []teleportation.Teleportation 106 | 107 | for _, ts := range mp.teleportationSources { 108 | teleportations = append(teleportations, ts) 109 | } 110 | 111 | for _, ts := range mp.teleportationSinks { 112 | teleportations = append(teleportations, ts) 113 | } 114 | 115 | return teleportations, nil 116 | } 117 | 118 | func (mp *Meepo) listTeleportationsByPeerID(id string) ([]teleportation.Teleportation, error) { 119 | mp.teleportationsMtx.Lock() 120 | defer mp.teleportationsMtx.Unlock() 121 | 122 | return mp.listTeleportationsByPeerIDNL(id) 123 | } 124 | 125 | func (mp *Meepo) listTeleportationsByPeerIDNL(id string) ([]teleportation.Teleportation, error) { 126 | xs, err := mp.listTeleportationsNL() 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | var ys []teleportation.Teleportation 132 | for _, x := range xs { 133 | if x.Transport().PeerID() == id { 134 | ys = append(ys, x) 135 | } 136 | } 137 | 138 | return ys, nil 139 | } 140 | 141 | func (mp *Meepo) getTeleportationSource(name string) (*teleportation.TeleportationSource, bool) { 142 | mp.teleportationsMtx.Lock() 143 | defer mp.teleportationsMtx.Unlock() 144 | return mp.getTeleportationSourceNL(name) 145 | } 146 | 147 | func (mp *Meepo) getTeleportationSourceNL(name string) (*teleportation.TeleportationSource, bool) { 148 | ts, ok := mp.teleportationSources[name] 149 | return ts, ok 150 | } 151 | 152 | func (mp *Meepo) getTeleportationSink(name string) (*teleportation.TeleportationSink, bool) { 153 | mp.teleportationsMtx.Lock() 154 | defer mp.teleportationsMtx.Unlock() 155 | return mp.getTeleportationSinkNL(name) 156 | } 157 | 158 | func (mp *Meepo) getTeleportationSinkNL(name string) (*teleportation.TeleportationSink, bool) { 159 | ts, ok := mp.teleportationSinks[name] 160 | return ts, ok 161 | } 162 | -------------------------------------------------------------------------------- /pkg/meepo/acl_test.go: -------------------------------------------------------------------------------- 1 | package meepo_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/PeerXu/meepo/pkg/meepo" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type AclTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func (s *AclTestSuite) TestAddrContains() { 16 | for _, t := range []struct { 17 | a net.Addr 18 | b net.Addr 19 | }{ 20 | {meepo.ACL_ANY_ADDR, meepo.NewAclAddr("socks5", "127.0.0.1:22")}, 21 | {meepo.ACL_ANY_ADDR, meepo.NewAclAddr("tcp", "127.0.0.1:8080")}, 22 | {meepo.NewAclAddr("tcp", "192.168.1.0/24:*"), meepo.NewAclAddr("tcp", "192.168.1.1:8080")}, 23 | } { 24 | s.True(meepo.AddrContains(t.a, t.b), "%s:%s %s:%s", t.a.Network(), t.a.String(), t.b.Network(), t.b.String()) 25 | } 26 | 27 | for _, t := range []struct { 28 | a net.Addr 29 | b net.Addr 30 | }{ 31 | {meepo.NewAclAddr(meepo.ACL_ANY, "192.168.2.1:22"), meepo.NewAclAddr("tcp", "192.168.2.2:22")}, 32 | {meepo.NewAclAddr(meepo.ACL_ANY, "192.168.2.1:22"), meepo.NewAclAddr("tcp", "192.168.2.1:44")}, 33 | {meepo.NewAclAddr(meepo.ACL_ANY, "192.168.1.0/24:*"), meepo.NewAclAddr("tcp", "192.168.2.1:22")}, 34 | } { 35 | s.False(meepo.AddrContains(t.a, t.b), "%s:%s %s:%s", t.a.Network(), t.a.String(), t.b.Network(), t.b.String()) 36 | } 37 | } 38 | 39 | func (s *AclTestSuite) TestParseAclPolicy() { 40 | for _, t := range []struct { 41 | s string 42 | p meepo.AclPolicy 43 | }{ 44 | {"*", meepo.NewAclPolicy(meepo.ACL_ANY_ENTITY, meepo.ACL_ANY_ENTITY)}, 45 | {"192.168.1.1:22", meepo.NewAclPolicy(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity(meepo.ACL_ANY, meepo.NewAclAddr(meepo.ACL_ANY, "192.168.1.1:22")))}, 46 | {"10.0.0.0/24:*", meepo.NewAclPolicy(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity(meepo.ACL_ANY, meepo.NewAclAddr(meepo.ACL_ANY, "10.0.0.0/24:*")))}, 47 | {"tcp:10.0.0.1:22", meepo.NewAclPolicy(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity(meepo.ACL_ANY, meepo.NewAclAddr("tcp", "10.0.0.1:22")))}, 48 | {"a:*:10.0.0.1:22", meepo.NewAclPolicy(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr(meepo.ACL_ANY, "10.0.0.1:22")))}, 49 | {"socks5:*:*,127.0.0.1:80", meepo.NewAclPolicy(meepo.NewAclEntity(meepo.ACL_ANY, meepo.NewAclAddr("socks5", "*:*")), meepo.NewAclEntity(meepo.ACL_ANY, meepo.NewAclAddr(meepo.ACL_ANY, "127.0.0.1:80")))}, 50 | } { 51 | p, err := meepo.ParseAclPolicy(t.s) 52 | s.Require().Nil(err) 53 | s.Equal(t.p.Source().ID(), p.Source().ID(), "%s %s", s, p) 54 | s.Equal(t.p.Source().Addr().Network(), p.Source().Addr().Network(), "%s %s", s, p) 55 | s.Equal(t.p.Source().Addr().String(), p.Source().Addr().String(), "%s %s", s, p) 56 | s.Equal(t.p.Destination().ID(), p.Destination().ID(), "%s %s", s, p) 57 | s.Equal(t.p.Destination().Addr().Network(), p.Destination().Addr().Network(), "%s %s", s, p) 58 | s.Equal(t.p.Destination().Addr().String(), p.Destination().Addr().String(), "%s %s", s, p) 59 | } 60 | } 61 | 62 | func (ts *AclTestSuite) TestAclAllowed() { 63 | for _, t := range []struct { 64 | ap []string 65 | bp []string 66 | ac []meepo.AclChallenge 67 | bc []meepo.AclChallenge 68 | }{ 69 | { 70 | []string{"*"}, 71 | nil, 72 | []meepo.AclChallenge{ 73 | meepo.NewAclChallenge(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "127.0.0.1:22"))), 74 | }, 75 | nil, 76 | }, 77 | { 78 | []string{"127.0.0.1:22"}, 79 | nil, 80 | []meepo.AclChallenge{ 81 | meepo.NewAclChallenge(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "127.0.0.1:22"))), 82 | }, 83 | []meepo.AclChallenge{ 84 | meepo.NewAclChallenge(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "127.0.0.1:80"))), 85 | }, 86 | }, 87 | { 88 | []string{"*"}, 89 | []string{"127.0.0.1:*"}, 90 | []meepo.AclChallenge{ 91 | meepo.NewAclChallenge(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "10.1.1.1:22"))), 92 | }, 93 | []meepo.AclChallenge{ 94 | meepo.NewAclChallenge(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "127.0.0.1:22"))), 95 | meepo.NewAclChallenge(meepo.ACL_ANY_ENTITY, meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "127.0.0.1:80"))), 96 | }, 97 | }, 98 | { 99 | []string{"*:socks5:*:*,*"}, 100 | nil, 101 | []meepo.AclChallenge{ 102 | meepo.NewAclChallenge(meepo.NewAclEntity("a", meepo.NewAclAddr("socks5", "0.0.0.0:0")), meepo.NewAclEntity("b", meepo.NewAclAddr("tcp", "127.0.0.1:22"))), 103 | }, 104 | []meepo.AclChallenge{ 105 | meepo.NewAclChallenge(meepo.NewAclEntity("a", meepo.NewAclAddr("tcp", "127.0.0.1:2222")), meepo.NewAclEntity("b", meepo.NewAclAddr("tcp", "127.0.0.1:22"))), 106 | }, 107 | }, 108 | } { 109 | var allows []meepo.AclPolicy 110 | var blocks []meepo.AclPolicy 111 | for _, s := range t.ap { 112 | p, err := meepo.ParseAclPolicy(s) 113 | ts.Require().Nil(err) 114 | allows = append(allows, p) 115 | } 116 | for _, s := range t.bp { 117 | p, err := meepo.ParseAclPolicy(s) 118 | ts.Require().Nil(err) 119 | blocks = append(blocks, p) 120 | } 121 | acl := meepo.NewAcl(meepo.WithAllowPolicies(allows), meepo.WithBlockPolicies(blocks)) 122 | for _, c := range t.ac { 123 | err := acl.Allowed(c) 124 | ts.Nil(err) 125 | } 126 | for _, c := range t.bc { 127 | err := acl.Allowed(c) 128 | ts.Equal(meepo.ErrAclNotAllowed, err) 129 | } 130 | } 131 | } 132 | 133 | func TestAclTestSuite(t *testing.T) { 134 | suite.Run(t, new(AclTestSuite)) 135 | } 136 | -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | "github.com/spf13/cast" 10 | "gopkg.in/yaml.v3" 11 | 12 | mfs "github.com/PeerXu/meepo/pkg/util/fs" 13 | ) 14 | 15 | type Config struct { 16 | Meepo *MeepoConfig `yaml:"meepo"` 17 | } 18 | 19 | func (c *Config) Get(key string) (string, error) { 20 | switch key { 21 | case "identityFile": 22 | return c.Meepo.IdentityFile, nil 23 | case "log.level": 24 | return c.Meepo.Log.Level, nil 25 | case "signaling.url": 26 | return c.Meepo.SignalingI.(*RedisSignalingConfig).URL, nil 27 | case "transport.iceServers": 28 | return strings.Join(c.Meepo.TransportI.(*WebrtcTransportConfig).ICEServers, ","), nil 29 | case "asSignaling": 30 | return cast.ToString(c.Meepo.AsSignaling), nil 31 | case "auth": 32 | buf, err := yaml.Marshal(c.Meepo.AuthI) 33 | return string(buf), err 34 | case "proxy.socks5": 35 | buf, err := yaml.Marshal(c.Meepo.Proxy.Socks5) 36 | return string(buf), err 37 | default: 38 | return "", UnsupportedConfigKeyError{key} 39 | } 40 | } 41 | 42 | func (c *Config) Set(key, val string) error { 43 | var err error 44 | 45 | switch key { 46 | case "identityFile": 47 | c.Meepo.IdentityFile = val 48 | case "log.level": 49 | c.Meepo.Log.Level = val 50 | case "signaling.url": 51 | c.Meepo.SignalingI.(*RedisSignalingConfig).URL = val 52 | case "asSignaling": 53 | c.Meepo.AsSignaling = cast.ToBool(val) 54 | case "auth": 55 | var ac AuthConfig 56 | 57 | if err = yaml.Unmarshal([]byte(val), &ac); err != nil { 58 | return err 59 | } 60 | 61 | c.Meepo.Auth = &ac 62 | if c.Meepo.AuthI, err = UnmarshalConfig("meepo.auth", ac.Name, WrapKeyYaml("auth", string(val))); err != nil { 63 | return err 64 | } 65 | case "auth.secret": 66 | c.Meepo.Auth = &AuthConfig{Name: "secret"} 67 | c.Meepo.AuthI = &SecretAuthConfig{ 68 | Name: "secret", 69 | Secret: val, 70 | } 71 | case "auth.dummy": 72 | c.Meepo.Auth = &AuthConfig{Name: "dummy"} 73 | c.Meepo.AuthI = &DummyAuthConfig{Name: "dummy"} 74 | case "proxy.socks5": 75 | var sc Socks5Config 76 | 77 | if val == "" { 78 | c.Meepo.Proxy.Socks5 = nil 79 | break 80 | } 81 | 82 | if err = yaml.Unmarshal([]byte(val), &sc); err != nil { 83 | return err 84 | } 85 | 86 | c.Meepo.Proxy.Socks5 = &sc 87 | default: 88 | return UnsupportedConfigKeyError{key} 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (c *Config) Dump(p string) error { 95 | var err error 96 | 97 | if p, err = homedir.Expand(p); err != nil { 98 | return err 99 | } 100 | 101 | if err = mfs.EnsureDirectoryExist(p); err != nil { 102 | return err 103 | } 104 | 105 | buf, err := yaml.Marshal(c) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if err = ioutil.WriteFile(p, buf, 0644); err != nil { 111 | return err 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func Load(p string) (config *Config, loaded bool, err error) { 118 | if p, err = homedir.Expand(p); err != nil { 119 | return nil, false, err 120 | } 121 | 122 | config = NewDefaultConfig() 123 | 124 | buf, err := ioutil.ReadFile(p) 125 | if err != nil { 126 | if !os.IsNotExist(err) { 127 | return nil, false, err 128 | } 129 | return config, false, nil 130 | } 131 | 132 | if err = yaml.Unmarshal(buf, config); err != nil { 133 | return nil, false, err 134 | } 135 | 136 | if config.Meepo.Auth == nil { 137 | config.Meepo.Auth = &AuthConfig{Name: "dummy"} 138 | config.Meepo.AuthI = &DummyAuthConfig{Name: "dummy"} 139 | } 140 | 141 | if config.Meepo.Proxy == nil { 142 | config.Meepo.Proxy = &ProxyConfig{} 143 | } 144 | 145 | return config, true, nil 146 | } 147 | 148 | func NewDefaultConfig() *Config { 149 | return &Config{ 150 | Meepo: &MeepoConfig{ 151 | IdentityFile: "", 152 | Daemon: true, 153 | AsSignaling: true, 154 | Log: &LogConfig{ 155 | Level: "error", 156 | }, 157 | Auth: &AuthConfig{ 158 | Name: "dummy", 159 | }, 160 | AuthI: &DummyAuthConfig{ 161 | Name: "dummy", 162 | }, 163 | Transport: &TransportConfig{ 164 | Name: "webrtc", 165 | }, 166 | TransportI: &WebrtcTransportConfig{ 167 | Name: "webrtc", 168 | ICEServers: []string{ 169 | "stun:stun.xten.com:3478", 170 | "stun:stun.voipbuster.com:3478", 171 | "stun:stun.sipgate.net:3478", 172 | "stun:stun.ekiga.net:3478", 173 | "stun:stun.ideasip.com:3478", 174 | "stun:stun.schlund.de:3478", 175 | "stun:stun.voiparound.com:3478", 176 | "stun:stun.voipbuster.com:3478", 177 | "stun:stun.voipstunt.com:3478", 178 | "stun:stun.counterpath.com:3478", 179 | "stun:stun.1und1.de:3478", 180 | "stun:stun.gmx.net:3478", 181 | "stun:stun.callwithus.com:3478", 182 | "stun:stun.counterpath.net:3478", 183 | "stun:stun.internetcalls.com:3478", 184 | "stun:numb.viagenie.ca:3478", 185 | }, 186 | }, 187 | Signaling: &SignalingConfig{ 188 | Name: "redis", 189 | }, 190 | SignalingI: &RedisSignalingConfig{ 191 | Name: "redis", 192 | URL: "redis://meepo.redis.signaling.peerstud.io:6379/1", 193 | }, 194 | Api: &ApiConfig{ 195 | Name: "http", 196 | }, 197 | ApiI: &HttpApiConfig{ 198 | Name: "http", 199 | Host: "127.0.0.1", 200 | Port: 12345, 201 | }, 202 | Proxy: &ProxyConfig{ 203 | Socks5: &Socks5Config{ 204 | Host: "127.0.0.1", 205 | Port: 12341, 206 | }, 207 | }, 208 | }, 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /pkg/meepo/new_transport.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/pion/webrtc/v3" 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/PeerXu/meepo/pkg/signaling" 10 | "github.com/PeerXu/meepo/pkg/transport" 11 | _ "github.com/PeerXu/meepo/pkg/transport/loopback" 12 | webrtc_transport "github.com/PeerXu/meepo/pkg/transport/webrtc" 13 | ) 14 | 15 | func (mp *Meepo) onTransportSysDataChannelCreate(dc transport.DataChannel) { 16 | logger := mp.getLogger().WithField("#method", "onTransportSysDataChannelCreate") 17 | 18 | dc.OnOpen(func() { 19 | innerLogger := mp.getLogger().WithFields(logrus.Fields{ 20 | "#method": "OnOpen", 21 | "label": dc.Label(), 22 | }) 23 | go mp.sysDataChannelLoop(dc) 24 | innerLogger.Tracef("data channel opened") 25 | }) 26 | 27 | logger.Tracef("data channel created") 28 | } 29 | 30 | func (mp *Meepo) NewTransport(peerID string) (transport.Transport, error) { 31 | var err error 32 | 33 | logger := mp.getLogger().WithFields(logrus.Fields{ 34 | "#method": "NewTransport", 35 | "peerID": peerID, 36 | }) 37 | 38 | tlck := mp.getTransportLock(peerID) 39 | tlck.Lock() 40 | defer tlck.Unlock() 41 | 42 | if _, err = mp.getTransport(peerID); err == nil { 43 | err = ErrTransportExist 44 | logger.WithError(err).Errorf("transport already exists") 45 | return nil, err 46 | } 47 | 48 | var name string 49 | opts := []transport.NewTransportOption{ 50 | transport.WithID(mp.GetID()), 51 | transport.WithPeerID(peerID), 52 | transport.WithLogger(mp.getRawLogger()), 53 | } 54 | if peerID == mp.GetID() { 55 | name = "loopback" 56 | } else { 57 | name = "webrtc" 58 | opts = append(opts, 59 | webrtc_transport.WithWebrtcAPI(mp.rtc), 60 | webrtc_transport.WithICEServers(mp.getICEServers()), 61 | webrtc_transport.AsOfferer(), 62 | webrtc_transport.WithOfferHook(func(offer *webrtc.SessionDescription) (*webrtc.SessionDescription, error) { 63 | req, gcm, nonce, err := mp.marshalRequestDescriptor(peerID, offer) 64 | if err != nil { 65 | logger.WithError(err).Errorf("failed to marshal offer session description") 66 | return nil, err 67 | } 68 | 69 | res, err := mp.se.Wire(&signaling.Descriptor{ID: peerID}, req) 70 | if err != nil { 71 | logger.WithError(err).Errorf("failed to wire") 72 | return nil, err 73 | } 74 | 75 | answer, err := mp.unmarshalResponseDescriptor(res, gcm, nonce) 76 | if err != nil { 77 | logger.WithError(err).Errorf("failed to unmarshal answer session description") 78 | return nil, err 79 | } 80 | 81 | logger.Tracef("signaling engine wire") 82 | 83 | return answer, nil 84 | }), 85 | ) 86 | } 87 | 88 | tp, err := transport.NewTransport(name, opts...) 89 | if err != nil { 90 | logger.WithError(err).Errorf("failed to new transport") 91 | return nil, err 92 | } 93 | tp.OnDataChannelCreate("sys", mp.onTransportSysDataChannelCreate) 94 | logger.Tracef("register on data channel create handler") 95 | 96 | h := func(transport.HandleID) { 97 | mp.closeTeleportationsByPeerID(peerID) 98 | logger.Tracef("close teleportations") 99 | 100 | mp.removeTransport(peerID) 101 | logger.Tracef("remove transport") 102 | } 103 | tp.OnTransportState(transport.TransportStateFailed, h) 104 | tp.OnTransportState(transport.TransportStateClosed, h) 105 | logger.Tracef("register on transport state change handler") 106 | 107 | mp.addTransport(peerID, tp) 108 | logger.Tracef("add transport") 109 | 110 | logger.Info("new transport") 111 | 112 | return tp, nil 113 | } 114 | 115 | func (mp *Meepo) onNewTransport(req *signaling.Descriptor) (*signaling.Descriptor, error) { 116 | var err error 117 | 118 | peerID := req.ID 119 | logger := mp.getLogger().WithFields(logrus.Fields{ 120 | "#method": "onNewTransport", 121 | "peerID": peerID, 122 | }) 123 | 124 | tlck := mp.getTransportLock(peerID) 125 | tlck.Lock() 126 | defer tlck.Unlock() 127 | 128 | if _, err = mp.getTransport(peerID); err == nil { 129 | err = ErrTransportExist 130 | logger.WithError(err).Errorf("transport already exists") 131 | return nil, err 132 | } 133 | 134 | offer, gcm, nonce, err := mp.unmarshalRequestDescriptor(req) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | var res *signaling.Descriptor 140 | var wg sync.WaitGroup 141 | var er error 142 | wg.Add(1) 143 | tp, err := transport.NewTransport("webrtc", 144 | transport.WithID(mp.GetID()), 145 | transport.WithPeerID(peerID), 146 | transport.WithLogger(mp.getRawLogger()), 147 | webrtc_transport.WithWebrtcAPI(mp.rtc), 148 | webrtc_transport.WithICEServers(mp.getICEServers()), 149 | webrtc_transport.WithOffer(offer), 150 | webrtc_transport.AsAnswerer(), 151 | webrtc_transport.WithAnswerHook(func(answer *webrtc.SessionDescription, hookErr error) { 152 | defer wg.Done() 153 | 154 | if hookErr != nil { 155 | er = hookErr 156 | logger.WithError(er).Errorf("failed to wire") 157 | return 158 | } 159 | 160 | res, er = mp.marshalResponseDescriptor(answer, gcm, nonce) 161 | if er != nil { 162 | logger.WithError(er).Errorf("failed to marshal response descriptor") 163 | return 164 | } 165 | }), 166 | ) 167 | if err != nil { 168 | logger.WithError(err).Errorf("failed to new transport") 169 | return nil, err 170 | } 171 | tp.OnDataChannelCreate("sys", mp.onTransportSysDataChannelCreate) 172 | logger.Tracef("register on data channel create handler") 173 | 174 | h := func(transport.HandleID) { 175 | mp.closeTeleportationsByPeerID(peerID) 176 | logger.Tracef("close teleportations") 177 | 178 | mp.removeTransport(peerID) 179 | logger.Tracef("remove transport") 180 | } 181 | tp.OnTransportState(transport.TransportStateFailed, h) 182 | tp.OnTransportState(transport.TransportStateClosed, h) 183 | logger.Tracef("register on transport state change handler") 184 | 185 | wg.Wait() 186 | if er != nil { 187 | // logged in answer hook 188 | return nil, er 189 | } 190 | 191 | mp.addTransport(peerID, tp) 192 | logger.Tracef("add transport") 193 | 194 | logger.Infof("new transport") 195 | 196 | return res, nil 197 | } 198 | -------------------------------------------------------------------------------- /pkg/transport/loopback/datachannel.go: -------------------------------------------------------------------------------- 1 | package loopback_transport 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | 10 | "github.com/PeerXu/meepo/pkg/transport" 11 | msync "github.com/PeerXu/meepo/pkg/util/sync" 12 | ) 13 | 14 | type LoopbackDataChannel struct { 15 | c1Rd *io.PipeReader 16 | c1Wr *io.PipeWriter 17 | c2Rd *io.PipeReader 18 | c2Wr *io.PipeWriter 19 | 20 | left *LoopbackDataChannelWrapper 21 | right *LoopbackDataChannelWrapper 22 | 23 | label string 24 | transport transport.Transport 25 | state transport.DataChannelState 26 | mtx msync.Locker 27 | 28 | logger logrus.FieldLogger 29 | } 30 | 31 | func NewLoopbackDataChannel(label string, tp transport.Transport, logger logrus.FieldLogger) *LoopbackDataChannel { 32 | c1Rd, c1Wr := io.Pipe() 33 | c2Rd, c2Wr := io.Pipe() 34 | 35 | return &LoopbackDataChannel{ 36 | c1Rd: c1Rd, 37 | c1Wr: c1Wr, 38 | c2Rd: c2Rd, 39 | c2Wr: c2Wr, 40 | label: label, 41 | transport: tp, 42 | logger: logger, 43 | state: transport.DataChannelStateConnecting, 44 | mtx: msync.NewLock(), 45 | } 46 | } 47 | 48 | func (ldc *LoopbackDataChannel) getLogger() logrus.FieldLogger { 49 | return ldc.logger.WithFields(logrus.Fields{ 50 | "#instance": "LoopbackDataChannel", 51 | "label": ldc.Label(), 52 | }) 53 | } 54 | 55 | func (ldc *LoopbackDataChannel) getRawLogger() logrus.FieldLogger { 56 | return ldc.logger 57 | } 58 | 59 | func (ldc *LoopbackDataChannel) Transport() transport.Transport { 60 | ldc.mtx.Lock() 61 | defer ldc.mtx.Unlock() 62 | return ldc.transport 63 | } 64 | 65 | func (ldc *LoopbackDataChannel) Label() string { 66 | return ldc.label 67 | } 68 | 69 | func (ldc *LoopbackDataChannel) setState(s transport.DataChannelState) { 70 | logger := ldc.getLogger().WithField("#method", "setState") 71 | 72 | ldc.mtx.Lock() 73 | ldc.state = s 74 | ldc.mtx.Unlock() 75 | 76 | logger.WithField("state", s).Tracef("set state") 77 | } 78 | 79 | func (ldc *LoopbackDataChannel) State() transport.DataChannelState { 80 | ldc.mtx.Lock() 81 | defer ldc.mtx.Unlock() 82 | return ldc.state 83 | } 84 | 85 | func (ldc *LoopbackDataChannel) OnOpen(f func()) { 86 | panic("unimplemented") 87 | } 88 | 89 | func (ldc *LoopbackDataChannel) Read([]byte) (int, error) { 90 | panic("unimplemented") 91 | } 92 | 93 | func (ldc *LoopbackDataChannel) Write([]byte) (int, error) { 94 | panic("unimplemented") 95 | } 96 | 97 | func (ldc *LoopbackDataChannel) Close() error { 98 | logger := ldc.getLogger().WithField("#method", "Close") 99 | 100 | // HACK: lazy closing wait for send response to other side 101 | time.Sleep(50 * time.Millisecond) 102 | 103 | ldc.mtx.Lock() 104 | defer ldc.mtx.Unlock() 105 | 106 | if ldc.state == transport.DataChannelStateClosed { 107 | return nil 108 | } 109 | 110 | ldc.state = transport.DataChannelStateClosing 111 | logger.WithField("state", transport.DataChannelStateClosing).Tracef("change state") 112 | 113 | logger.WithError(ldc.c1Rd.Close()).Tracef("c1rd closed") 114 | logger.WithError(ldc.c1Wr.Close()).Tracef("c1wr closed") 115 | logger.WithError(ldc.c2Rd.Close()).Tracef("c2rd closed") 116 | logger.WithError(ldc.c2Wr.Close()).Tracef("c2wr closed") 117 | 118 | ldc.state = transport.DataChannelStateClosed 119 | logger.WithField("state", transport.DataChannelStateClosed).Tracef("change state") 120 | 121 | defer logger.Tracef("closed") 122 | 123 | return nil 124 | } 125 | 126 | func (ldc *LoopbackDataChannel) Left() *LoopbackDataChannelWrapper { 127 | ldc.mtx.Lock() 128 | defer ldc.mtx.Unlock() 129 | if ldc.left == nil { 130 | ldc.left = NewLoopbackDataChannelWrapper( 131 | ldc, ldc.c1Rd, ldc.c2Wr, ldc.getRawLogger().WithField("side", "left")) 132 | } 133 | return ldc.left 134 | } 135 | 136 | func (ldc *LoopbackDataChannel) Right() *LoopbackDataChannelWrapper { 137 | ldc.mtx.Lock() 138 | defer ldc.mtx.Unlock() 139 | if ldc.right == nil { 140 | ldc.right = NewLoopbackDataChannelWrapper( 141 | ldc, ldc.c2Rd, ldc.c1Wr, ldc.getRawLogger().WithField("side", "right")) 142 | } 143 | return ldc.right 144 | } 145 | 146 | type LoopbackDataChannelWrapper struct { 147 | *LoopbackDataChannel 148 | logger logrus.FieldLogger 149 | reader io.Reader 150 | writer io.Writer 151 | mtx msync.Locker 152 | onOpenHandler func() 153 | onOpenHandlerOnce sync.Once 154 | } 155 | 156 | func NewLoopbackDataChannelWrapper( 157 | ldc *LoopbackDataChannel, 158 | reader io.Reader, writer io.Writer, 159 | logger logrus.FieldLogger, 160 | ) *LoopbackDataChannelWrapper { 161 | return &LoopbackDataChannelWrapper{ 162 | LoopbackDataChannel: ldc, 163 | 164 | reader: reader, 165 | writer: writer, 166 | mtx: msync.NewLock(), 167 | logger: logger, 168 | } 169 | } 170 | 171 | func (w *LoopbackDataChannelWrapper) getLogger() logrus.FieldLogger { 172 | return w.logger.WithFields(logrus.Fields{ 173 | "#instance": "LoopbackDataChannelWrapper", 174 | "label": w.Label(), 175 | }) 176 | } 177 | 178 | func (w *LoopbackDataChannelWrapper) Read(p []byte) (int, error) { 179 | return w.reader.Read(p) 180 | } 181 | 182 | func (w *LoopbackDataChannelWrapper) Write(p []byte) (int, error) { 183 | return w.writer.Write(p) 184 | } 185 | 186 | func (w *LoopbackDataChannelWrapper) onOpen() { 187 | logger := w.getLogger().WithField("#method", "onOpen") 188 | 189 | w.mtx.Lock() 190 | handler := w.onOpenHandler 191 | w.mtx.Unlock() 192 | 193 | if handler != nil { 194 | w.onOpenHandlerOnce.Do(func() { 195 | go func() { 196 | handler() 197 | logger.Tracef("data channel opened") 198 | }() 199 | }) 200 | } 201 | } 202 | 203 | func (w *LoopbackDataChannelWrapper) OnOpen(f func()) { 204 | logger := w.getLogger().WithField("#method", "OnOpen") 205 | 206 | w.mtx.Lock() 207 | w.onOpenHandlerOnce = sync.Once{} 208 | w.onOpenHandler = f 209 | w.mtx.Unlock() 210 | 211 | if w.State() == transport.DataChannelStateOpen { 212 | w.onOpenHandlerOnce.Do(func() { 213 | go func() { 214 | f() 215 | logger.Tracef("data channel opened") 216 | }() 217 | }) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /pkg/teleportation/sink.go: -------------------------------------------------------------------------------- 1 | package teleportation 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "sync" 8 | 9 | "github.com/sirupsen/logrus" 10 | "golang.org/x/sync/errgroup" 11 | 12 | "github.com/PeerXu/meepo/pkg/ofn" 13 | "github.com/PeerXu/meepo/pkg/transport" 14 | mgroup "github.com/PeerXu/meepo/pkg/util/group" 15 | ) 16 | 17 | type TeleportationSink struct { 18 | opt ofn.Option 19 | logger logrus.FieldLogger 20 | 21 | name string 22 | source net.Addr 23 | sink net.Addr 24 | transport transport.Transport 25 | datachannels map[string]transport.DataChannel 26 | 27 | newDial NewDial 28 | 29 | onDoTeleportFunc OnDoTeleportFunc 30 | onCloseHandler OnCloseHandler 31 | onErrorHandler OnErrorHandler 32 | 33 | datachannelsMtx sync.Mutex 34 | } 35 | 36 | func (ts *TeleportationSink) getLogger() logrus.FieldLogger { 37 | return ts.logger.WithFields(logrus.Fields{ 38 | "#instance": "TeleportationSink", 39 | "name": ts.Name(), 40 | }) 41 | } 42 | 43 | func (ts *TeleportationSink) onError(err error) { 44 | if ts.onErrorHandler != nil { 45 | ts.onErrorHandler(err) 46 | } 47 | 48 | ts.close() 49 | } 50 | 51 | func (ts *TeleportationSink) close() error { 52 | var eg errgroup.Group 53 | 54 | ts.datachannelsMtx.Lock() 55 | for _, dc := range ts.datachannels { 56 | eg.Go(dc.Close) 57 | } 58 | ts.datachannelsMtx.Unlock() 59 | 60 | return eg.Wait() 61 | } 62 | 63 | func (ts *TeleportationSink) OnDoTeleport(label string) error { 64 | logger := ts.getLogger().WithFields(logrus.Fields{ 65 | "#method": "OnDoTeleport", 66 | "label": label, 67 | }) 68 | 69 | sink := ts.Sink() 70 | conn, err := ts.newDial(sink.Network(), sink.String()) 71 | if err != nil { 72 | logger.WithError(err).Debugf("failed to dial to sink") 73 | return err 74 | } 75 | logger.Tracef("dial") 76 | 77 | tp := ts.Transport() 78 | tp.OnDataChannelCreate(label, func(dc transport.DataChannel) { 79 | rg := mgroup.NewRaceGroupFunc() 80 | 81 | outerDataChannelCloser := dc.Close 82 | defer func() { 83 | if outerDataChannelCloser != nil { 84 | outerDataChannelCloser() 85 | } 86 | }() 87 | 88 | dc.OnOpen(func() { 89 | innerLogger := ts.getLogger().WithFields(logrus.Fields{ 90 | "#method": "dataChannelCopyLoop", 91 | "label": dc.Label(), 92 | }) 93 | 94 | rg.Go(func() (interface{}, error) { 95 | _, err := io.Copy(dc, conn) 96 | innerLogger.WithError(err).Tracef("conn->dc closed") 97 | return nil, err 98 | }) 99 | rg.Go(func() (interface{}, error) { 100 | _, err := io.Copy(conn, dc) 101 | innerLogger.WithError(err).Tracef("conn<-dc closed") 102 | return nil, err 103 | }) 104 | go func() { 105 | _, err := rg.Wait() 106 | innerLogger.WithError(dc.Close()).Tracef("datachannel closed") 107 | innerLogger.WithError(conn.Close()).Tracef("conn closed") 108 | 109 | ts.datachannelsMtx.Lock() 110 | delete(ts.datachannels, dc.Label()) 111 | ts.datachannelsMtx.Unlock() 112 | innerLogger.Tracef("remove from data channels") 113 | 114 | innerLogger.WithError(err).Tracef("done") 115 | }() 116 | logger.Tracef("data channel opened") 117 | }) 118 | 119 | ts.datachannelsMtx.Lock() 120 | ts.datachannels[label] = dc 121 | ts.datachannelsMtx.Unlock() 122 | logger.Tracef("add to data channels") 123 | 124 | outerDataChannelCloser = nil 125 | 126 | logger.Tracef("data channel created") 127 | }) 128 | 129 | return nil 130 | } 131 | 132 | func (ts *TeleportationSink) Name() string { 133 | return ts.name 134 | } 135 | 136 | func (ts *TeleportationSink) Source() net.Addr { 137 | return ts.source 138 | } 139 | 140 | func (ts *TeleportationSink) Sink() net.Addr { 141 | return ts.sink 142 | } 143 | 144 | func (ts *TeleportationSink) Portal() Portal { 145 | return PortalSink 146 | } 147 | 148 | func (ts *TeleportationSink) Transport() transport.Transport { 149 | return ts.transport 150 | } 151 | 152 | func (ts *TeleportationSink) DataChannels() []transport.DataChannel { 153 | var dcs []transport.DataChannel 154 | 155 | ts.datachannelsMtx.Lock() 156 | for _, dc := range ts.datachannels { 157 | dcs = append(dcs, dc) 158 | } 159 | ts.datachannelsMtx.Unlock() 160 | 161 | return dcs 162 | } 163 | 164 | func (ts *TeleportationSink) Close() error { 165 | if ts.onCloseHandler != nil { 166 | ts.onCloseHandler() 167 | } 168 | 169 | return ts.close() 170 | } 171 | 172 | func newNewTeleportationSinkOptions() ofn.Option { 173 | return ofn.NewOption(map[string]interface{}{}) 174 | } 175 | 176 | func NewTeleportationSink(opts ...NewTeleportationSinkOption) (*TeleportationSink, error) { 177 | var logger logrus.FieldLogger 178 | var name string 179 | var source, sink net.Addr 180 | var tp transport.Transport 181 | var newDial NewDial 182 | var odtf OnDoTeleportFunc 183 | var och OnCloseHandler 184 | var oeh OnErrorHandler 185 | var val interface{} 186 | var ok bool 187 | 188 | o := newNewTeleportationSinkOptions() 189 | 190 | for _, opt := range opts { 191 | opt(o) 192 | } 193 | 194 | if logger, ok = o.Get("logger").Inter().(logrus.FieldLogger); !ok { 195 | logger = logrus.New() 196 | } 197 | 198 | if source, ok = o.Get("source").Inter().(net.Addr); !ok { 199 | return nil, fmt.Errorf("Require source") 200 | } 201 | 202 | if sink, ok = o.Get("sink").Inter().(net.Addr); !ok { 203 | return nil, fmt.Errorf("Require sink") 204 | } 205 | 206 | if tp, ok = o.Get("transport").Inter().(transport.Transport); !ok { 207 | return nil, fmt.Errorf("Require transport") 208 | } 209 | 210 | if name, ok = o.Get("name").Inter().(string); !ok { 211 | name = fmt.Sprintf("%s:%s", sink.Network(), sink.String()) 212 | } 213 | 214 | if val = o.Get("newDial").Inter(); val != nil { 215 | newDial = val.(NewDial) 216 | } else { 217 | newDial = net.Dial 218 | } 219 | 220 | if val = o.Get("onDoTeleportFunc").Inter(); val != nil { 221 | odtf = val.(OnDoTeleportFunc) 222 | } 223 | 224 | if val = o.Get("onCloseHandler").Inter(); val != nil { 225 | och = val.(OnCloseHandler) 226 | } 227 | 228 | if val = o.Get("onErrorHandler").Inter(); val != nil { 229 | oeh = val.(OnErrorHandler) 230 | } 231 | 232 | ts := &TeleportationSink{ 233 | opt: o, 234 | logger: logger, 235 | name: name, 236 | source: source, 237 | sink: sink, 238 | transport: tp, 239 | datachannels: make(map[string]transport.DataChannel), 240 | newDial: newDial, 241 | onDoTeleportFunc: odtf, 242 | onCloseHandler: och, 243 | onErrorHandler: oeh, 244 | } 245 | 246 | return ts, nil 247 | } 248 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "crypto/ed25519" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/PeerXu/meepo/cmd/config" 14 | "github.com/PeerXu/meepo/pkg/api" 15 | http_api "github.com/PeerXu/meepo/pkg/api/http" 16 | "github.com/PeerXu/meepo/pkg/meepo" 17 | "github.com/PeerXu/meepo/pkg/signaling" 18 | redis_signaling "github.com/PeerXu/meepo/pkg/signaling/redis" 19 | mcrypt "github.com/PeerXu/meepo/pkg/util/crypt" 20 | mdaemon "github.com/PeerXu/meepo/pkg/util/daemon" 21 | meg "github.com/PeerXu/meepo/pkg/util/group" 22 | ) 23 | 24 | var ( 25 | serveCmd = &cobra.Command{ 26 | Use: "serve [-c config] [-d daemon]", 27 | Aliases: []string{"summon"}, 28 | Short: "Summon a Meepo", 29 | RunE: meepoSummon, 30 | } 31 | ) 32 | 33 | func meepoSummon(cmd *cobra.Command, args []string) error { 34 | fs := cmd.Flags() 35 | configStr, _ := fs.GetString("config") 36 | 37 | cfg, loaded, err := config.Load(configStr) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if fs.Lookup("daemon").Changed { 43 | cfg.Meepo.Daemon, _ = fs.GetBool("daemon") 44 | } 45 | 46 | if fs.Lookup("log-level").Changed { 47 | cfg.Meepo.Log.Level, _ = fs.GetString("log-level") 48 | } 49 | 50 | logger := logrus.New() 51 | logLevel, err := logrus.ParseLevel(cfg.Meepo.Log.Level) 52 | if err != nil { 53 | return err 54 | } 55 | logger.SetLevel(logLevel) 56 | 57 | switch logLevel { 58 | case logrus.PanicLevel, 59 | logrus.FatalLevel, 60 | logrus.ErrorLevel, 61 | logrus.WarnLevel, 62 | logrus.InfoLevel: 63 | gin.SetMode(gin.ReleaseMode) 64 | case logrus.DebugLevel: 65 | case logrus.TraceLevel: 66 | } 67 | 68 | if cfg.Meepo.Daemon { 69 | mdaemon.Daemon() 70 | } 71 | 72 | if !loaded { 73 | logger.Warningf("Config file not found, load default config\n") 74 | } 75 | 76 | if fs.Lookup("identity-file").Changed { 77 | cfg.Meepo.IdentityFile, _ = fs.GetString("identity-file") 78 | } 79 | 80 | var pubk ed25519.PublicKey 81 | var prik ed25519.PrivateKey 82 | if cfg.Meepo.IdentityFile != "" { 83 | pubk, prik, err = mcrypt.LoadEd25519Key(cfg.Meepo.IdentityFile) 84 | if err != nil { 85 | return err 86 | } 87 | } else { 88 | pubk, prik = mcrypt.Ed25519GenerateKey() 89 | } 90 | 91 | signalingEngineOptions := []signaling.NewEngineOption{ 92 | signaling.WithID(meepo.Ed25519PublicKeyToMeepoID(pubk)), 93 | signaling.WithLogger(logger), 94 | } 95 | 96 | switch cfg.Meepo.Signaling.Name { 97 | case "redis": 98 | rsCfg := cfg.Meepo.SignalingI.(*config.RedisSignalingConfig) 99 | signalingEngineOptions = append( 100 | signalingEngineOptions, 101 | redis_signaling.WithURL(rsCfg.URL), 102 | ) 103 | } 104 | 105 | signalingEngine, err := signaling.NewEngine(cfg.Meepo.Signaling.Name, signalingEngineOptions...) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | newMeepoOptions := []meepo.NewMeepoOption{ 111 | meepo.WithSignalingEngine(signalingEngine), 112 | meepo.WithLogger(logger), 113 | meepo.WithED25519KeyPair(pubk, prik), 114 | meepo.WithICEServers(cfg.Meepo.TransportI.(*config.WebrtcTransportConfig).ICEServers), 115 | } 116 | 117 | if cfg.Meepo.AsSignaling { 118 | newMeepoOptions = append(newMeepoOptions, meepo.WithAsSignaling(true)) 119 | } 120 | 121 | switch cfg.Meepo.Auth.Name { 122 | case "secret": 123 | sa := cfg.Meepo.AuthI.(*config.SecretAuthConfig) 124 | newMeepoOptions = append( 125 | newMeepoOptions, 126 | meepo.WithAuthorizationName("secret"), 127 | meepo.WithAuthorizationSecret(sa.Secret), 128 | ) 129 | case "dummy": 130 | fallthrough 131 | default: 132 | newMeepoOptions = append( 133 | newMeepoOptions, 134 | meepo.WithAuthorizationName("dummy"), 135 | ) 136 | } 137 | 138 | var allows, blocks []meepo.AclPolicy 139 | if cfg.Meepo.Acl != nil { 140 | if allows, err = meepo.ParseAclPolicies(cfg.Meepo.Acl.Allows); err != nil { 141 | return err 142 | } 143 | if blocks, err = meepo.ParseAclPolicies(cfg.Meepo.Acl.Blocks); err != nil { 144 | return err 145 | } 146 | } 147 | newMeepoOptions = append( 148 | newMeepoOptions, 149 | meepo.WithAcl(meepo.NewAcl(meepo.WithAllowPolicies(allows), meepo.WithBlockPolicies(blocks))), 150 | ) 151 | 152 | mp, err := meepo.NewMeepo(newMeepoOptions...) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | apiCfg := cfg.Meepo.ApiI.(*config.HttpApiConfig) 158 | api, err := api.NewServer( 159 | "http", 160 | http_api.WithHost(apiCfg.Host), 161 | http_api.WithPort(apiCfg.Port), 162 | api.WithMeepo(mp), 163 | ) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | if err = api.Start(context.TODO()); err != nil { 169 | return err 170 | } 171 | logger.Infof("api server startd") 172 | 173 | var socks5 meepo.Socks5Server 174 | if cfg.Meepo.Proxy != nil && cfg.Meepo.Proxy.Socks5 != nil { 175 | socks5Cfg := cfg.Meepo.Proxy.Socks5 176 | socks5, err = meepo.NewSocks5Server( 177 | meepo.WithMeepo(mp), 178 | meepo.WithHost(socks5Cfg.Host), 179 | meepo.WithPort(socks5Cfg.Port), 180 | ) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | if err = socks5.Start(context.TODO()); err != nil { 186 | return err 187 | } 188 | logger.Infof("socks5 server started") 189 | } 190 | 191 | go func() { 192 | c := make(chan os.Signal, 1) 193 | signal.Notify(c, os.Interrupt) 194 | 195 | <-c 196 | 197 | if err = api.Stop(context.TODO()); err != nil { 198 | logger.WithError(err).Errorf("failed to stop api server") 199 | } else { 200 | logger.Debugf("api server terminating") 201 | } 202 | 203 | if socks5 != nil { 204 | if err = socks5.Stop(context.TODO()); err != nil { 205 | logger.WithError(err).Errorf("failed to stop socks5 server") 206 | } else { 207 | logger.Debugf("socks5 server terminating") 208 | } 209 | } 210 | }() 211 | 212 | eg := meg.NewAllGroupFunc() 213 | 214 | eg.Go(func() (interface{}, error) { 215 | er := api.Wait() 216 | logger.WithError(er).Debugf("api server terminated") 217 | return nil, er 218 | }) 219 | 220 | if socks5 != nil { 221 | eg.Go(func() (interface{}, error) { 222 | er := socks5.Wait() 223 | logger.WithError(er).Debugf("socks5 server terminated") 224 | return nil, er 225 | }) 226 | } 227 | 228 | _, err = eg.Wait() 229 | logger.WithError(err).Infof("meepo terminated") 230 | 231 | return nil 232 | } 233 | 234 | func init() { 235 | rootCmd.AddCommand(serveCmd) 236 | 237 | serveCmd.PersistentFlags().StringP("config", "c", config.GetDefaultConfigPath(), "Location of meepo config file") 238 | serveCmd.PersistentFlags().BoolP("daemon", "d", true, "Run as daemon") 239 | serveCmd.PersistentFlags().StringP("identity-file", "i", "", "Select a file from which the identity for Meepo to read") 240 | } 241 | -------------------------------------------------------------------------------- /pkg/meepo/crypt.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/ed25519" 7 | "crypto/rand" 8 | "encoding/base64" 9 | "io" 10 | 11 | "github.com/pion/webrtc/v3" 12 | "golang.org/x/crypto/bcrypt" 13 | 14 | "github.com/PeerXu/meepo/pkg/meepo/packet" 15 | "github.com/PeerXu/meepo/pkg/signaling" 16 | "github.com/PeerXu/meepo/pkg/util/base36" 17 | mcrypt "github.com/PeerXu/meepo/pkg/util/crypt" 18 | "github.com/PeerXu/meepo/pkg/util/msgpack" 19 | ) 20 | 21 | func b64DecodeStringFromMap(k string, m map[string]interface{}) (r []byte, err error) { 22 | v, ok := m[k] 23 | if !ok { 24 | err = ErrNotFound 25 | return 26 | } 27 | 28 | s, ok := v.(string) 29 | if !ok { 30 | err = ErrUnexpectedType 31 | return 32 | } 33 | 34 | r, err = base64.StdEncoding.DecodeString(s) 35 | 36 | return 37 | } 38 | 39 | func generateRandomEd25519KeyPair() (pubk ed25519.PublicKey, prik ed25519.PrivateKey) { 40 | pubk, prik, _ = ed25519.GenerateKey(rand.Reader) 41 | return 42 | } 43 | 44 | func generateGcmNonce() []byte { 45 | nonce := make([]byte, 12) 46 | io.ReadFull(rand.Reader, nonce) 47 | return nonce 48 | } 49 | 50 | const ( 51 | MEEPO_ID_MAGIC_CODE = byte(0x22) 52 | ) 53 | 54 | func Ed25519PublicKeyToMeepoID(pubk ed25519.PublicKey) string { 55 | return base36.Encode(append([]byte{MEEPO_ID_MAGIC_CODE}, pubk...)) 56 | } 57 | 58 | func MeepoIDToEd25519PublicKey(peerID string) (pubk ed25519.PublicKey, err error) { 59 | buf := base36.Decode(peerID) 60 | if len(buf) == 0 { 61 | return nil, ErrInvalidPeerID 62 | } 63 | 64 | if buf[0] != MEEPO_ID_MAGIC_CODE { 65 | return nil, ErrInvalidPeerID 66 | } 67 | 68 | return ed25519.PublicKey(buf[1:]), nil 69 | } 70 | 71 | func (mp *Meepo) newGCM(secret []byte) (cipher.AEAD, error) { 72 | blk, err := aes.NewCipher(secret) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return cipher.NewGCM(blk) 78 | } 79 | 80 | func (mp *Meepo) signDescriptor(d *signaling.Descriptor) error { 81 | buf, err := msgpack.Marshal(d) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | sig := ed25519.Sign(mp.prik, buf) 87 | d.UserData["sig"] = base64.StdEncoding.EncodeToString(sig) 88 | 89 | return nil 90 | } 91 | 92 | func (mp *Meepo) verifyDescriptor(peerID string, d *signaling.Descriptor) (err error) { 93 | peerPubk, err := MeepoIDToEd25519PublicKey(peerID) 94 | if err != nil { 95 | return 96 | } 97 | 98 | sig, err := b64DecodeStringFromMap("sig", d.UserData) 99 | if err != nil { 100 | return 101 | } 102 | 103 | delete(d.UserData, "sig") 104 | 105 | buf, err := msgpack.Marshal(d) 106 | if err != nil { 107 | return 108 | } 109 | 110 | if !ed25519.Verify(peerPubk, buf, sig) { 111 | return ErrIncorrectSignature 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (mp *Meepo) marshalRequestDescriptor(peerID string, offer *webrtc.SessionDescription) (req *signaling.Descriptor, gcm cipher.AEAD, nonce []byte, err error) { 118 | randPubk, randPrik := generateRandomEd25519KeyPair() 119 | 120 | peerPubk, err := MeepoIDToEd25519PublicKey(peerID) 121 | if err != nil { 122 | return 123 | } 124 | 125 | nonce = generateGcmNonce() 126 | secret := mcrypt.CalcSharedSecret(peerPubk, randPrik) 127 | gcm, err = mp.newGCM(secret[:]) 128 | if err != nil { 129 | return 130 | } 131 | 132 | plaintext, err := msgpack.Marshal(offer) 133 | if err != nil { 134 | return 135 | } 136 | 137 | ciphertext := gcm.Seal(nil, nonce, plaintext, nil) 138 | 139 | req = &signaling.Descriptor{ 140 | ID: mp.GetID(), 141 | UserData: map[string]interface{}{ 142 | "randPubk": base64.StdEncoding.EncodeToString(randPubk), 143 | "ct": base64.StdEncoding.EncodeToString(ciphertext), 144 | "nonce": base64.StdEncoding.EncodeToString(nonce), 145 | }, 146 | } 147 | 148 | if err = mp.signDescriptor(req); err != nil { 149 | return 150 | } 151 | 152 | return 153 | } 154 | 155 | func (mp *Meepo) unmarshalResponseDescriptor(res *signaling.Descriptor, gcm cipher.AEAD, nonce []byte) (answer *webrtc.SessionDescription, err error) { 156 | err = mp.verifyDescriptor(res.ID, res) 157 | if err != nil { 158 | return 159 | } 160 | 161 | ciphertext, err := b64DecodeStringFromMap("ct", res.UserData) 162 | if err != nil { 163 | return 164 | } 165 | 166 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 167 | if err != nil { 168 | return 169 | } 170 | 171 | if err = msgpack.Unmarshal(plaintext, &answer); err != nil { 172 | return 173 | } 174 | 175 | return 176 | } 177 | 178 | func (mp *Meepo) unmarshalRequestDescriptor(req *signaling.Descriptor) (offer *webrtc.SessionDescription, gcm cipher.AEAD, nonce []byte, err error) { 179 | peerID := req.ID 180 | if err = mp.verifyDescriptor(peerID, req); err != nil { 181 | return 182 | } 183 | 184 | bRandPubk, err := b64DecodeStringFromMap("randPubk", req.UserData) 185 | if err != nil { 186 | return 187 | } 188 | randPubk := ed25519.PublicKey(bRandPubk) 189 | 190 | if nonce, err = b64DecodeStringFromMap("nonce", req.UserData); err != nil { 191 | return 192 | } 193 | 194 | ciphertext, err := b64DecodeStringFromMap("ct", req.UserData) 195 | if err != nil { 196 | return 197 | } 198 | 199 | secret := mcrypt.CalcSharedSecret(randPubk, mp.prik) 200 | 201 | if gcm, err = mp.newGCM(secret[:]); err != nil { 202 | return 203 | } 204 | 205 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 206 | if err != nil { 207 | return 208 | } 209 | 210 | if err = msgpack.Unmarshal(plaintext, &offer); err != nil { 211 | return 212 | } 213 | 214 | return 215 | } 216 | 217 | func (mp *Meepo) marshalResponseDescriptor(answer *webrtc.SessionDescription, gcm cipher.AEAD, nonce []byte) (res *signaling.Descriptor, err error) { 218 | plaintext, err := msgpack.Marshal(answer) 219 | if err != nil { 220 | return 221 | } 222 | 223 | ciphertext := gcm.Seal(nil, nonce, plaintext, nil) 224 | 225 | res = &signaling.Descriptor{ 226 | ID: mp.GetID(), 227 | UserData: map[string]interface{}{ 228 | "ct": base64.StdEncoding.EncodeToString(ciphertext), 229 | }, 230 | } 231 | 232 | if err = mp.signDescriptor(res); err != nil { 233 | return 234 | } 235 | 236 | return 237 | } 238 | 239 | func (mp *Meepo) signPacket(in packet.Packet) (out packet.Packet, err error) { 240 | var buf []byte 241 | if buf, err = packet.MarshalPacket(in); err != nil { 242 | return 243 | } 244 | 245 | out = in.SetSignature(ed25519.Sign(mp.prik, buf)) 246 | 247 | return 248 | } 249 | 250 | func (mp *Meepo) verifyPacket(p packet.Packet) error { 251 | pubk, err := MeepoIDToEd25519PublicKey(p.Header().Source()) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | sig := p.Header().Signature() 257 | pp := p.UnsetSignature() 258 | buf, err := packet.MarshalPacket(pp) 259 | 260 | if !ed25519.Verify(pubk, buf, sig) { 261 | return ErrIncorrectSignature 262 | } 263 | 264 | return nil 265 | } 266 | 267 | func hashSecret(secret string) (string, error) { 268 | bHashedSecret, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) 269 | if err != nil { 270 | return "", err 271 | } 272 | 273 | return base64.StdEncoding.EncodeToString(bHashedSecret), nil 274 | } 275 | -------------------------------------------------------------------------------- /pkg/teleportation/source.go: -------------------------------------------------------------------------------- 1 | package teleportation 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "sync" 8 | "sync/atomic" 9 | 10 | "github.com/sirupsen/logrus" 11 | "golang.org/x/sync/errgroup" 12 | 13 | "github.com/PeerXu/meepo/pkg/ofn" 14 | "github.com/PeerXu/meepo/pkg/transport" 15 | mgroup "github.com/PeerXu/meepo/pkg/util/group" 16 | ) 17 | 18 | type TeleportationSource struct { 19 | opt ofn.Option 20 | logger logrus.FieldLogger 21 | idx int32 22 | closed int32 23 | 24 | name string 25 | source net.Addr 26 | sink net.Addr 27 | transport transport.Transport 28 | datachannels map[string]transport.DataChannel 29 | 30 | dialRequests chan *DialRequest 31 | 32 | doTeleportFunc DoTeleportFunc 33 | onCloseHandler OnCloseHandler 34 | onErrorHandler OnErrorHandler 35 | 36 | datachannelsMtx sync.Mutex 37 | lisMtx sync.Mutex 38 | } 39 | 40 | func (ts *TeleportationSource) getLogger() logrus.FieldLogger { 41 | return ts.logger.WithFields(logrus.Fields{ 42 | "#instance": "TeleportationSource", 43 | "name": ts.Name(), 44 | }) 45 | } 46 | 47 | func (ts *TeleportationSource) onError(err error) { 48 | defer ts.close() 49 | 50 | if ts.onErrorHandler != nil { 51 | ts.onErrorHandler(err) 52 | } 53 | } 54 | 55 | func (ts *TeleportationSource) close() error { 56 | if atomic.SwapInt32(&ts.closed, 1) == 1 { 57 | return nil 58 | } 59 | 60 | var eg errgroup.Group 61 | 62 | ts.datachannelsMtx.Lock() 63 | for _, dc := range ts.datachannels { 64 | eg.Go(dc.Close) 65 | } 66 | ts.datachannelsMtx.Unlock() 67 | 68 | return eg.Wait() 69 | } 70 | 71 | func (ts *TeleportationSource) requestLoop() { 72 | logger := ts.getLogger().WithField("#method", "fetchConnLoop") 73 | 74 | defer logger.Tracef("exited") 75 | 76 | for { 77 | dr, ok := <-ts.dialRequests 78 | if !ok { 79 | return 80 | } 81 | go ts.onDial(dr) 82 | } 83 | } 84 | 85 | func (ts *TeleportationSource) onDial(dr *DialRequest) { 86 | var dc transport.DataChannel 87 | var err error 88 | 89 | conn := dr.Conn 90 | rg := mgroup.NewRaceGroupFunc() 91 | logger := ts.getLogger().WithField("#method", "onDial") 92 | 93 | outerConnCloser := conn.Close 94 | defer func() { 95 | if outerConnCloser != nil { 96 | outerConnCloser() 97 | } 98 | }() 99 | 100 | tp := ts.Transport() 101 | 102 | idx := atomic.AddInt32(&ts.idx, 1) 103 | label := fmt.Sprintf("%s:%d", ts.Name(), idx) 104 | 105 | if err := ts.doTeleportFunc(label); err != nil { 106 | logger.WithError(err).Debugf("failed to do teleport func") 107 | rg.Go(mgroup.DONE) 108 | return 109 | } 110 | logger.Tracef("do teleport func") 111 | 112 | dc, err = tp.CreateDataChannel( 113 | label, 114 | transport.WithOrdered(true), 115 | ) 116 | if err != nil { 117 | logger.WithError(err).Debugf("failed to create data channel") 118 | rg.Go(mgroup.DONE) 119 | return 120 | } 121 | outerDataChannelCloser := dc.Close 122 | defer func() { 123 | if outerDataChannelCloser != nil { 124 | outerDataChannelCloser() 125 | } 126 | }() 127 | logger.Tracef("create data channel") 128 | 129 | dc.OnOpen(func() { 130 | innerLogger := ts.getLogger().WithFields(logrus.Fields{ 131 | "#method": "dataChannelCopyLoop", 132 | "label": dc.Label(), 133 | }) 134 | 135 | rg.Go(func() (interface{}, error) { 136 | _, err := io.Copy(dc, conn) 137 | innerLogger.WithError(err).Tracef("conn->dc closed") 138 | return nil, err 139 | }) 140 | rg.Go(func() (interface{}, error) { 141 | _, err := io.Copy(conn, dc) 142 | innerLogger.WithError(err).Tracef("conn<-dc closed") 143 | return nil, err 144 | }) 145 | go func() { 146 | _, err := rg.Wait() 147 | innerLogger.WithError(conn.Close()).Tracef("conn closed") 148 | innerLogger.WithError(dc.Close()).Tracef("datachannel closed") 149 | 150 | ts.datachannelsMtx.Lock() 151 | delete(ts.datachannels, dc.Label()) 152 | ts.datachannelsMtx.Unlock() 153 | innerLogger.Tracef("remove from data channels") 154 | 155 | if dr.Quit != nil { 156 | close(dr.Quit) 157 | innerLogger.Tracef("send quit signal") 158 | } 159 | 160 | innerLogger.WithError(err).Tracef("done") 161 | }() 162 | 163 | logger.Tracef("data channel opened") 164 | }) 165 | 166 | ts.datachannelsMtx.Lock() 167 | ts.datachannels[label] = dc 168 | ts.datachannelsMtx.Unlock() 169 | logger.Tracef("add to data channels") 170 | 171 | outerConnCloser = nil 172 | outerDataChannelCloser = nil 173 | 174 | logger.Tracef("done") 175 | } 176 | 177 | func (ts *TeleportationSource) Name() string { 178 | return ts.name 179 | } 180 | 181 | func (ts *TeleportationSource) Source() net.Addr { 182 | return ts.source 183 | } 184 | 185 | func (ts *TeleportationSource) Sink() net.Addr { 186 | return ts.sink 187 | } 188 | 189 | func (ts *TeleportationSource) Portal() Portal { 190 | return PortalSource 191 | } 192 | 193 | func (ts *TeleportationSource) Transport() transport.Transport { 194 | return ts.transport 195 | } 196 | 197 | func (ts *TeleportationSource) DataChannels() []transport.DataChannel { 198 | var dcs []transport.DataChannel 199 | 200 | ts.datachannelsMtx.Lock() 201 | for _, dc := range ts.datachannels { 202 | dcs = append(dcs, dc) 203 | } 204 | ts.datachannelsMtx.Unlock() 205 | 206 | return dcs 207 | } 208 | 209 | func (ts *TeleportationSource) Close() error { 210 | if ts.onCloseHandler != nil { 211 | ts.onCloseHandler() 212 | } 213 | 214 | return ts.close() 215 | } 216 | 217 | func newNewteleportationSourceOptions() ofn.Option { 218 | return ofn.NewOption(map[string]interface{}{}) 219 | } 220 | 221 | func NewTeleportationSource(opts ...NewTeleportationSourceOption) (*TeleportationSource, error) { 222 | var logger logrus.FieldLogger 223 | var name string 224 | var source, sink net.Addr 225 | var tp transport.Transport 226 | var ok bool 227 | var dtf DoTeleportFunc 228 | var och OnCloseHandler 229 | var oeh OnErrorHandler 230 | var drc chan *DialRequest 231 | var val interface{} 232 | 233 | o := newNewteleportationSourceOptions() 234 | 235 | for _, opt := range opts { 236 | opt(o) 237 | } 238 | 239 | if logger, ok = o.Get("logger").Inter().(logrus.FieldLogger); !ok { 240 | logger = logrus.New() 241 | } 242 | 243 | if source, ok = o.Get("source").Inter().(net.Addr); !ok { 244 | return nil, fmt.Errorf("Require source") 245 | } 246 | 247 | if sink, ok = o.Get("sink").Inter().(net.Addr); !ok { 248 | return nil, fmt.Errorf("Require sink") 249 | } 250 | 251 | if tp, ok = o.Get("transport").Inter().(transport.Transport); !ok { 252 | return nil, fmt.Errorf("Require transport") 253 | } 254 | 255 | if name, ok = o.Get("name").Inter().(string); !ok { 256 | name = fmt.Sprintf("%s:%s", sink.Network(), sink.String()) 257 | } 258 | 259 | if val = o.Get("doTeleportFunc").Inter(); val != nil { 260 | dtf = val.(DoTeleportFunc) 261 | } 262 | 263 | if val = o.Get("onCloseHandler").Inter(); val != nil { 264 | och = val.(OnCloseHandler) 265 | } 266 | 267 | if val = o.Get("onErrorHandler").Inter(); val != nil { 268 | oeh = val.(OnErrorHandler) 269 | } 270 | 271 | if val = o.Get("dialRequestChannel").Inter(); val != nil { 272 | drc = val.(chan *DialRequest) 273 | } 274 | 275 | ts := &TeleportationSource{ 276 | opt: o, 277 | logger: logger, 278 | name: name, 279 | source: source, 280 | sink: sink, 281 | transport: tp, 282 | datachannels: make(map[string]transport.DataChannel), 283 | doTeleportFunc: dtf, 284 | onCloseHandler: och, 285 | onErrorHandler: oeh, 286 | dialRequests: drc, 287 | } 288 | 289 | go ts.requestLoop() 290 | 291 | return ts, nil 292 | } 293 | -------------------------------------------------------------------------------- /pkg/meepo/acl.go: -------------------------------------------------------------------------------- 1 | package meepo 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/PeerXu/meepo/pkg/ofn" 9 | ) 10 | 11 | var ( 12 | ACL_ANY string = "*" 13 | ACL_ANY_ADDR = &aclAddr{n: "*", s: "*"} 14 | ACL_ANY_ENTITY = NewAclEntity(ACL_ANY, ACL_ANY_ADDR) 15 | ) 16 | 17 | type aclAddr struct { 18 | n string 19 | s string 20 | } 21 | 22 | func (a *aclAddr) Network() string { 23 | return a.n 24 | } 25 | 26 | func (a *aclAddr) String() string { 27 | return a.s 28 | } 29 | 30 | func NewAclAddr(n, s string) net.Addr { 31 | return &aclAddr{n: n, s: s} 32 | } 33 | 34 | func AddrContains(a, b net.Addr) bool { 35 | if a.Network() != ACL_ANY && a.Network() != b.Network() { 36 | return false 37 | } 38 | 39 | if a.String() == ACL_ANY { 40 | return true 41 | } 42 | 43 | aHost, aPort, err := net.SplitHostPort(a.String()) 44 | if err != nil { 45 | return false 46 | } 47 | 48 | bHost, bPort, err := net.SplitHostPort(b.String()) 49 | if err != nil { 50 | return false 51 | } 52 | 53 | if aPort != ACL_ANY && aPort != bPort { 54 | return false 55 | } 56 | 57 | if aHost != ACL_ANY && aHost != bHost { 58 | if _, aIPNet, err := net.ParseCIDR(aHost); err != nil { 59 | if aHost != bHost { 60 | return false 61 | } 62 | } else { 63 | if !aIPNet.Contains(net.ParseIP(bHost)) { 64 | return false 65 | } 66 | } 67 | } 68 | 69 | return true 70 | } 71 | 72 | type AclEntity interface { 73 | ID() string 74 | Addr() net.Addr 75 | Contains(AclEntity) bool 76 | String() string 77 | } 78 | 79 | type aclEntity struct { 80 | id string 81 | addr net.Addr 82 | } 83 | 84 | func NewAclEntity(id string, addr net.Addr) AclEntity { 85 | return &aclEntity{ 86 | id: id, 87 | addr: addr, 88 | } 89 | } 90 | 91 | func (e *aclEntity) ID() string { 92 | return e.id 93 | } 94 | 95 | func (e *aclEntity) Addr() net.Addr { 96 | return e.addr 97 | } 98 | 99 | func (a *aclEntity) Contains(b AclEntity) bool { 100 | if a.ID() != ACL_ANY && a.ID() != b.ID() { 101 | return false 102 | } 103 | 104 | if !AddrContains(a.Addr(), b.Addr()) { 105 | return false 106 | } 107 | 108 | return true 109 | } 110 | 111 | func (e *aclEntity) String() string { 112 | return fmt.Sprintf("", e.ID(), e.Addr().Network(), e.Addr().String()) 113 | } 114 | 115 | type AclChallenge interface { 116 | Source() AclEntity 117 | Destination() AclEntity 118 | String() string 119 | } 120 | 121 | type aclChallenge struct { 122 | source AclEntity 123 | destination AclEntity 124 | } 125 | 126 | func (c *aclChallenge) Source() AclEntity { 127 | return c.source 128 | } 129 | 130 | func (c *aclChallenge) Destination() AclEntity { 131 | return c.destination 132 | } 133 | 134 | func (c *aclChallenge) String() string { 135 | return fmt.Sprintf("", c.Source(), c.Destination()) 136 | } 137 | 138 | func NewAclChallenge(source, destination AclEntity) AclChallenge { 139 | return &aclChallenge{ 140 | source: source, 141 | destination: destination, 142 | } 143 | } 144 | 145 | type AclPolicy interface { 146 | Source() AclEntity 147 | Destination() AclEntity 148 | Contains(AclChallenge) bool 149 | String() string 150 | } 151 | 152 | // ParseAclPolicy: 153 | // Policy: [[[:]:]:,][[:]:]: 154 | // Rules: 155 | // src-addr-network and dst-addr-network only support tcp or socks5 now. 156 | // Examples: 157 | // * => *:*:*:*,*:*:*:* 158 | // 192.168.1.1:22 => *:*:*:*,*:*:192.168.1.1:22 159 | // 10.0.0.0/24:* => *:*:*:*,*:*:10.0.0.0/24:* 160 | // tcp:10.0.0.1:22 => *:*:*:*,*:tcp:10.0.0.1:22 161 | // a:*:10.0.0.1:22 => *:*:*:*,a:*:10.0.0.1:22 162 | // socks5:*:*,127.0.0.1:80 => *:socks5:*:*,*:*:127.0.0.1:80 163 | func ParseAclPolicy(s string) (p AclPolicy, err error) { 164 | var src, dst AclEntity 165 | 166 | ss := strings.SplitN(s, ",", 2) 167 | switch len(ss) { 168 | case 0: 169 | return nil, ErrInvalidAclPolicyString 170 | case 1: 171 | src = ACL_ANY_ENTITY 172 | if dst, err = parseAclEntity(ss[0]); err != nil { 173 | return nil, err 174 | } 175 | case 2: 176 | if src, err = parseAclEntity(ss[0]); err != nil { 177 | return nil, err 178 | } 179 | if dst, err = parseAclEntity(ss[1]); err != nil { 180 | return nil, err 181 | } 182 | default: 183 | return nil, ErrInvalidAclPolicyString 184 | } 185 | 186 | p = NewAclPolicy(src, dst) 187 | return 188 | } 189 | 190 | func ParseAclPolicies(ss []string) (ps []AclPolicy, err error) { 191 | for _, s := range ss { 192 | p, err := ParseAclPolicy(s) 193 | if err != nil { 194 | return nil, err 195 | } 196 | ps = append(ps, p) 197 | } 198 | return ps, nil 199 | } 200 | 201 | func parseAclEntity(s string) (e AclEntity, err error) { 202 | id, network, host, port := ACL_ANY, ACL_ANY, ACL_ANY, ACL_ANY 203 | 204 | ss := strings.SplitN(s, ":", 4) 205 | switch len(ss) { 206 | case 0: 207 | return nil, ErrInvalidAclPolicyString 208 | case 1: 209 | if ss[0] != ACL_ANY { 210 | return nil, ErrInvalidAclPolicyString 211 | } 212 | e = ACL_ANY_ENTITY 213 | case 2: 214 | host = ss[0] 215 | port = ss[1] 216 | case 3: 217 | network = ss[0] 218 | host = ss[1] 219 | port = ss[2] 220 | case 4: 221 | id = ss[0] 222 | network = ss[1] 223 | host = ss[2] 224 | port = ss[3] 225 | default: 226 | return nil, ErrInvalidAclPolicyString 227 | } 228 | 229 | if e == nil { 230 | e = NewAclEntity(id, NewAclAddr(network, net.JoinHostPort(host, port))) 231 | } 232 | 233 | return 234 | } 235 | 236 | type aclPolicy struct { 237 | source AclEntity 238 | destination AclEntity 239 | } 240 | 241 | func (p *aclPolicy) Source() AclEntity { 242 | return p.source 243 | } 244 | 245 | func (p *aclPolicy) Destination() AclEntity { 246 | return p.destination 247 | } 248 | 249 | func (p *aclPolicy) Contains(c AclChallenge) bool { 250 | return p.Source().Contains(c.Source()) && p.Destination().Contains(c.Destination()) 251 | } 252 | 253 | func (p *aclPolicy) String() string { 254 | return fmt.Sprintf("", p.Source(), p.Destination()) 255 | } 256 | 257 | func NewAclPolicy(source, destination AclEntity) AclPolicy { 258 | return &aclPolicy{ 259 | source: source, 260 | destination: destination, 261 | } 262 | } 263 | 264 | type Acl interface { 265 | Allowed(AclChallenge) error 266 | } 267 | 268 | type chainList struct { 269 | allows []*allowList 270 | blocks []*blockList 271 | } 272 | 273 | func (l *chainList) Allowed(c AclChallenge) error { 274 | for _, b := range l.blocks { 275 | if err := b.Allowd(c); err != nil { 276 | return err 277 | } 278 | } 279 | 280 | for _, a := range l.allows { 281 | if err := a.Allowed(c); err == nil { 282 | return nil 283 | } 284 | } 285 | 286 | return ErrAclNotAllowed 287 | } 288 | 289 | type allowList struct { 290 | policies []AclPolicy 291 | } 292 | 293 | func (l *allowList) Allowed(c AclChallenge) error { 294 | for _, p := range l.policies { 295 | if p.Contains(c) { 296 | return nil 297 | } 298 | } 299 | 300 | return ErrAclNotAllowed 301 | } 302 | 303 | type blockList struct { 304 | policies []AclPolicy 305 | } 306 | 307 | func (l *blockList) Allowd(c AclChallenge) error { 308 | for _, p := range l.policies { 309 | if p.Contains(c) { 310 | return ErrAclNotAllowed 311 | } 312 | } 313 | 314 | return nil 315 | } 316 | 317 | type NewAclOption = ofn.OFN 318 | 319 | func WithAllowPolicies(ps []AclPolicy) NewAclOption { 320 | return func(o ofn.Option) { 321 | o["allowPolicies"] = append(o["allowPolicies"].([]AclPolicy), ps...) 322 | } 323 | } 324 | 325 | func WithBlockPolicies(ps []AclPolicy) NewAclOption { 326 | return func(o ofn.Option) { 327 | o["blockPolicies"] = append(o["blockPolicies"].([]AclPolicy), ps...) 328 | } 329 | } 330 | 331 | func defaultNewAclOption() ofn.Option { 332 | return ofn.NewOption(map[string]interface{}{ 333 | "allowPolicies": []AclPolicy{}, 334 | "blockPolicies": []AclPolicy{}, 335 | }) 336 | } 337 | 338 | func NewAcl(opts ...NewAclOption) Acl { 339 | o := defaultNewAclOption() 340 | for _, opt := range opts { 341 | opt(o) 342 | } 343 | 344 | allows := o["allowPolicies"].([]AclPolicy) 345 | blocks := o["blockPolicies"].([]AclPolicy) 346 | 347 | if len(allows) == 0 { 348 | allows, _ = ParseAclPolicies([]string{"*"}) 349 | } 350 | 351 | return &chainList{ 352 | allows: []*allowList{{allows}}, 353 | blocks: []*blockList{{blocks}}, 354 | } 355 | } 356 | --------------------------------------------------------------------------------