This commit is contained in:
Taneli Leppä
2021-10-04 22:07:47 +02:00
committed by GitHub
10 changed files with 281 additions and 93 deletions

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:1.17
WORKDIR $GOPATH/src/github.com/amitbet/vncproxy
RUN mkdir -p $GOPATH/src/github.com/amitbet/vncproxy
COPY . .
RUN cd $GOPATH/src/github.com/amitbet/vncproxy/recorder/cmd && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /recorder .
RUN cd $GOPATH/src/github.com/amitbet/vncproxy/proxy/cmd && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /proxy .
RUN cd $GOPATH/src/github.com/amitbet/vncproxy/player/cmd && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /player .
FROM scratch
COPY --from=0 /recorder /recorder
COPY --from=0 /proxy /proxy
COPY --from=0 /player /player
EXPOSE 5900
ENTRYPOINT ["/proxy"]

View File

@@ -8,6 +8,7 @@ An RFB proxy, written in go that can save and replay FBS files
* Can also be used as: * Can also be used as:
* A screen recorder vnc-client * A screen recorder vnc-client
* A replay server to show fbs recordings to connecting clients * A replay server to show fbs recordings to connecting clients
* Authentication proxy for reMarkable tablet (2.10+)
- Tested on tight encoding with: - Tested on tight encoding with:
- Tightvnc (client + java client + server) - Tightvnc (client + java client + server)
@@ -40,6 +41,21 @@ An RFB proxy, written in go that can save and replay FBS files
* Listens to Tcp & WS ports * Listens to Tcp & WS ports
* Replays a hard-coded FBS file in normal speed to all connecting vnc clients * Replays a hard-coded FBS file in normal speed to all connecting vnc clients
### Examples of using with reMarkable
* Simply run the proxy with the `-reMarkable DEVICE_ID` flag
* To get the `DEVICE_ID`:
* Log into reMarkable via SSH
* Extract the `devicetoken` string (exclude the `@ByteArray` wrapper) the string from `/etc/remarkable.conf`
* Run the following Python snippet to decrypt the `devicetoken`:
```
pip3 install --user PyJWT
python3 -c 'import sys,jwt;t=jwt.decode(sys.argv[1],options={"verify_signature":False});print(t)' '(DEVICE TOKEN HERE)'
```
* In output, you should get a string starting with `auth0|`. The whole string is your device ID which should
be passed to be `-reMarkable` flag.
* After you should be able to connect to the reMarkable via the proxy with a normal
VNC client (tested with TightVNC)
## **Architecture** ## **Architecture**
![Image of Arch](https://github.com/amitbet/vncproxy/blob/master/architecture/proxy-arch.png?raw=true) ![Image of Arch](https://github.com/amitbet/vncproxy/blob/master/architecture/proxy-arch.png?raw=true)

View File

@@ -4,11 +4,12 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/amitbet/vncproxy/common"
"github.com/amitbet/vncproxy/logger"
"io" "io"
"net" "net"
"unicode" "unicode"
"github.com/amitbet/vncproxy/common"
"github.com/amitbet/vncproxy/logger"
) )
// A ServerMessage implements a message sent from the server to the client. // A ServerMessage implements a message sent from the server to the client.

View File

@@ -1,9 +1,13 @@
package client package client
import ( import (
"bytes"
"crypto/des" "crypto/des"
"crypto/sha256"
"encoding/binary" "encoding/binary"
"io" "io"
"github.com/amitbet/vncproxy/logger"
) )
// ClientAuthNone is the "none" authentication. See 7.2.1 // ClientAuthNone is the "none" authentication. See 7.2.1
@@ -110,3 +114,46 @@ func (p *PasswordAuth) encrypt(key string, bytes []byte) ([]byte, error) {
return crypted, nil return crypted, nil
} }
// RemarkableAuth is the authentication method used by reMarkable tablet's
// Screen Sharing feature
type RemarkableAuth struct {
RemarkableTimestamp uint64
RemarkableDeviceId string
}
func (p *RemarkableAuth) SecurityType() uint8 {
return 100
}
func (p *RemarkableAuth) Handshake(c io.ReadWriteCloser) error {
userIdHash := sha256.Sum256([]byte(p.RemarkableDeviceId))
var tsBuf []byte
pb := bytes.NewBuffer(tsBuf)
err := binary.Write(pb, binary.BigEndian, p.RemarkableTimestamp)
if err != nil {
return err
}
logger.Debugf("Hash with timestamp: %x", pb.Bytes())
userIdAndTs := append(pb.Bytes(), userIdHash[:]...)
logger.Debugf("Hash with timestamp and user ID hash: %x", userIdAndTs)
challenge := sha256.Sum256(userIdAndTs)
var challengeLength uint32 = uint32(len(challenge))
logger.Debugf("Writing challenge length: %v", challengeLength)
if err := binary.Write(c, binary.BigEndian, challengeLength); err != nil {
return err
}
logger.Debugf("Writing challenge: %x", challenge)
if err := binary.Write(c, binary.BigEndian, challenge); err != nil {
return err
}
// Some reason there is another security result... we're just gonna ignore this.
var securityResult uint8
if err = binary.Read(c, binary.BigEndian, &securityResult); err != nil {
return err
}
return nil
}

2
go.mod
View File

@@ -1,3 +1,5 @@
module github.com/amitbet/vncproxy module github.com/amitbet/vncproxy
require golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76 require golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76
go 1.13

View File

@@ -4,6 +4,8 @@ import (
"flag" "flag"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"github.com/amitbet/vncproxy/logger" "github.com/amitbet/vncproxy/logger"
vncproxy "github.com/amitbet/vncproxy/proxy" vncproxy "github.com/amitbet/vncproxy/proxy"
@@ -20,7 +22,9 @@ func main() {
var targetVncHost = flag.String("targHost", "", "target vnc server host (deprecated, use -target)") var targetVncHost = flag.String("targHost", "", "target vnc server host (deprecated, use -target)")
var targetVncPass = flag.String("targPass", "", "target vnc password") var targetVncPass = flag.String("targPass", "", "target vnc password")
var logLevel = flag.String("logLevel", "info", "change logging level") var logLevel = flag.String("logLevel", "info", "change logging level")
var reMarkable = flag.String("reMarkable", "", "reMarkable device ID (enable reMarkable 2.10+ support)")
var overrideEncodings = flag.String("overrideEncodings", "", "force a specific set of encodings to be sent to the target vnc server (encoding types separated by comma)")
var tls = flag.Bool("tls", false, "use TLS connection (turned on automatically if reMarkable)")
flag.Parse() flag.Parse()
logger.SetLogLevel(*logLevel) logger.SetLogLevel(*logLevel)
@@ -48,18 +52,31 @@ func main() {
if *wsPort != "" { if *wsPort != "" {
wsURL = "http://0.0.0.0:" + string(*wsPort) + "/" wsURL = "http://0.0.0.0:" + string(*wsPort) + "/"
} }
var overrideEncodingsList []uint32
if *overrideEncodings != "" {
for _, enc := range strings.Split(*overrideEncodings, ",") {
encI, err := strconv.Atoi(enc)
if err != nil {
panic(err)
}
overrideEncodingsList = append(overrideEncodingsList, uint32(encI))
}
}
proxy := &vncproxy.VncProxy{ proxy := &vncproxy.VncProxy{
WsListeningURL: wsURL, // empty = not listening on ws WsListeningURL: wsURL, // empty = not listening on ws
TCPListeningURL: tcpURL, TCPListeningURL: tcpURL,
ProxyVncPassword: *vncPass, //empty = no auth ProxyVncPassword: *vncPass, //empty = no auth
SingleSession: &vncproxy.VncSession{ SingleSession: &vncproxy.VncSession{
Target: *targetVnc, Target: *targetVnc,
TargetHostname: *targetVncHost, TargetHostname: *targetVncHost,
TargetPort: *targetVncPort, TargetPort: *targetVncPort,
TargetPassword: *targetVncPass, //"vncPass", TargetPassword: *targetVncPass, //"vncPass",
ID: "dummySession", ID: "dummySession",
Status: vncproxy.SessionStatusInit, Status: vncproxy.SessionStatusInit,
Type: vncproxy.SessionTypeProxyPass, Type: vncproxy.SessionTypeProxyPass,
RemarkableDeviceId: *reMarkable,
TLS: *reMarkable != "" || *tls,
OverrideEncodings: overrideEncodingsList,
}, // to be used when not using sessions }, // to be used when not using sessions
UsingSessions: false, //false = single session - defined in the var above UsingSessions: false, //false = single session - defined in the var above
} }
@@ -75,6 +92,10 @@ func main() {
} else { } else {
logger.Info("FBS recording is turned off") logger.Info("FBS recording is turned off")
} }
if *reMarkable != "" {
logger.Info("reMarkable 2.10+ support turned on")
proxy.Remarkable = true
}
proxy.StartListening() proxy.StartListening()
} }

View File

@@ -8,7 +8,9 @@ import (
) )
type ClientUpdater struct { type ClientUpdater struct {
conn *client.ClientConn conn *client.ClientConn
suppressedMessageTypes []common.ClientMessageType
overrideEncodings []common.EncodingType
} }
// Consume recieves vnc-server-bound messages (Client messages) and updates the server part of the proxy // Consume recieves vnc-server-bound messages (Client messages) and updates the server part of the proxy
@@ -19,8 +21,15 @@ func (cc *ClientUpdater) Consume(seg *common.RfbSegment) error {
case common.SegmentFullyParsedClientMessage: case common.SegmentFullyParsedClientMessage:
clientMsg := seg.Message.(common.ClientMessage) clientMsg := seg.Message.(common.ClientMessage)
logger.Debugf("ClientUpdater.Consume:(vnc-server-bound) got ClientMessage type=%s", clientMsg.Type()) logger.Debugf("ClientUpdater.Consume:(vnc-server-bound) got ClientMessage type=%s", clientMsg.Type())
switch clientMsg.Type() {
switch clientMsg.Type() {
case common.SetEncodingsMsgType:
if len(cc.overrideEncodings) > 0 {
logger.Debugf("ClientUpdater.Consume:(vnc-server-bound) overriding supported encodings with %v", cc.overrideEncodings)
encodingsMsg := clientMsg.(*server.MsgSetEncodings)
encodingsMsg.EncNum = uint16(len(cc.overrideEncodings))
encodingsMsg.Encodings = cc.overrideEncodings
}
case common.SetPixelFormatMsgType: case common.SetPixelFormatMsgType:
// update pixel format // update pixel format
logger.Debugf("ClientUpdater.Consume: updating pixel format") logger.Debugf("ClientUpdater.Consume: updating pixel format")
@@ -28,6 +37,18 @@ func (cc *ClientUpdater) Consume(seg *common.RfbSegment) error {
cc.conn.PixelFormat = pixFmtMsg.PF cc.conn.PixelFormat = pixFmtMsg.PF
} }
suppressMessage := false
for _, suppressed := range cc.suppressedMessageTypes {
if suppressed == clientMsg.Type() {
suppressMessage = true
break
}
}
if suppressMessage {
logger.Infof("ClientUpdater.Consume:(vnc-server-bound) Suppressing client message type=%s", clientMsg.Type())
return nil
}
err := clientMsg.Write(cc.conn) err := clientMsg.Write(cc.conn)
if err != nil { if err != nil {
logger.Errorf("ClientUpdater.Consume (vnc-server-bound, SegmentFullyParsedClientMessage): problem writing to port: %s", err) logger.Errorf("ClientUpdater.Consume (vnc-server-bound, SegmentFullyParsedClientMessage): problem writing to port: %s", err)

View File

@@ -1,6 +1,9 @@
package proxy package proxy
import ( import (
"bytes"
"crypto/tls"
"encoding/binary"
"net" "net"
"path" "path"
"strconv" "strconv"
@@ -22,10 +25,11 @@ type VncProxy struct {
ProxyVncPassword string //empty = no auth ProxyVncPassword string //empty = no auth
SingleSession *VncSession // to be used when not using sessions SingleSession *VncSession // to be used when not using sessions
UsingSessions bool //false = single session - defined in the var above UsingSessions bool //false = single session - defined in the var above
Remarkable bool
sessionManager *SessionManager sessionManager *SessionManager
} }
func (vp *VncProxy) createClientConnection(target string, vncPass string) (*client.ClientConn, error) { func (vp *VncProxy) createClientConnection(target string, vncPass string, tlsEnabled bool, reMarkableDeviceId string) (*client.ClientConn, error) {
var ( var (
nc net.Conn nc net.Conn
err error err error
@@ -35,6 +39,19 @@ func (vp *VncProxy) createClientConnection(target string, vncPass string) (*clie
nc, err = net.Dial("unix", target) nc, err = net.Dial("unix", target)
} else { } else {
nc, err = net.Dial("tcp", target) nc, err = net.Dial("tcp", target)
if tlsEnabled {
logger.Info("Upgrading to TLS connection...")
config := tls.Config{
InsecureSkipVerify: true,
}
tc := tls.Client(nc, &config)
err = tc.Handshake()
if err != nil {
return nil, err
}
nc = tc
}
} }
if err != nil { if err != nil {
@@ -44,6 +61,31 @@ func (vp *VncProxy) createClientConnection(target string, vncPass string) (*clie
var noauth client.ClientAuthNone var noauth client.ClientAuthNone
authArr := []client.ClientAuth{&client.PasswordAuth{Password: vncPass}, &noauth} authArr := []client.ClientAuth{&client.PasswordAuth{Password: vncPass}, &noauth}
var rmTimestamp uint64
if reMarkableDeviceId != "" {
logger.Info("Enabling reMarkable 2.10+ support")
authC, err := net.ListenPacket("udp", ":5901")
if err != nil {
return nil, err
}
defer authC.Close()
buffer := make([]byte, 128) // datagram size 51 bytes
n, addr, err := authC.ReadFrom(buffer)
logger.Debugf("Received datagram from reMarkable (%v, %v bytes)", addr, n)
pb := bytes.NewBuffer(buffer)
err = binary.Read(pb, binary.BigEndian, &rmTimestamp)
if err != nil {
return nil, err
}
logger.Debugf("reMarkable timestamp is %v", rmTimestamp)
authArr = append(authArr, &client.RemarkableAuth{
RemarkableTimestamp: rmTimestamp,
RemarkableDeviceId: reMarkableDeviceId,
})
}
clientConn, err := client.NewClientConn(nc, clientConn, err := client.NewClientConn(nc,
&client.ClientConfig{ &client.ClientConfig{
@@ -100,7 +142,7 @@ func (vp *VncProxy) newServerConnHandler(cfg *server.ServerConfig, sconn *server
target = session.TargetHostname + ":" + session.TargetPort target = session.TargetHostname + ":" + session.TargetPort
} }
cconn, err := vp.createClientConnection(target, session.TargetPassword) cconn, err := vp.createClientConnection(target, session.TargetPassword, session.TLS, session.RemarkableDeviceId)
if err != nil { if err != nil {
session.Status = SessionStatusError session.Status = SessionStatusError
logger.Errorf("Proxy.newServerConnHandler error creating connection: %s", err) logger.Errorf("Proxy.newServerConnHandler error creating connection: %s", err)
@@ -119,7 +161,16 @@ func (vp *VncProxy) newServerConnHandler(cfg *server.ServerConfig, sconn *server
// gets the messages from the server part (from vnc-client), // gets the messages from the server part (from vnc-client),
// and write through the client to the actual vnc-server // and write through the client to the actual vnc-server
clientUpdater := &ClientUpdater{cconn} var clientUpdater *ClientUpdater
clientUpdater = &ClientUpdater{conn: cconn}
if session.RemarkableDeviceId != "" {
clientUpdater.suppressedMessageTypes = []common.ClientMessageType{common.SetPixelFormatMsgType}
}
if len(session.OverrideEncodings) > 0 {
for _, enc := range session.OverrideEncodings {
clientUpdater.overrideEncodings = append(clientUpdater.overrideEncodings, common.EncodingType(enc))
}
}
sconn.Listeners.AddListener(clientUpdater) sconn.Listeners.AddListener(clientUpdater)
err = cconn.Connect() err = cconn.Connect()
@@ -185,6 +236,9 @@ func (vp *VncProxy) StartListening() {
NewConnHandler: vp.newServerConnHandler, NewConnHandler: vp.newServerConnHandler,
UseDummySession: !vp.UsingSessions, UseDummySession: !vp.UsingSessions,
} }
if vp.Remarkable {
cfg.DesktopName = []byte("reMarkable rfb")
}
if vp.TCPListeningURL != "" && vp.WsListeningURL != "" { if vp.TCPListeningURL != "" && vp.WsListeningURL != "" {
logger.Infof("running two listeners: tcp port: %s, ws url: %s", vp.TCPListeningURL, vp.WsListeningURL) logger.Infof("running two listeners: tcp port: %s, ws url: %s", vp.TCPListeningURL, vp.WsListeningURL)

View File

@@ -16,12 +16,15 @@ const (
) )
type VncSession struct { type VncSession struct {
Target string Target string
TargetHostname string TargetHostname string
TargetPort string TargetPort string
TargetPassword string TargetPassword string
ID string ID string
Status SessionStatus Status SessionStatus
Type SessionType Type SessionType
ReplayFilePath string ReplayFilePath string
RemarkableDeviceId string
TLS bool
OverrideEncodings []uint32
} }

View File

@@ -3,6 +3,7 @@ package server
import ( import (
"encoding/binary" "encoding/binary"
"io" "io"
"github.com/amitbet/vncproxy/common" "github.com/amitbet/vncproxy/common"
) )
@@ -313,10 +314,10 @@ func (msg *MsgClientCutText) Write(c io.Writer) error {
// MsgClientQemuExtendedKey holds the wire format message, for qemu keys // MsgClientQemuExtendedKey holds the wire format message, for qemu keys
type MsgClientQemuExtendedKey struct { type MsgClientQemuExtendedKey struct {
SubType uint8 // sub type SubType uint8 // sub type
IsDown uint16 // button down indicator IsDown uint16 // button down indicator
KeySym uint32 // key symbol KeySym uint32 // key symbol
KeyCode uint32 // key code KeyCode uint32 // key code
} }
func (*MsgClientQemuExtendedKey) Type() common.ClientMessageType { func (*MsgClientQemuExtendedKey) Type() common.ClientMessageType {