├── .gitignore ├── LICENSE ├── README.md ├── bin └── imux │ ├── certs.go │ ├── client.go │ ├── imux.go │ └── server.go ├── chunk.go ├── data_imux.go ├── imux_socket.go ├── many_to_one.go ├── one_to_many.go ├── tlb.go └── write_queue.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Hayden Parker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imux 2 | 3 | imux is a go library and corresponding command line tool for inverse multiplexing sockets 4 | 5 | ![imux](http://i.imgur.com/i52mcL4.png) 6 | 7 | An imux client will create a listener and forward data from any connections to that listener to an imux server, using a configurable number of sockets. An imux server receives data and opens corresponding sockets to the final destination. Encryption is done with self-signed TLS and Trust Of First Use (TOFU). 8 | 9 | ## installation 10 | 11 | ``` 12 | go get github.com/hkparker/imux/... 13 | ``` 14 | 15 | ## example 16 | 17 | let's say you wanted to expose an SSH server over imux 18 | 19 | **server** 20 | 21 | serve imux on `0.0.0.0:443` and connect out to `localhost:22` 22 | 23 | ``` 24 | imux -server --listen=0.0.0.0:443 --dial=localhost:22 25 | ``` 26 | 27 | **client** 28 | 29 | inverse multiplex over 10 sockets bound to any interface and connect to the server 30 | 31 | ``` 32 | imux -client --binds='{"0.0.0.0": 10}' --listen=localhost:22 --dial=server:443 33 | ``` 34 | 35 | now on the client, connect to `localhost:22` to ssh to the sever's `localhost:22` over the imux connection 36 | 37 | ``` 38 | ssh localhost 39 | ``` 40 | 41 | ## multiple routes 42 | 43 | imux can be used to transport a single socket over multiple internet connections using source routing in linux 44 | 45 | For example, consider simultaneously using two interfaces: 46 | 47 | |Interface|Address|Default Gateway| 48 | |:-------:|:-----:|:-------------:| 49 | |`eth0`|`192.168.1.2`|`192.168.1.1`| 50 | |`eth1`|`10.0.0.2`|`10.0.0.1`| 51 | 52 | **create routing tables** 53 | 54 | ``` 55 | echo '128 imux0' >> /etc/iproute2/rt_tables 56 | echo '129 imux1' >> /etc/iproute2/rt_tables 57 | ``` 58 | 59 | **add routes** 60 | 61 | ``` 62 | ip route add default via 192.168.1.1 table imux0 dev eth0 63 | ip route add default via 10.0.0.1 table imux1 dev eth1 64 | ``` 65 | 66 | **add rules** 67 | 68 | ``` 69 | ip rule add from 192.168.1.2 table imux0 70 | ip rule add from 10.0.0.2 table imux1 71 | ``` 72 | 73 | **flush cache** 74 | 75 | ``` 76 | ip route flush cache 77 | ``` 78 | 79 | **connect with binds** 80 | 81 | here we choose 20 sockets on each interface 82 | 83 | ``` 84 | imux -client --binds='{"192.168.1.2": 20, "10.0.0.2": 20}' --listen=localhost:22 --dial=server:443 85 | ``` 86 | -------------------------------------------------------------------------------- /bin/imux/certs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | log "github.com/Sirupsen/logrus" 12 | "math/big" 13 | "net" 14 | "os" 15 | "time" 16 | ) 17 | 18 | // Load or generate a new self-signed TLS certificate 19 | func serverTLSCert(bind string) tls.Certificate { 20 | config_home := os.Getenv("HOME") + "/.imux" 21 | crt_filename := config_home + "/" + bind + ".crt" 22 | key_filename := config_home + "/" + bind + ".key" 23 | _, crt_err := os.Stat(crt_filename) 24 | _, key_err := os.Stat(key_filename) 25 | if os.IsNotExist(crt_err) || os.IsNotExist(key_err) { 26 | // create config directory if needed 27 | _, config_dir_err := os.Stat(config_home) 28 | if config_dir_err != nil { 29 | os.Mkdir(config_home, 0700) 30 | } 31 | 32 | // create new cert, write to files 33 | cn, _, err := net.SplitHostPort(bind) 34 | if err != nil { 35 | log.WithFields(log.Fields{ 36 | "at": "serverTLSPair", 37 | "error": err.Error(), 38 | }).Fatal("invalid bind") 39 | } 40 | cert_data, key_data := selfSignedCert(cn) 41 | cert_file, err := os.OpenFile(crt_filename, os.O_CREATE|os.O_WRONLY, 0600) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | cert_file.Write(cert_data) 46 | cert_file.Close() 47 | key_file, err := os.OpenFile(key_filename, os.O_CREATE|os.O_WRONLY, 0600) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | key_file.Write(key_data) 52 | key_file.Close() 53 | } 54 | certificate, err := tls.LoadX509KeyPair(crt_filename, key_filename) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | return certificate 59 | } 60 | 61 | func randomSerial() *big.Int { 62 | serial_number_limit := new(big.Int).Lsh(big.NewInt(1), 128) 63 | serial_number, err := rand.Int(rand.Reader, serial_number_limit) 64 | if err != nil { 65 | log.Fatalf("failed to generate serial number for self signed certificate: %s", err) 66 | } 67 | return serial_number 68 | } 69 | 70 | func selfSignedCert(cn string) (cert_data []byte, key_data []byte) { 71 | // Self signed certificate for provided hostname 72 | ca := &x509.Certificate{ 73 | SerialNumber: randomSerial(), 74 | Subject: pkix.Name{ 75 | Organization: []string{"imux"}, 76 | OrganizationalUnit: []string{"imux"}, 77 | CommonName: cn, 78 | }, 79 | NotBefore: time.Now(), 80 | NotAfter: time.Now().AddDate(6, 0, 0), 81 | BasicConstraintsValid: true, 82 | IsCA: true, 83 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 84 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 85 | } 86 | 87 | // Generate key 88 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 89 | if err != nil { 90 | log.Fatalf("failed to generate private key for self signed certificate: %s", err) 91 | } 92 | pub := &priv.PublicKey 93 | 94 | // Create Certificate 95 | cert_der, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv) 96 | if err != nil { 97 | log.Fatalf("failed to create self signed certificate: %s", err) 98 | } 99 | 100 | // Create PEM encoding of certificate 101 | var cert_buffer bytes.Buffer 102 | err = pem.Encode(&cert_buffer, &pem.Block{Type: "CERTIFICATE", Bytes: cert_der}) 103 | if err != nil { 104 | log.Fatalf("could not PEM encode certificate data: %s", err) 105 | } 106 | cert_data = cert_buffer.Bytes() 107 | 108 | // Create PEM encoding of key 109 | var key_buffer bytes.Buffer 110 | err = pem.Encode(&key_buffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) 111 | if err != nil { 112 | log.Fatalf("could not PEM encode key data: %s", err) 113 | } 114 | key_data = key_buffer.Bytes() 115 | 116 | return 117 | } 118 | -------------------------------------------------------------------------------- /bin/imux/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha256" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/hex" 9 | "fmt" 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/hkparker/imux" 12 | "net" 13 | "os" 14 | "reflect" 15 | "strings" 16 | ) 17 | 18 | // Dial the TLS server specified in the dial address and 19 | // perform Trust Of First Use, interactively checking if 20 | // the presented certificate is safe and if it should be 21 | // saved in ~/.imux/known_hosts or if the connection 22 | // should be aborted. 23 | func TOFU(dial string) *x509.Certificate { 24 | known_hosts := LoadKnownHosts() 25 | conn, err := tls.Dial( 26 | "tcp", 27 | dial, 28 | &tls.Config{InsecureSkipVerify: true}, 29 | ) 30 | if err != nil { 31 | log.WithFields(log.Fields{ 32 | "at": "TOFU", 33 | "error": err.Error(), 34 | }).Fatal("unable to dial server") 35 | } 36 | signature := SHA256Sig(conn) 37 | 38 | if saved_signature, present := known_hosts[dial]; present { 39 | if signature != saved_signature { 40 | connect, update := MitMWarning(signature, saved_signature) 41 | if !connect { 42 | log.WithFields(log.Fields{ 43 | "at": "TOFU", 44 | }).Fatal("TLS certificate mismatch") 45 | } 46 | if update { 47 | AppendHost(dial, signature) 48 | } 49 | } 50 | } else { 51 | connect, save_cert := TrustDialog(dial, signature) 52 | if !connect { 53 | log.WithFields(log.Fields{ 54 | "at": "TOFU", 55 | }).Fatal("TLS certificate rejected by user") 56 | } else if save_cert { 57 | AppendHost(dial, signature) 58 | } 59 | } 60 | 61 | return conn.ConnectionState().PeerCertificates[0] 62 | } 63 | 64 | // Parse the listen address and return a TCP listsner 65 | func createClientListener(listen string) net.Listener { 66 | listener, err := net.Listen("tcp", listen) 67 | if err != nil { 68 | log.WithFields(log.Fields{ 69 | "at": "createClientListener", 70 | "address": listen, 71 | "error": err.Error(), 72 | }).Fatal("unable to open client listener") 73 | } 74 | return listener 75 | } 76 | 77 | // Create a function that accepts bind address and returns imux.Redialer 78 | // functions that bind to that address and dial the specified dial address 79 | func createRedailerGenerator(dial string, cert *x509.Certificate) imux.RedialerGenerator { 80 | return func(bind string) imux.Redialer { 81 | return func() (net.Conn, error) { 82 | bind_addr, err := net.ResolveTCPAddr("tcp", bind+":0") 83 | if err != nil { 84 | log.WithFields(log.Fields{ 85 | "at": "redailer", 86 | "address": bind, 87 | "error": err.Error(), 88 | }).Error("error parsing bind address") 89 | return nil, err 90 | } 91 | conn, err := tls.DialWithDialer( 92 | &net.Dialer{ 93 | LocalAddr: bind_addr, 94 | }, 95 | "tcp", 96 | dial, 97 | &tls.Config{InsecureSkipVerify: true}, 98 | ) 99 | if err != nil { 100 | log.WithFields(log.Fields{ 101 | "at": "redailer", 102 | "bind": bind, 103 | "address": dial, 104 | "error": err.Error(), 105 | }).Error("error dialing destination") 106 | return conn, err 107 | } 108 | if !reflect.DeepEqual(cert.Signature, conn.ConnectionState().PeerCertificates[0].Signature) { 109 | log.Fatal("SECURITY FATAL ERROR: CERTIFICATE MISMATCH IN IMUX SOCKET!") 110 | } 111 | return conn, err 112 | } 113 | } 114 | } 115 | 116 | func LoadKnownHosts() map[string]string { 117 | sigs := make(map[string]string) 118 | path := os.Getenv("HOME") + "/.imux/" 119 | filename := path + "known_hosts" 120 | if _, err := os.Stat(path); os.IsNotExist(err) { 121 | os.MkdirAll(path, 0755) 122 | os.Create(filename) 123 | return sigs 124 | } 125 | if _, err := os.Stat(filename); os.IsNotExist(err) { 126 | os.Create(filename) 127 | return sigs 128 | } 129 | known_hosts, err := os.Open(filename) 130 | defer known_hosts.Close() 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | scanner := bufio.NewScanner(known_hosts) 135 | for scanner.Scan() { 136 | contents := strings.Split(scanner.Text(), " ") 137 | sigs[contents[0]] = contents[1] 138 | } 139 | return sigs 140 | } 141 | 142 | func MitMWarning(new_signature, old_signature string) (bool, bool) { 143 | fmt.Println(fmt.Sprintf( 144 | "WARNING: Remote certificate has changed!!\nold: %s\nnew: %s", 145 | old_signature, 146 | new_signature, 147 | )) 148 | fmt.Println("[A]bort, [C]ontinue without updating, [U]pdate and continue?") 149 | connect := false 150 | update := false 151 | stdin := bufio.NewReader(os.Stdin) 152 | for { 153 | fmt.Print("> ") 154 | line, _ := stdin.ReadString('\n') 155 | text := strings.TrimSpace(line) 156 | if text == "A" { 157 | break 158 | } else if text == "C" { 159 | connect = true 160 | break 161 | } else if text == "U" { 162 | connect = true 163 | update = true 164 | break 165 | } 166 | } 167 | return connect, update 168 | } 169 | 170 | func AppendHost(hostname string, signature string) { 171 | filename := os.Getenv("HOME") + "/.imux/known_hosts" 172 | known_hosts, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) 173 | if err != nil { 174 | log.Fatal(err) 175 | } 176 | known_hosts.WriteString(hostname + " " + signature + "\n") 177 | known_hosts.Close() 178 | } 179 | 180 | func TrustDialog(hostname, signature string) (bool, bool) { 181 | fmt.Println(fmt.Sprintf( 182 | "%s presents certificate with signature:\n%s", 183 | hostname, 184 | signature, 185 | )) 186 | fmt.Println("[A]bort, [C]ontinue without saving, [S]ave and continue?") 187 | connect := false 188 | save := false 189 | stdin := bufio.NewReader(os.Stdin) 190 | for { 191 | fmt.Print("> ") 192 | line, _ := stdin.ReadString('\n') 193 | text := strings.TrimSpace(line) 194 | if text == "A" { 195 | break 196 | } else if text == "C" { 197 | connect = true 198 | break 199 | } else if text == "S" { 200 | connect = true 201 | save = true 202 | break 203 | } 204 | } 205 | return connect, save 206 | } 207 | 208 | func SHA256Sig(conn *tls.Conn) string { 209 | sig := conn.ConnectionState().PeerCertificates[0].Signature 210 | sha := sha256.Sum256(sig) 211 | str := hex.EncodeToString(sha[:]) 212 | return str 213 | } 214 | -------------------------------------------------------------------------------- /bin/imux/imux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | log "github.com/Sirupsen/logrus" 7 | "github.com/hkparker/imux" 8 | ) 9 | 10 | var client bool 11 | var binds string 12 | var server bool 13 | var listen string 14 | var dial string 15 | var chunk_size int 16 | var debug bool 17 | 18 | func main() { 19 | flag.BoolVar(&client, "client", false, "create an imux client") 20 | flag.StringVar(&binds, "binds", "{\"0.0.0.0\": 10}", "JSON encoding of map from bind address strings to int counts") 21 | flag.BoolVar(&server, "server", false, "create an imux server") 22 | flag.StringVar(&listen, "listen", "0.0.0.0:443", "listener address and port for clients to imux out and servers to imux in") 23 | flag.StringVar(&dial, "dial", "127.0.0.1:443", "dial address and port for clients to dial servers and servers to dial out") 24 | flag.IntVar(&chunk_size, "chunk-size", 16384, "maximum number of bytes per chunk") 25 | flag.BoolVar(&debug, "debug", false, "debug logging") 26 | flag.Parse() 27 | validateFlags() 28 | 29 | if server { 30 | imux.ManyToOne( 31 | createServerListener(listen), 32 | createDestinationDialer(dial), 33 | ) 34 | } else if client { 35 | imux.MaxChunkDataSize = chunk_size 36 | bind_map := make(map[string]int) 37 | err := json.Unmarshal([]byte(binds), &bind_map) 38 | if err != nil { 39 | log.Fatal("invalid binds option") 40 | } 41 | good_cert := TOFU(dial) 42 | imux.OneToMany( 43 | createClientListener(listen), 44 | bind_map, 45 | createRedailerGenerator(dial, good_cert), 46 | ) 47 | } 48 | } 49 | 50 | func validateFlags() { 51 | if client && server { 52 | log.Fatal("cannot be in client and server mode at the same time") 53 | } else if !client && !server { 54 | log.Fatal("must be in client mode or server mode") 55 | } 56 | if debug { 57 | log.SetLevel(log.DebugLevel) 58 | } else { 59 | log.SetLevel(log.WarnLevel) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /bin/imux/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | log "github.com/Sirupsen/logrus" 6 | "net" 7 | ) 8 | 9 | // Create a new listener to accept transport imux sockets 10 | func createServerListener(listen string) net.Listener { 11 | certificate := serverTLSCert(listen) 12 | listener, err := tls.Listen( 13 | "tcp", 14 | listen, 15 | &tls.Config{ 16 | Certificates: []tls.Certificate{ 17 | certificate, 18 | }, 19 | }, 20 | ) 21 | if err != nil { 22 | log.WithFields(log.Fields{ 23 | "at": "createServerListener", 24 | "bind": listen, 25 | "error": err.Error(), 26 | }).Fatal("unable to start server listener") 27 | } 28 | return listener 29 | } 30 | 31 | // Return a function that when called dials the specified address 32 | // and returns the new connection 33 | func createDestinationDialer(dial string) func() (net.Conn, error) { 34 | return func() (net.Conn, error) { 35 | return net.Dial("tcp", dial) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chunk.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "github.com/hkparker/TLB" 6 | "gopkg.in/mgo.v2/bson" 7 | ) 8 | 9 | // A chunk represents a piece of information exchanged between a 10 | // socket on the client side and a socket on the server size. A 11 | // session ID defines sockets that are part of one imux session, 12 | // while the socket ID specifies which socket a chunk should queue 13 | // into, ordered by the Sequence ID. 14 | type Chunk struct { 15 | SessionID string 16 | SocketID string 17 | SequenceID uint64 18 | Data []byte 19 | Close bool 20 | Setup bool 21 | } 22 | 23 | // TLB code to unpack Chunk data into an interface 24 | func buildChunk(data []byte, _ tlb.TLBContext) interface{} { 25 | chunk := &Chunk{} 26 | err := bson.Unmarshal(data, &chunk) 27 | if err != nil { 28 | log.WithFields(log.Fields{ 29 | "at": "BuildChunk", 30 | "error": err.Error(), 31 | }).Error("error unmarshaling chunk data") 32 | return nil 33 | } 34 | log.WithFields(log.Fields{ 35 | "at": "BuildChunk", 36 | "sequence_id": chunk.SequenceID, 37 | "socket_id": chunk.SocketID, 38 | "session_id": chunk.SessionID, 39 | }).Debug("unmarshalled chunk data") 40 | return chunk 41 | } 42 | -------------------------------------------------------------------------------- /data_imux.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "io" 6 | ) 7 | 8 | var MaxChunkDataSize = 16384 9 | 10 | // A DataIMUX will read data from multiple io.Readers and chunk the data 11 | // into a chunk chan. The Stale attribute provides a way to insert chunks 12 | // back into the chan from external sources. 13 | type DataIMUX struct { 14 | Chunks chan Chunk 15 | Stale chan Chunk 16 | SessionID string 17 | } 18 | 19 | // Create a new DataIMUX for a given session 20 | func NewDataIMUX(session_id string) DataIMUX { 21 | log.WithFields(log.Fields{ 22 | "at": "NewDataIMUX", 23 | "session_id": session_id, 24 | }).Debug("creating data imux") 25 | return DataIMUX{ 26 | Chunks: make(chan Chunk, 10), 27 | Stale: make(chan Chunk, 50), 28 | SessionID: session_id, 29 | } 30 | } 31 | 32 | // Read from a new data source in this DataIMUX, create chunks from it tagged with the 33 | // provided socket ID. 34 | func (data_imux *DataIMUX) ReadFrom(id string, conn io.Reader) { 35 | log.WithFields(log.Fields{ 36 | "at": "DataIMUX.ReadFrom", 37 | "socket_id": id, 38 | }).Debug("reading from new data source") 39 | data_imux.Chunks <- Chunk{ 40 | SocketID: id, 41 | SessionID: data_imux.SessionID, 42 | Setup: true, 43 | } 44 | sequence := uint64(1) 45 | for { 46 | chunk_data := make([]byte, MaxChunkDataSize) 47 | read, err := conn.Read(chunk_data) 48 | log.WithFields(log.Fields{ 49 | "at": "DataIMUX.ReadFrom", 50 | "socket_id": id, 51 | "size": read, 52 | }).Debug("read data from data source") 53 | chunk_data = chunk_data[:read] 54 | close := false 55 | if err != nil { 56 | if err == io.EOF { 57 | log.WithFields(log.Fields{ 58 | "at": "DataIMUX.ReadFrom", 59 | "error": err.Error(), 60 | "socket_id": id, 61 | }).Debug("EOF from data imux source") 62 | } else { 63 | log.WithFields(log.Fields{ 64 | "at": "DataIMUX.ReadFrom", 65 | "error": err.Error(), 66 | "socket_id": id, 67 | }).Debug("error reading data from imux data source") 68 | } 69 | close = true 70 | } 71 | data_imux.Chunks <- Chunk{ 72 | SequenceID: sequence, 73 | SocketID: id, 74 | SessionID: data_imux.SessionID, 75 | Data: chunk_data, 76 | Close: close, 77 | } 78 | log.WithFields(log.Fields{ 79 | "at": "DataIMUX.ReadFrom", 80 | "socket_id": id, 81 | "size": read, 82 | }).Debug("write chunk from data source") 83 | sequence += 1 84 | if close { 85 | return 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /imux_socket.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "github.com/hkparker/TLB" 6 | "net" 7 | "reflect" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // A function that can be called by an IMUXSocket to reconnect after an error 13 | type Redialer func() (net.Conn, error) 14 | 15 | // A function that generates Redialers for specific bind addresses 16 | type RedialerGenerator func(string) Redialer 17 | 18 | // A map of all TLB servers used to read chunks back from sessions 19 | var sessionResponsesTLBServers = make(map[string]tlb.Server) 20 | var srtsMux sync.Mutex 21 | 22 | // A client socket that transports data in an imux session, autoreconnecting 23 | type IMUXSocket struct { 24 | IMUXer DataIMUX 25 | Redialer Redialer 26 | } 27 | 28 | // Dial a new connection in an imux session, creating a TLB server for 29 | // responses if needed. Read data from the sockets IMUXer and write it up. 30 | func (imux_socket *IMUXSocket) init(session_id string) { 31 | log.WithFields(log.Fields{ 32 | "at": "IMUXSocket.init", 33 | }).Debug("starting imux socket") 34 | tlb_server := imuxClientSocketTLBServer(session_id) 35 | cooldown := 10 * time.Second 36 | for { 37 | log.WithFields(log.Fields{ 38 | "at": "IMUXSocket.init", 39 | }).Debug("dialing imux socket") 40 | socket, err := imux_socket.Redialer() 41 | if err != nil { 42 | log.WithFields(log.Fields{ 43 | "at": "IMUXSocket.init", 44 | "error": err.Error(), 45 | }).Error("error dialing imux socket, entering cooldown") 46 | time.Sleep(cooldown) 47 | continue 48 | } 49 | tlb_server.Insert(socket) 50 | writer, err := tlb.NewStreamWriter(socket, type_store(), reflect.TypeOf(Chunk{})) 51 | if err != nil { 52 | log.WithFields(log.Fields{ 53 | "at": "IMUXSocket.init", 54 | "error": err.Error(), 55 | }).Error("error creating stream writer, entering cooldown") 56 | time.Sleep(cooldown) 57 | continue 58 | } 59 | 60 | for { 61 | chunk := <-imux_socket.IMUXer.Chunks 62 | log.WithFields(log.Fields{ 63 | "at": "IMUXSocket.init", 64 | "sequence_id": chunk.SequenceID, 65 | "socket_id": chunk.SocketID, 66 | "session_id": chunk.SessionID, 67 | }).Debug("writing chunk up transport socket") 68 | err := writer.Write(chunk) 69 | if err != nil { 70 | imux_socket.IMUXer.Stale <- chunk 71 | log.WithFields(log.Fields{ 72 | "at": "IMUXSocket.init", 73 | "error": err.Error(), 74 | "sequence_id": chunk.SequenceID, 75 | "socket_id": chunk.SocketID, 76 | "session_id": chunk.SessionID, 77 | }).Error("error writing chunk up transport socket") 78 | break 79 | } 80 | } 81 | log.WithFields(log.Fields{ 82 | "at": "IMUXSocket.init", 83 | }).Debug("transport socket died, redailing after cooldown") 84 | time.Sleep(cooldown) 85 | } 86 | } 87 | 88 | // Create a TLB server for a session if needed, or return the already existing server 89 | func imuxClientSocketTLBServer(session_id string) tlb.Server { 90 | srtsMux.Lock() 91 | defer srtsMux.Unlock() 92 | if server, exists := sessionResponsesTLBServers[session_id]; exists { 93 | return server 94 | } 95 | tlb_server := tlb.Server{ 96 | TypeStore: type_store(), 97 | Tag: tag_socket, 98 | Tags: make(map[net.Conn][]string), 99 | Sockets: make(map[string][]net.Conn), 100 | Events: make(map[string]map[uint16][]func(interface{}, tlb.TLBContext)), 101 | Requests: make(map[string]map[uint16][]func(interface{}, tlb.TLBContext)), 102 | FailedServer: make(chan error, 1), 103 | FailedSockets: make(chan net.Conn, 200), 104 | TagManipulation: &sync.Mutex{}, 105 | InsertRequests: &sync.Mutex{}, 106 | InsertEvents: &sync.Mutex{}, 107 | } 108 | go func(server tlb.Server) { 109 | for { 110 | <-server.FailedSockets 111 | } 112 | }(tlb_server) 113 | tlb_server.Accept("all", reflect.TypeOf(Chunk{}), func(iface interface{}, context tlb.TLBContext) { 114 | if chunk, ok := iface.(*Chunk); ok { 115 | cwqMux.Lock() 116 | if writer, ok := client_write_queues[chunk.SocketID]; ok { 117 | log.WithFields(log.Fields{ 118 | "at": "imuxClientSocketTLBServer", 119 | "session_id": session_id, 120 | }).Debug("accepting response chunk in transport socket TLB server") 121 | writer.Chunks <- chunk 122 | } else { 123 | log.WithFields(log.Fields{ 124 | "at": "imuxClientSocketTLBServer", 125 | "session_id": session_id, 126 | "socket_id": chunk.SocketID, 127 | }).Error("could not find write queue for response chunk") 128 | } 129 | cwqMux.Unlock() 130 | } 131 | }) 132 | sessionResponsesTLBServers[session_id] = tlb_server 133 | log.WithFields(log.Fields{ 134 | "at": "imuxClientSocketTLBServer", 135 | "session_id": session_id, 136 | }).Debug("created new TLB server for session") 137 | return tlb_server 138 | } 139 | -------------------------------------------------------------------------------- /many_to_one.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "github.com/hkparker/TLB" 6 | "net" 7 | "reflect" 8 | "sync" 9 | ) 10 | 11 | // Chan of failures to write to destinations 12 | var FailedSocketOuts = make(map[string]chan bool) 13 | var fsoMux sync.Mutex 14 | 15 | // WriteQueues for each outgoing socket on the server 16 | var server_write_queues = make(map[string]*WriteQueue) 17 | var swqMux sync.Mutex 18 | 19 | // DataIMUX objects to read responses from each outgoing destination socket 20 | var responders = make(map[string]DataIMUX) 21 | var respondersMux sync.Mutex 22 | 23 | // Tracks if goroutines have been created for each socket to read from the 24 | // DataIMUXer for its session and write responses down 25 | var loopers = make(map[net.Conn]bool) 26 | var loopersMux sync.Mutex 27 | 28 | // Create a new TLB server to accept chunks from anywhere and order them, writing them to corresponding sockets 29 | func ManyToOne(listener net.Listener, dial_destination Redialer) { 30 | tlb_server := tlb.NewServer(listener, tag_socket, type_store()) 31 | tlb_server.Accept("all", reflect.TypeOf(Chunk{}), func(iface interface{}, context tlb.TLBContext) { 32 | if chunk, ok := iface.(*Chunk); ok { 33 | log.WithFields(log.Fields{ 34 | "at": "ManyToOne", 35 | "sequence_id": chunk.SequenceID, 36 | "socket_id": chunk.SocketID, 37 | "session_id": chunk.SessionID, 38 | }).Debug("received chunk") 39 | createFailReporterIfNeeded(chunk.SocketID, chunk.SessionID) 40 | createResponderIMUXIfNeeded(chunk.SessionID) 41 | writeResponseChunksIfNeeded(context.Socket, chunk.SessionID) 42 | queue, err := queueForDestinationDialIfNeeded(chunk.SocketID, chunk.SessionID, dial_destination) 43 | if err == nil { 44 | queue.Chunks <- chunk 45 | log.WithFields(log.Fields{ 46 | "at": "ManyToOne", 47 | "sequence_id": chunk.SequenceID, 48 | "socket_id": chunk.SocketID, 49 | "session_id": chunk.SessionID, 50 | }).Debug("wrote chunk") 51 | } else { 52 | log.WithFields(log.Fields{ 53 | "at": "ManyToOne", 54 | "error": err.Error(), 55 | "sequence_id": chunk.SequenceID, 56 | "socket_id": chunk.SocketID, 57 | "session_id": chunk.SessionID, 58 | }).Error("dropped chunk") 59 | if reporter, ok := FailedSocketOuts[chunk.SocketID]; ok { 60 | reporter <- true 61 | } else { 62 | log.WithFields(log.Fields{ 63 | "at": "ManyToOne", 64 | "socket_id": chunk.SocketID, 65 | }).Error("unable to lookup fail socket out channel") 66 | } 67 | } 68 | } 69 | }) 70 | 71 | log.WithFields(log.Fields{ 72 | "at": "ManyToOne", 73 | }).Debug("created new ManyToOne") 74 | err := <-tlb_server.FailedServer 75 | log.WithFields(log.Fields{ 76 | "error": err.Error(), 77 | }).Error("TLB server failed for ManyToOne") 78 | } 79 | 80 | // Create a chan if needed to pass events about this socket failing 81 | func createFailReporterIfNeeded(socket_id, session_id string) { 82 | fsoMux.Lock() 83 | defer fsoMux.Unlock() 84 | if _, present := FailedSocketOuts[socket_id]; !present { 85 | FailedSocketOuts[socket_id] = make(chan bool, 0) 86 | go func(socket_id, session_id string) { 87 | for { 88 | <-FailedSocketOuts[socket_id] 89 | responders[session_id].Chunks <- Chunk{ 90 | SessionID: session_id, 91 | SocketID: socket_id, 92 | SequenceID: 0, 93 | Close: true, 94 | } 95 | } 96 | }(socket_id, session_id) 97 | } 98 | } 99 | 100 | // If it does not exist, create a DataIMUX to read data from 101 | // outgoing destination sockets with a common session 102 | func createResponderIMUXIfNeeded(session_id string) { 103 | respondersMux.Lock() 104 | defer respondersMux.Unlock() 105 | if _, present := responders[session_id]; !present { 106 | responders[session_id] = NewDataIMUX(session_id) 107 | log.WithFields(log.Fields{ 108 | "at": "createResponderIMUXIfNeeded", 109 | "session_id": session_id, 110 | }).Debug("created new responder imux for session") 111 | } 112 | } 113 | 114 | // If it is not already happening, ensure that response chunks for a specified 115 | // session_id are written back down this socket. 116 | func writeResponseChunksIfNeeded(socket net.Conn, session_id string) { 117 | loopersMux.Lock() 118 | defer loopersMux.Unlock() 119 | if _, looping := loopers[socket]; !looping { 120 | log.WithFields(log.Fields{ 121 | "at": "writeResponseChunksIfNeeded", 122 | "session_id": session_id, 123 | }).Debug("creating write back routine for socket") 124 | go func() { 125 | writer, err := tlb.NewStreamWriter(socket, type_store(), reflect.TypeOf(Chunk{})) 126 | if err != nil { 127 | log.WithFields(log.Fields{ 128 | "at": "writeResponseChunksIfNeeded", 129 | "session_id": session_id, 130 | "error": err.Error(), 131 | }).Error("error create return stream writer") 132 | return 133 | } 134 | respondersMux.Lock() 135 | chunk_stream, ok := responders[session_id] 136 | respondersMux.Unlock() 137 | if !ok { 138 | log.WithFields(log.Fields{ 139 | "at": "writeResponseChunksIfNeeded", 140 | "session_id": session_id, 141 | }).Error("error looking up responder imux for session_id") 142 | return 143 | } 144 | for { 145 | new_chunk := <-chunk_stream.Chunks 146 | err := writer.Write(new_chunk) 147 | if err != nil { 148 | responders[session_id].Stale <- new_chunk 149 | log.WithFields(log.Fields{ 150 | "at": "writeResponseChunksIfNeeded", 151 | "session_id": session_id, 152 | "data_len": len(new_chunk.Data), 153 | "error": err.Error(), 154 | }).Error("error writing a chunk down transport socket") 155 | break 156 | } else { 157 | log.WithFields(log.Fields{ 158 | "at": "writeResponseChunksIfNeeded", 159 | "session_id": session_id, 160 | "data_len": len(new_chunk.Data), 161 | }).Debug("wrote a chunk down transport socket") 162 | } 163 | } 164 | }() 165 | loopers[socket] = true 166 | } 167 | } 168 | 169 | // Get the queue a new chunk should go to, dialing the outgoing destination socket if this is the first time 170 | // a socket ID has been observed. 171 | func queueForDestinationDialIfNeeded(socket_id, session_id string, dial_destination func() (net.Conn, error)) (*WriteQueue, error) { 172 | swqMux.Lock() 173 | defer swqMux.Unlock() 174 | queue, present := server_write_queues[socket_id] 175 | if !present { 176 | log.WithFields(log.Fields{ 177 | "at": "queueForDestinationDialIfNeeded", 178 | "session_id": session_id, 179 | "socket_id": socket_id, 180 | }).Debug("dialing destination") 181 | destination, err := dial_destination() 182 | if err != nil { 183 | log.WithFields(log.Fields{ 184 | "at": "queueForDestinationDialIfNeeded", 185 | "session_id": session_id, 186 | "socket_id": socket_id, 187 | "error": err.Error(), 188 | }).Error("error dialing destination") 189 | return queue, err 190 | } 191 | queue = NewWriteQueue(destination) 192 | server_write_queues[socket_id] = queue 193 | respondersMux.Lock() 194 | if imuxer, ok := responders[session_id]; ok { 195 | go imuxer.ReadFrom(socket_id, destination) 196 | } else { 197 | log.WithFields(log.Fields{ 198 | "at": "queueForDestinationDialIfNeeded", 199 | "session_id": session_id, 200 | "socket_id": socket_id, 201 | }).Fatal("no responding reader exists, should not be possible") 202 | } 203 | respondersMux.Unlock() 204 | } 205 | return queue, nil 206 | } 207 | -------------------------------------------------------------------------------- /one_to_many.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "github.com/satori/go.uuid" 6 | "net" 7 | "sync" 8 | ) 9 | 10 | // Write Queues for all chunks coming back in response 11 | var client_write_queues = make(map[string]*WriteQueue) 12 | var cwqMux sync.Mutex 13 | 14 | // Provide a net.Listener, for which any accepted sockets will have their data 15 | // inverse multiplexed to a corresponding socket on the server. 16 | func OneToMany(listener net.Listener, binds map[string]int, redialer_generator RedialerGenerator) error { 17 | // Create a new SessionID shared by all sockets accepted by this OneToMany 18 | session_id := uuid.NewV4().String() 19 | log.WithFields(log.Fields{ 20 | "at": "OneToMany", 21 | "session_id": session_id, 22 | "binds": binds, 23 | }).Debug("creating new OneToMany") 24 | 25 | // Create a new DataIMUX to read data from accepted connections and 26 | // chunk all data. Create IMUXSockets to read chunks from the DataIMUX 27 | // and write them to connections to the server. 28 | imuxer := NewDataIMUX(session_id) 29 | for bind, count := range binds { 30 | for i := 0; i < count; i++ { 31 | go func(bind_addr string) { 32 | log.WithFields(log.Fields{ 33 | "at": "OneToMany", 34 | "bind": bind, 35 | "session_id": session_id, 36 | }).Debug("creating new imux socket") 37 | imux_socket := IMUXSocket{ 38 | IMUXer: imuxer, 39 | Redialer: redialer_generator(bind_addr), 40 | } 41 | imux_socket.init(session_id) 42 | }(bind) 43 | } 44 | } 45 | 46 | // In an infinite loop, accept new connections to this listener 47 | // and read data into the session DataIMUX. Create a WriteQueue 48 | // to write out return chunks. 49 | for { 50 | socket, err := listener.Accept() 51 | if err != nil { 52 | log.WithFields(log.Fields{ 53 | "at": "OneToMany", 54 | "session_id": session_id, 55 | "error": err.Error(), 56 | }).Error("error accepting new inbound connection to imux") 57 | return err 58 | } 59 | socket_id := uuid.NewV4().String() 60 | log.WithFields(log.Fields{ 61 | "at": "OneToMany", 62 | "session_id": session_id, 63 | "socket_id": socket_id, 64 | }).Debug("accepted new inbound connection to imux") 65 | 66 | // Create a new WriteQueue addressed by the socket ID to 67 | // take return chunks and write them into this socket 68 | cwqMux.Lock() 69 | client_write_queues[socket_id] = NewWriteQueue(socket) 70 | cwqMux.Unlock() 71 | createFailClientReporter(socket_id, session_id, imuxer) 72 | 73 | go imuxer.ReadFrom(socket_id, socket) 74 | } 75 | } 76 | 77 | func createFailClientReporter(socket_id, session_id string, mux DataIMUX) { 78 | fsoMux.Lock() 79 | defer fsoMux.Unlock() 80 | if _, present := FailedSocketOuts[socket_id]; !present { 81 | FailedSocketOuts[socket_id] = make(chan bool, 0) 82 | go func(socket_id, session_id string) { 83 | for { 84 | <-FailedSocketOuts[socket_id] 85 | mux.Chunks <- Chunk{ 86 | SessionID: session_id, 87 | SocketID: socket_id, 88 | SequenceID: 0, 89 | Close: true, 90 | } 91 | } 92 | }(socket_id, session_id) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tlb.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "github.com/hkparker/TLB" 6 | "net" 7 | "reflect" 8 | ) 9 | 10 | // Tag all TLB sockets as "all" 11 | func tag_socket(socket net.Conn, server *tlb.Server) { 12 | log.WithFields(log.Fields{ 13 | "at": "tag_socket", 14 | }).Debug("accepted new socket") 15 | server.TagSocket(socket, "all") 16 | } 17 | 18 | // Create a TLB type store for only chunks 19 | func type_store() tlb.TypeStore { 20 | type_store := tlb.NewTypeStore() 21 | type_store.AddType( 22 | reflect.TypeOf(Chunk{}), 23 | reflect.TypeOf(&Chunk{}), 24 | buildChunk, 25 | ) 26 | return type_store 27 | } 28 | -------------------------------------------------------------------------------- /write_queue.go: -------------------------------------------------------------------------------- 1 | package imux 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | "io" 6 | ) 7 | 8 | // A WriteQueue will receive chunks and order them, writing 9 | // their data out to the Destination in the correct order 10 | type WriteQueue struct { 11 | destination io.WriteCloser 12 | lastDump int 13 | Chunks chan *Chunk 14 | queue []*Chunk 15 | } 16 | 17 | func NewWriteQueue(destination io.WriteCloser) *WriteQueue { 18 | write_queue := WriteQueue{ 19 | destination: destination, 20 | Chunks: make(chan *Chunk, 0), 21 | queue: make([]*Chunk, 0), 22 | } 23 | go write_queue.process() 24 | return &write_queue 25 | } 26 | 27 | func (write_queue *WriteQueue) process() { 28 | for chunk := range write_queue.Chunks { 29 | write_queue.insert(chunk) 30 | write_queue.dump() 31 | } 32 | } 33 | 34 | // Place a chunk in the correct location in the queue 35 | func (write_queue *WriteQueue) insert(chunk *Chunk) { 36 | if chunk.Setup { 37 | log.WithFields(log.Fields{ 38 | "socket": chunk.SocketID, 39 | "session": chunk.SessionID, 40 | }).Debug("setup chunk received") 41 | return 42 | } 43 | if chunk.SequenceID == 0 { 44 | log.WithFields(log.Fields{ 45 | "socket": chunk.SocketID, 46 | "session": chunk.SessionID, 47 | }).Debug("reset chunk received") 48 | write_queue.bail(chunk.SocketID) 49 | return 50 | } 51 | smaller := 0 52 | for _, item := range write_queue.queue { 53 | if item.SequenceID < chunk.SequenceID { 54 | smaller++ 55 | } 56 | } 57 | smaller_chunks := write_queue.queue[:smaller] 58 | larger_chunks := write_queue.queue[smaller:] 59 | write_queue.queue = append(smaller_chunks, append([]*Chunk{chunk}, larger_chunks...)...) 60 | } 61 | 62 | // Dump as much chunk data out the Destination as available in order 63 | func (write_queue *WriteQueue) dump() { 64 | for { 65 | if len(write_queue.queue) == 0 { 66 | break 67 | } 68 | chunk := write_queue.queue[0] 69 | if chunk.SequenceID == uint64(write_queue.lastDump+1) { 70 | log.WithFields(log.Fields{ 71 | "at": "WriteQueue.Dump", 72 | "sequence": chunk.SequenceID, 73 | "socket": chunk.SocketID, 74 | "session": chunk.SessionID, 75 | }).Debug("writing out chunk data") 76 | write_queue.queue = write_queue.queue[1:] 77 | _, err := write_queue.destination.Write(chunk.Data) 78 | if chunk.Close { 79 | log.WithFields(log.Fields{ 80 | "sequence": chunk.SequenceID, 81 | "socket": chunk.SocketID, 82 | "session": chunk.SessionID, 83 | "data_len": len(chunk.Data), 84 | }).Debug("close chunk") 85 | write_queue.bail(chunk.SocketID) 86 | return 87 | } 88 | if err != nil { 89 | log.WithFields(log.Fields{ 90 | "at": "WriteQueue.Dump", 91 | "error": err.Error(), 92 | }).Warn("error writing data out") 93 | if reporter, ok := FailedSocketOuts[chunk.SocketID]; ok { 94 | reporter <- true 95 | } else { 96 | log.WithFields(log.Fields{ 97 | "at": "WriteQueue.dump", 98 | "socket_id": chunk.SocketID, 99 | }).Error("unable to lookup fail socket out channel") 100 | } 101 | } 102 | write_queue.lastDump = write_queue.lastDump + 1 103 | } else { 104 | break 105 | } 106 | } 107 | } 108 | 109 | func (write_queue *WriteQueue) bail(socket_id string) { 110 | write_queue.destination.Close() 111 | swqMux.Lock() 112 | delete(server_write_queues, socket_id) 113 | swqMux.Unlock() 114 | cwqMux.Lock() 115 | delete(client_write_queues, socket_id) 116 | cwqMux.Unlock() 117 | } 118 | --------------------------------------------------------------------------------