proof of concept

This commit is contained in:
Leopold Schabel 2021-12-16 16:01:47 +01:00
parent 28215e5ea7
commit 4985a5635a
7 changed files with 508 additions and 0 deletions

72
cmd/pcap/pcap.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"fmt"
"github.com/certusone/tpuproxy/pkg/tpu"
"github.com/gagliardetto/solana-go"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"sort"
)
// pkcon install libpcap-devel
// readPCAP reads a PCAP file and returns a channel of packets.
func readPCAP(file string) chan []byte {
packets := make(chan []byte)
go func() {
defer close(packets)
handle, err := pcap.OpenOffline(file)
if err != nil {
panic(err)
}
defer handle.Close()
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
udpLayer := packet.Layer(layers.LayerTypeUDP)
if udpLayer == nil {
continue
}
packets <- udpLayer.LayerPayload()
}
}()
return packets
}
func main() {
packets := readPCAP("fixtures/tpu.pcap")
signerCount := make(map[solana.PublicKey]uint)
for p := range packets {
tx, err := tpu.ParseTx(p)
if err != nil {
fmt.Println(err)
continue
}
ok := tpu.VerifyTxSig(tx)
if !ok {
fmt.Printf("bad signature on %s", tx.Signatures[0])
continue
}
signers := tpu.ExtractSigners(tx)
for _, signer := range signers {
signerCount[signer]++
}
}
// sort by count
var keys []solana.PublicKey
for k := range signerCount {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return signerCount[keys[i]] > signerCount[keys[j]]
})
for _, k := range keys {
fmt.Printf("%s\t%d\n", k, signerCount[k])
}
}

133
cmd/sniff/sniff.go Normal file
View File

@ -0,0 +1,133 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/certusone/tpuproxy/pkg/endpoints"
"github.com/certusone/tpuproxy/pkg/tpu"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"k8s.io/klog/v2"
"net"
"strconv"
"strings"
)
type packet struct {
data []byte
port uint16
src net.IP
}
func readPacketsFromInterface(iface string, ports []uint16, dst net.IP) (chan packet, error) {
// bpf filter
var filter string
for _, port := range ports {
filter += fmt.Sprintf(" or dst port %d", port)
}
filter = fmt.Sprintf("udp and dst host %s and (%s)", dst.String(), filter[4:])
klog.Info("filter: ", filter)
handle, err := pcap.OpenLive(iface, 1600, false, pcap.BlockForever)
if err != nil {
return nil, err
}
// set filter
err = handle.SetBPFFilter(filter)
if err != nil {
return nil, err
}
packets := make(chan packet)
go func() {
defer close(packets)
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for p := range packetSource.Packets() {
udpLayer := p.Layer(layers.LayerTypeUDP)
if udpLayer == nil {
continue
}
packets <- packet{
data: udpLayer.(*layers.UDP).Payload,
port: uint16(udpLayer.(*layers.UDP).DstPort),
}
}
}()
return packets, nil
}
var (
flagIface = flag.String("iface", "", "interface to read packets from")
flagPorts = flag.String("ports", "", "destination ports to sniff (comma-separated), asks local RPC if empty")
)
// getInterfaceIP returns the primary IP address associated with the given interface.
func getInterfaceIP(iface string) (net.IP, error) {
ifaceAddr, err := net.InterfaceByName(iface)
if err != nil {
return nil, err
}
addrs, err := ifaceAddr.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
if ip, ok := addr.(*net.IPNet); ok && ip.IP.To4() != nil {
return ip.IP, nil
}
}
return nil, fmt.Errorf("no IP address found for interface %s", iface)
}
func main() {
flag.Parse()
if *flagIface == "" {
klog.Exit("-iface is required")
}
dst, err := getInterfaceIP(*flagIface)
if err != nil {
klog.Exit("failed to get IP: ", err)
}
klog.Infof("interface %s has primary IP %s", *flagIface, dst)
ports := make([]uint16, 0)
if *flagPorts == "" {
klog.Infof("no ports specified, asking local RPC for ports")
ports, err = endpoints.GetNodeTPUPorts(context.Background(), endpoints.RPCLocalhost, dst)
if err != nil {
klog.Exit("failed to get ports: ", err)
}
klog.Infof("found ports: %v", ports)
} else {
for _, port := range strings.Split(*flagPorts, ",") {
p, err := strconv.ParseUint(port, 10, 16)
if err != nil {
klog.Exit("failed to parse port: ", err)
}
ports = append(ports, uint16(p))
}
}
packets, err := readPacketsFromInterface(*flagIface, ports, dst)
if err != nil {
klog.Exit("error reading packets: ", err)
}
for p := range packets {
tx, err := tpu.ParseTx(p.data)
if err != nil {
klog.Warning("port %d error parsing tx: ", p.port, err)
continue
}
signers := tpu.ExtractSigners(tx)
klog.Infof("port %d sig %s signers %v", p.port, tx.Signatures[0], signers)
}
}

1
fixtures/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.pcap

46
go.mod Normal file
View File

@ -0,0 +1,46 @@
module github.com/certusone/tpuproxy
go 1.17
require (
github.com/gagliardetto/binary v0.5.0
github.com/gagliardetto/solana-go v1.0.2
github.com/google/gopacket v1.1.19
k8s.io/klog/v2 v2.30.0
)
require (
contrib.go.opencensus.io/exporter/stackdriver v0.13.4 // indirect
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/gagliardetto/treeout v0.1.4 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 // indirect
github.com/tidwall/gjson v1.6.7 // indirect
github.com/tidwall/match v1.0.3 // indirect
github.com/tidwall/pretty v1.0.2 // indirect
go.opencensus.io v0.22.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect
go.uber.org/zap v1.16.0 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
)

View File

@ -0,0 +1,44 @@
package endpoints
import (
"context"
"fmt"
"github.com/gagliardetto/solana-go/rpc"
"net"
"strconv"
)
const RPCLocalhost = "http://127.0.0.1:8899"
func GetNodeTPUPorts(ctx context.Context, rpcHost string, nodeIP net.IP) ([]uint16, error) {
c := rpc.New(rpcHost)
out, err := c.GetClusterNodes(ctx)
if err != nil {
return nil, err
}
for _, node := range out {
if node.TPU == nil {
continue
}
tpuAddr := *node.TPU
host, port, err := net.SplitHostPort(tpuAddr)
if err != nil {
return nil, fmt.Errorf("error parsing node TPU %s: %v", tpuAddr, err)
}
if host == nodeIP.String() {
port, err := strconv.Atoi(port)
if err != nil {
return nil, fmt.Errorf("error parsing node TPU %s: %v", tpuAddr, err)
}
return []uint16{
uint16(port), // TPU
uint16(port + 1), // TPUfwd
uint16(port + 2), // TPUvote
}, nil
}
}
return nil, fmt.Errorf("node %s not found in cluster", nodeIP)
}

53
pkg/tpu/tpu.go Normal file
View File

@ -0,0 +1,53 @@
package tpu
import (
"crypto/ed25519"
"errors"
"github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
)
func ParseTx(p []byte) (tx *solana.Transaction, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("ParseTx panic")
}
}()
tx, err = solana.TransactionFromDecoder(bin.NewBinDecoder(p))
if err != nil {
return nil, err
}
return
}
func VerifyTxSig(tx *solana.Transaction) (ok bool) {
msg, err := tx.Message.MarshalBinary()
if err != nil {
panic(err)
}
signers := ExtractSigners(tx)
if len(signers) != len(tx.Signatures) {
return false
}
for i, sig := range tx.Signatures {
if !ed25519.Verify(signers[i][:], msg, sig[:]) {
return false
}
}
return true
}
func ExtractSigners(tx *solana.Transaction) []solana.PublicKey {
signers := make([]solana.PublicKey, 0, len(tx.Signatures))
for _, acc := range tx.Message.AccountKeys {
if tx.IsSigner(acc) {
signers = append(signers, acc)
}
}
return signers
}

159
pkg/tpu/tpu_test.go Normal file
View File

@ -0,0 +1,159 @@
package tpu
import (
"encoding/hex"
"strings"
"testing"
)
const breakTx = `
01cd 381f 2c7a 02e7 8131 e2dd 7973 e950
0ee5 f641 e277 5214 2d40 a1d0 2680 7261
373a 360b 9bb0 c1b2 0527 d05f ded6 9854
15ce f10c 7e1e ecf7 9936 1f62 7391 a717
0c01 0001 030b e57d dad5 f720 6443 5af8
88f3 ea02 fb25 0872 708a ef6d 2af7 fb0d
b3a7 fd60 902d cb74 4542 57c0 11b2 f71d
b25c 7097 820a f6ba 23a8 581e f842 03c1
f458 161b 81a1 3135 a853 75d0 0a62 175a
a55b 90d5 b1b1 cd35 e740 0ff4 b648 ee90
d1cd 6134 3b79 7e54 aae3 3c97 7e48 8f09
2d48 a4b5 367e 88e1 6cc6 c3bb 8c66 22fa
4491 add8 6701 0201 0102 000d
`
/*
(*solana.Transaction)(0xc0001efea0)({
Signatures: ([]solana.Signature) (len=1 cap=1) {
(solana.Signature) (len=64 cap=64) 64uYQJaaAbLinRXWH661uzDvdZaWFTrh8RiN7LKN18eGs2XcngX1NqRi5w8YcXzxPKodbfG1dD8XsyeNHHVhMb1g
},
Message: (solana.Message) {
AccountKeys: ([]solana.PublicKey) (len=6 cap=8) {
(solana.PublicKey) (len=32 cap=32) Gxwia5TTd63XbSMB9AhV5LtPQucGKnvjYzaUQ3iAH7GV,
(solana.PublicKey) (len=32 cap=32) 2X122BRxKJGvjmcjQdJUouTFxKbtFLnfZWA3Uz6ST9sD,
(solana.PublicKey) (len=32 cap=32) 4LUro5jaPaTurXK737QAxgJywdhABnFAMQkXX4ZyqqaZ,
(solana.PublicKey) (len=32 cap=32) 9gpfTc4zsndJSdpnpXQbey16L5jW2GWcKeY3PLixqU4,
(solana.PublicKey) (len=32 cap=32) 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin,
(solana.PublicKey) (len=32 cap=32) 11111111111111111111111111111111
},
Header: (solana.MessageHeader) {
NumRequiredSignatures: (uint8) 1,
NumReadonlySignedAccounts: (uint8) 0,
NumReadonlyUnsignedAccounts: (uint8) 2
},
RecentBlockhash: (solana.Hash) (len=32 cap=32) EuMLgzXp8c467FynAUwSErE4EJrbRmds6SJA5vDH2s8b,
Instructions: ([]solana.CompiledInstruction) (len=2 cap=2) {
(solana.CompiledInstruction) {
ProgramIDIndex: (uint16) 4,
AccountCount: (bin.Varuint16) 5,
DataLength: (bin.Varuint16) 7,
Accounts: ([]uint16) (len=5 cap=8) {
(uint16) 1,
(uint16) 2,
(uint16) 3,
(uint16) 2,
(uint16) 2
},
Data: (solana.Base58) (len=7 cap=7) 12VeXEVRR
},
(solana.CompiledInstruction) {
ProgramIDIndex: (uint16) 5,
AccountCount: (bin.Varuint16) 2,
DataLength: (bin.Varuint16) 12,
Accounts: ([]uint16) (len=2 cap=4) {
(uint16) 0,
(uint16) 0
},
Data: (solana.Base58) (len=12 cap=12) 3Bxs43NbWyF9ibYB
}
}
}
})
*/
const tpuTx = `
01fd 740a 299c 8140 4158 2e9e d4cc 7c96
0c11 ead6 eefc 2681 ae9e df71 cb31 d2ca
608b 62a1 3555 f268 82fb 622c 9c60 132c
a362 d64d 6906 ed47 012d af73 f1b6 d805
0f01 0002 06ed 341d e66d 6788 6d10 97e4
ac2b cfd5 00c7 9411 872b 0f47 ed52 5786
ce23 f750 0a16 8b21 8f1e 3359 a2be 9952
02b4 d07f 1978 eecf e602 2fed dd83 3509
9f9e 25af 0031 9098 8c12 0f69 b745 18e1
5183 8682 b982 2fa5 7d3f 529d 87d2 0b05
0ceb c02b 1a02 39ac 5042 f7fd afc3 f269
19a7 9644 8de8 142c d2ee d20d c08e cf80
8b79 b09d 6985 0f2d 6e02 a47a f824 d09a
b69d c42d 70cb 28cb fa24 9fb7 ee57 b9d2
56c1 2762 ef00 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 00ce 9121 49be dcfc c6d4 82a1
7a1a 6ae3 d823 35a8 57e4 d86e 2c3f b730
e519 cca5 a802 0405 0102 0302 0207 0003
0000 000b 0005 0200 000c 0200 0000 0f0b
0000 0000 0000
`
func parseHexdump(s string) []byte {
s = strings.ReplaceAll(s, " ", "")
s = strings.ReplaceAll(s, "\n", "")
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}
func TestParseTx(t *testing.T) {
ts := []string{
breakTx,
tpuTx,
}
for _, s := range ts {
b := parseHexdump(s)
_, err := ParseTx(b)
if err != nil {
t.Error(err)
}
}
}
func BenchmarkParseTx(b *testing.B) {
r := parseHexdump(tpuTx)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := ParseTx(r)
if err != nil {
b.Error(err)
}
}
}
func TestVerifyTxSig(t *testing.T) {
tx, err := ParseTx(parseHexdump(tpuTx))
if err != nil {
panic(err)
}
if !VerifyTxSig(tx) {
t.Error("tx verify failed")
}
}
func BenchmarkVerifyTxSig(b *testing.B) {
tx, err := ParseTx(parseHexdump(tpuTx))
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
VerifyTxSig(tx)
}
}