mirror of
https://github.com/andatoshiki/shikigrid.git
synced 2026-06-06 04:04:15 +00:00
377 lines
9.8 KiB
Go
377 lines
9.8 KiB
Go
package mesh
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/evilsocket/islazy/log"
|
|
"github.com/gopacket/gopacket/layers"
|
|
"github.com/andatoshiki/shikigrid/crypto"
|
|
"github.com/andatoshiki/shikigrid/version"
|
|
"github.com/andatoshiki/shikigrid/wifi"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
SignalingPeriod = 300
|
|
|
|
fingValidator = regexp.MustCompile("^[a-fA-F0-9]{64}$")
|
|
)
|
|
|
|
type SessionID []byte
|
|
|
|
type Peer struct {
|
|
sync.Mutex
|
|
|
|
MetAt time.Time // first time met
|
|
DetectedAt time.Time // first time detected on this session
|
|
SeenAt time.Time // last time detected on this session
|
|
PrevSeenAt time.Time // if we met this unit before, this is the last time it's been seen
|
|
Encounters uint64
|
|
Channel int
|
|
RSSI int
|
|
SessionID SessionID
|
|
SessionIDStr string
|
|
Keys *crypto.KeyPair
|
|
AdvData sync.Map
|
|
AdvPeriod int
|
|
|
|
advEnabled bool
|
|
mux *PacketMuxer
|
|
stop chan struct{}
|
|
}
|
|
|
|
func MakeLocalPeer(name string, keys *crypto.KeyPair) *Peer {
|
|
now := time.Now()
|
|
peer := &Peer{
|
|
DetectedAt: now,
|
|
SeenAt: now,
|
|
PrevSeenAt: now,
|
|
SessionID: make([]byte, 6),
|
|
Keys: keys,
|
|
AdvData: sync.Map{},
|
|
AdvPeriod: SignalingPeriod,
|
|
stop: make(chan struct{}),
|
|
advEnabled: false,
|
|
}
|
|
|
|
if _, err := rand.Read(peer.SessionID); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
parts := make([]string, 6)
|
|
for idx, byte := range peer.SessionID {
|
|
parts[idx] = fmt.Sprintf("%02x", byte)
|
|
}
|
|
peer.SessionIDStr = strings.Join(parts, ":")
|
|
|
|
peer.AdvData.Store("name", name)
|
|
peer.AdvData.Store("identity", keys.FingerprintHex)
|
|
peer.AdvData.Store("session_id", peer.SessionIDStr)
|
|
peer.AdvData.Store("grid_version", version.Version)
|
|
|
|
peer.AdvData.Range(func(key, value interface{}) bool {
|
|
log.Debug("local.adv.%s = %s", key, value)
|
|
return true
|
|
})
|
|
|
|
return peer
|
|
}
|
|
|
|
func (peer *Peer) Advertise(enabled bool) {
|
|
peer.Lock()
|
|
defer peer.Unlock()
|
|
diff := peer.advEnabled != enabled
|
|
peer.advEnabled = enabled
|
|
if diff {
|
|
if enabled {
|
|
log.Info("peer advertisement enabled")
|
|
} else {
|
|
log.Info("peer advertisement disabled")
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewPeer(radiotap *layers.RadioTap, dot11 *layers.Dot11, adv map[string]interface{}) (peer *Peer, err error) {
|
|
now := time.Now()
|
|
peer = &Peer{
|
|
DetectedAt: now,
|
|
SeenAt: now,
|
|
PrevSeenAt: now,
|
|
Channel: wifi.Freq2Chan(int(radiotap.ChannelFrequency)),
|
|
RSSI: int(radiotap.DBMAntennaSignal),
|
|
SessionID: SessionID(dot11.Address3),
|
|
AdvData: sync.Map{},
|
|
}
|
|
|
|
parts := make([]string, 6)
|
|
for idx, byte := range peer.SessionID {
|
|
parts[idx] = fmt.Sprintf("%02x", byte)
|
|
}
|
|
peer.SessionIDStr = strings.Join(parts, ":")
|
|
|
|
// parse the fingerprint, the signature and the public key
|
|
fingerprint, found := adv["identity"].(string)
|
|
if !found {
|
|
return nil, fmt.Errorf("peer %x is not advertising any identity", peer.SessionID)
|
|
} else if !fingValidator.MatchString(fingerprint) {
|
|
return nil, fmt.Errorf("peer %x is advertising an invalid fingerprint: %s", peer.SessionID, fingerprint)
|
|
}
|
|
|
|
if pubKey64, found := adv["public_key"]; found {
|
|
pubKey, err := base64.StdEncoding.DecodeString(pubKey64.(string))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding peer %s public key: %s", fingerprint, err)
|
|
}
|
|
|
|
peer.Keys, err = crypto.FromPublicPEM(string(pubKey))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing peer %s public key: %s", fingerprint, err)
|
|
}
|
|
|
|
// basic consistency check
|
|
if peer.Keys.FingerprintHex != fingerprint {
|
|
return nil, fmt.Errorf("peer %x is advertising fingerprint %s, but it should be %s", peer.SessionID, fingerprint, peer.Keys.FingerprintHex)
|
|
}
|
|
} else if !found {
|
|
log.Debug("peer %s is not advertising any public key", fingerprint)
|
|
}
|
|
|
|
/*
|
|
No need for signature in the advertisement protocol, however:
|
|
|
|
signature64, found := adv["signature"].(string)
|
|
if !found {
|
|
return nil, fmt.Errorf("peer %s is advertising unsigned data", fingerprint)
|
|
}
|
|
|
|
signature, err := base64.StdEncoding.DecodeString(signature64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding peer %s signature: %s", fingerprint, err)
|
|
}
|
|
|
|
// the signature is SIGN(advertisement), so we need to remove the signature field and convert back to json.
|
|
// NOTE: fortunately, keys will be always sorted, so we don't have to do anything in order to guarantee signature
|
|
// consistency (https://stackoverflow.com/questions/18668652/how-to-produce-json-with-sorted-keys-in-go)
|
|
signedMap := adv
|
|
delete(signedMap, "signature")
|
|
|
|
signedData, err := json.Marshal(signedMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error packing data for signature verification: %v", err)
|
|
}
|
|
|
|
// verify the signature
|
|
if err = peer.Keys.VerifyMessage(signedData, signature); err != nil {
|
|
return nil, fmt.Errorf("peer %x signature is invalid", peer.SessionID)
|
|
}
|
|
*/
|
|
|
|
for key, value := range adv {
|
|
peer.AdvData.Store(key, value)
|
|
}
|
|
|
|
return peer, nil
|
|
}
|
|
|
|
func (peer *Peer) Update(radio *layers.RadioTap, dot11 *layers.Dot11, adv map[string]interface{}) (err error) {
|
|
peer.Lock()
|
|
defer peer.Unlock()
|
|
|
|
// parse the fingerprint, the signature and the public key
|
|
fingerprint, found := adv["identity"].(string)
|
|
if !found {
|
|
return fmt.Errorf("peer %x is not advertising any identity", peer.SessionID)
|
|
}
|
|
|
|
// basic consistency check
|
|
if peer.Keys != nil && peer.Keys.FingerprintHex != fingerprint {
|
|
return fmt.Errorf("peer %x is advertising fingerprint %s, but it should be %s", peer.SessionID, fingerprint, peer.Keys.FingerprintHex)
|
|
}
|
|
|
|
/*
|
|
No need for signature in the advertisement protocol, however:
|
|
|
|
signature64, found := adv["signature"].(string)
|
|
if !found {
|
|
return fmt.Errorf("peer %x is not advertising any signature", peer.SessionID)
|
|
}
|
|
|
|
signature, err := base64.StdEncoding.DecodeString(signature64)
|
|
if err != nil {
|
|
return fmt.Errorf("error decoding peer %d signature: %s", peer.SessionID, err)
|
|
}
|
|
|
|
// the signature is SIGN(advertisement), so we need to remove the signature field and convert back to json.
|
|
// NOTE: fortunately, keys will be always sorted, so we don't have to do anything in order to guarantee signature
|
|
// consistency (https://stackoverflow.com/questions/18668652/how-to-produce-json-with-sorted-keys-in-go)
|
|
signedMap := adv
|
|
delete(signedMap, "signature")
|
|
|
|
signedData, err := json.Marshal(signedMap)
|
|
if err != nil {
|
|
return fmt.Errorf("error packing data for signature verification: %v", err)
|
|
}
|
|
|
|
// verify the signature
|
|
if err = peer.Keys.VerifyMessage(signedData, signature); err != nil {
|
|
return fmt.Errorf("peer %x signature is invalid", peer.SessionID)
|
|
}
|
|
*/
|
|
|
|
peer.Channel = wifi.Freq2Chan(int(radio.ChannelFrequency))
|
|
peer.RSSI = int(radio.DBMAntennaSignal)
|
|
|
|
if !bytes.Equal(peer.SessionID, dot11.Address3) {
|
|
log.Info("peer %s changed session id: %x -> %x", peer.ID(), peer.SessionIDStr, dot11.Address3)
|
|
copy(peer.SessionID, dot11.Address3)
|
|
parts := make([]string, 6)
|
|
for idx, byte := range peer.SessionID {
|
|
parts[idx] = fmt.Sprintf("%02x", byte)
|
|
}
|
|
peer.SessionIDStr = strings.Join(parts, ":")
|
|
}
|
|
|
|
for key, value := range adv {
|
|
peer.AdvData.Store(key, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (peer *Peer) ID() string {
|
|
name, _ := peer.AdvData.Load("name")
|
|
ident := "???"
|
|
|
|
if peer.Keys != nil {
|
|
ident = peer.Keys.FingerprintHex
|
|
} else if _ident, found := peer.AdvData.Load("identity"); found {
|
|
ident = _ident.(string)
|
|
}
|
|
|
|
return fmt.Sprintf("%s@%s", name, ident)
|
|
}
|
|
|
|
func (peer *Peer) InactiveFor() float64 {
|
|
peer.Lock()
|
|
defer peer.Unlock()
|
|
return time.Since(peer.DetectedAt).Seconds()
|
|
}
|
|
|
|
func (peer *Peer) SetData(adv map[string]interface{}) {
|
|
peer.Lock()
|
|
defer peer.Unlock()
|
|
|
|
for key, val := range adv {
|
|
if val == nil {
|
|
peer.AdvData.Delete(key)
|
|
} else {
|
|
peer.AdvData.Store(key, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (peer *Peer) Data() map[string]interface{} {
|
|
peer.Lock()
|
|
defer peer.Unlock()
|
|
return peer.dataFrame()
|
|
}
|
|
|
|
func (peer *Peer) dataFrame() map[string]interface{} {
|
|
data := map[string]interface{}{}
|
|
peer.AdvData.Range(func(key, value interface{}) bool {
|
|
data[key.(string)] = value
|
|
return true
|
|
})
|
|
return data
|
|
}
|
|
|
|
func (peer *Peer) advertise() {
|
|
peer.Lock()
|
|
defer peer.Unlock()
|
|
|
|
if peer.advEnabled {
|
|
data := peer.dataFrame()
|
|
|
|
data["timestamp"] = time.Now().Unix()
|
|
adv, err := json.Marshal(data)
|
|
if err != nil {
|
|
log.Error("could not serialize advertisement data: %v", err)
|
|
return
|
|
}
|
|
|
|
/*
|
|
No need for signature in the advertisement protocol, however:
|
|
|
|
// sign the advertisement
|
|
signature, err := peer.Keys.SignMessage(adv)
|
|
if err != nil {
|
|
log.Error("error signing advertisement: %v", err)
|
|
return
|
|
}
|
|
|
|
// add the signature to the advertisement itself and encode again
|
|
data["signature"] = base64.StdEncoding.EncodeToString(signature)
|
|
adv, err = json.Marshal(data)
|
|
if err != nil {
|
|
log.Error("could not serialize signed advertisement data: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Debug("advertising:\n%+v", data)
|
|
*/
|
|
|
|
err, raw := wifi.Pack(
|
|
net.HardwareAddr(peer.SessionID),
|
|
wifi.BroadcastAddr,
|
|
adv,
|
|
false) // set compression to true if using signature
|
|
if err != nil {
|
|
log.Error("could not encapsulate %d bytes of advertisement data: %v", len(adv), err)
|
|
return
|
|
}
|
|
|
|
if err = peer.mux.Write(raw); err != nil {
|
|
log.Error("error sending %d bytes of advertisement frame: %v", len(raw), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (peer *Peer) StartAdvertising(iface string) (err error) {
|
|
if peer.mux == nil {
|
|
if peer.mux, err = NewPacketMuxer(iface, "", Workers); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
period := time.Duration(peer.AdvPeriod) * time.Millisecond
|
|
ticker := time.NewTicker(period)
|
|
|
|
log.Debug("advertiser started with a %s period", period)
|
|
|
|
for {
|
|
select {
|
|
case _ = <-ticker.C:
|
|
peer.advertise()
|
|
case <-peer.stop:
|
|
log.Info("advertiser stopped")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (peer *Peer) StopAdvertising() {
|
|
log.Debug("stopping advertiser ...")
|
|
peer.stop <- struct{}{}
|
|
}
|