Known hosts knowladge sharing algorithm; nicks and ports

This commit is contained in:
Mateusz Piątkowski 2022-12-15 00:28:36 +01:00 committed by Robert Bendun
parent 67c688d772
commit c49f7ade65
11 changed files with 458 additions and 61 deletions

2
server/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
test*.sh
server

35
server/README.md Normal file
View File

@ -0,0 +1,35 @@
# Server
## Development notes
### Testing server list synchronisation (2022-12-14)
For ease of testing you can launch N instances of server in N tmux panes.
```console
$ while true; do sleep {n}; echo "======="; ./server -nick {nick} -port {port}; done
```
where `n` is increasing for each server to ensure that initial scan would not cover entire task of network scanning.
Next you can use this script to test:
```bash
go build
killall server
sleep 4
# Repeat line below for all N servers
echo '{"version":1, "type":"hosts"}' | nc localhost {port} | jq
# Choose one or few that will request synchronization with their remotes
echo '{"version":1, "type":"synchronize-hosts-with-remotes"}' | nc localhost {port} | jq
# Ensure that all synchronisation propagated with enough sleep time
sleep 2
# Repeat line below for all N servers
echo '{"version":1, "type":"hosts"}' | nc localhost {port} | jq
```

3
server/go.mod Normal file
View File

@ -0,0 +1,3 @@
module musique/server
go 1.19

View File

@ -2,8 +2,13 @@ package main
import ( import (
"bufio" "bufio"
"encoding/json"
"errors"
"flag"
"fmt" "fmt"
"log" "log"
"musique/server/proto"
"musique/server/scan"
"net" "net"
"os" "os"
"strings" "strings"
@ -53,7 +58,7 @@ func (e *timeExchange) estimateFor(host string) bool {
e.after = time.Now().UnixMilli() e.after = time.Now().UnixMilli()
if err != nil { if err != nil {
log.Println("estimateFor: %v", err) log.Printf("estimateFor: %v\n", err)
return false return false
} }
if parsedCount != 1 { if parsedCount != 1 {
@ -95,12 +100,27 @@ func timesync(hosts []string) []client {
const maxReactionTime = 300 const maxReactionTime = 300
func isThisMyAddress(address string) bool {
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err == nil && ip.To4() != nil && fmt.Sprintf("%s:%d", ip, port) == address {
return true
}
}
return false
}
func notifyAll(clients []client) <-chan time.Time { func notifyAll(clients []client) <-chan time.Time {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(len(clients)) wg.Add(len(clients))
startDeadline := time.After(maxReactionTime * time.Millisecond) startDeadline := time.After(maxReactionTime * time.Millisecond)
for _, client := range clients { for _, client := range clients {
client := client client := client
go func() { go func() {
@ -116,7 +136,229 @@ func notifyAll(clients []client) <-chan time.Time {
return startDeadline return startDeadline
} }
func handleIncoming(incoming net.Conn) {
defer incoming.Close()
request := proto.Request{}
json.NewDecoder(incoming).Decode(&request)
log.Printf("%s: %+v\n", incoming.RemoteAddr(), request)
if request.Type == "handshake" {
var response proto.HandshakeResponse
response.Version = proto.Version
response.Nick = nick
json.NewEncoder(incoming).Encode(response)
return
}
if request.Type == "hosts" {
var response proto.HostsResponse
for _, remote := range remotes {
response.Hosts = append(response.Hosts, proto.HostsResponseEntry{
Nick: remote.Nick,
Version: remote.Version,
Address: remote.Address,
})
}
json.NewEncoder(incoming).Encode(response)
return
}
if request.Type == "synchronize-hosts" {
response := synchronizeHosts(request.HostsResponse)
json.NewEncoder(incoming).Encode(response)
return
}
if request.Type == "synchronize-hosts-with-remotes" {
synchronizeHostsWithRemotes()
return
}
}
func runCommandServer(port uint16) <-chan struct{} {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", baseIP, port))
if err != nil {
log.Fatalln(err)
}
exit := make(chan struct{})
go func() {
defer listener.Close()
defer close(exit)
for {
incoming, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
go handleIncoming(incoming)
}
}()
return exit
}
type Remote struct {
Address string
Nick string
Version string
}
var (
baseIP string
nick string
port int
remotes map[string]Remote
)
func synchronizeHosts(incoming proto.HostsResponse) (response proto.HostsResponse) {
visitedHosts := make(map[string]struct{})
// Add all hosts that are in incoming to our list of remotes
// Additionaly build set of all hosts that remote knows
for _, incomingHost := range incoming.Hosts {
if _, ok := remotes[incomingHost.Address]; !ok && !isThisMyAddress(incomingHost.Address) {
remotes[incomingHost.Address] = Remote{
Address: incomingHost.Address,
Nick: incomingHost.Nick,
Version: incomingHost.Version,
}
}
visitedHosts[incomingHost.Address] = struct{}{}
}
// Build list of hosts that incoming doesn't know
for _, remote := range remotes {
if _, ok := visitedHosts[remote.Address]; !ok {
response.Hosts = append(response.Hosts, proto.HostsResponseEntry{
Address: remote.Address,
Version: remote.Version,
Nick: remote.Nick,
})
}
}
return
}
func myAddressInTheSameNetwork(remote string) (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", fmt.Errorf("myAddressInTheSameNetwork: %v", err)
}
remoteInParts := strings.Split(remote, ":")
if len(remoteInParts) == 2 {
remote = remoteInParts[0]
}
remoteIP := net.ParseIP(remote)
if remoteIP == nil {
// TODO Hoist error to global variable
return "", errors.New("Cannot parse remote IP")
}
for _, addr := range addrs {
ip, ipNet, err := net.ParseCIDR(addr.String())
if err == nil && ipNet.Contains(remoteIP) {
return ip.String(), nil
}
}
// TODO Hoist error to global variable
return "", errors.New("Cannot find matching IP addr")
}
func synchronizeHostsWithRemotes() {
previousResponseLength := -1
var response proto.HostsResponse
for previousResponseLength != len(response.Hosts) {
response = proto.HostsResponse{}
// Add all known remotes
for _, remote := range remotes {
response.Hosts = append(response.Hosts, proto.HostsResponseEntry{
Address: remote.Address,
Nick: remote.Nick,
Version: remote.Version,
})
}
// Send constructed list to each remote
previousResponseLength = len(response.Hosts)
for _, remote := range response.Hosts {
var localResponse proto.HostsResponse
localResponse.Hosts = make([]proto.HostsResponseEntry, len(response.Hosts))
copy(localResponse.Hosts, response.Hosts)
myAddress, err := myAddressInTheSameNetwork(remote.Address)
// TODO Report when err != nil
if err == nil {
localResponse.Hosts = append(localResponse.Hosts, proto.HostsResponseEntry{
Address: myAddress,
Nick: nick,
Version: proto.Version,
})
}
var remoteResponse proto.HostsResponse
proto.Command(remote.Address, proto.SynchronizeHosts(localResponse), &remoteResponse)
synchronizeHosts(remoteResponse)
}
}
}
func main() { func main() {
var (
logsPath string
)
flag.StringVar(&baseIP, "ip", "", "IP where server will listen")
flag.StringVar(&nick, "nick", "", "Name that is going to be used to recognize this server")
flag.IntVar(&port, "port", 8081, "TCP port where server receives connections")
flag.StringVar(&logsPath, "logs", "", "Target file for logs from server. By default stdout")
flag.Parse()
if len(logsPath) != 0 {
// TODO Is defer logFile.Close() needed here? Dunno
logFile, err := os.OpenFile(logsPath, os.O_WRONLY|os.O_APPEND, 0o640)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot open log file: %v\n", err)
os.Exit(1)
}
log.SetOutput(logFile)
}
if len(nick) == 0 {
log.Fatalln("Please provide nick via --nick flag")
}
exit := runCommandServer(uint16(port))
networks, err := scan.AvailableNetworks()
if err != nil {
log.Fatalln(err)
}
hosts := scan.TCPHosts(networks, []uint16{8081, 8082, 8083, 8084})
remotes = make(map[string]Remote)
for host := range hosts {
if !isThisMyAddress(host.Address) {
remotes[host.Address] = Remote{
Address: host.Address,
Nick: host.Nick,
Version: host.Version,
}
}
}
for range exit {
}
}
func main2() {
l, err := net.Listen("tcp", ":8081") l, err := net.Listen("tcp", ":8081")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -139,7 +381,7 @@ func main() {
log.Println(resp) log.Println(resp)
if resp == "scan" { if resp == "scan" {
conn.Write([]byte("Scanning...\n")) conn.Write([]byte("Scanning...\n"))
scanResult = scan() scanResult = nil // scan()
conn.Write([]byte("Scanning done!\n")) conn.Write([]byte("Scanning done!\n"))
fmt.Println(len(scanResult)) fmt.Println(len(scanResult))
continue continue

View File

@ -2,12 +2,12 @@ package main
import ( import (
"C" "C"
"net"
"bufio" "bufio"
"fmt" "fmt"
"time" "net"
"strings" "strings"
"sync" "sync"
"time"
) )
var clients []client var clients []client
@ -56,7 +56,7 @@ func ServerInit() {
} }
}() }()
waitForConnection.Wait() waitForConnection.Wait()
scanResult := scan() scanResult := []string{} // scan()
clients = timesync(scanResult) clients = timesync(scanResult)
} }
@ -64,7 +64,7 @@ func ServerInit() {
func ServerBeginProtocol() { func ServerBeginProtocol() {
self := notifyAll(clients) self := notifyAll(clients)
select { select {
case <- self: case <-self:
case <- pinger: case <-pinger:
} }
} }

9
server/proto/basic.go Normal file
View File

@ -0,0 +1,9 @@
package proto
const Version = "1"
type Request struct {
Version string
Type string
HostsResponse
}

12
server/proto/handshake.go Normal file
View File

@ -0,0 +1,12 @@
package proto
type HandshakeResponse struct {
Nick string
Version string
}
func Handshake() (req Request) {
req.Type = "handshake"
req.Version = Version
return
}

24
server/proto/hosts.go Normal file
View File

@ -0,0 +1,24 @@
package proto
type HostsResponseEntry struct {
Nick string
Address string
Version string
}
type HostsResponse struct {
Hosts []HostsResponseEntry
}
func Hosts() (req Request) {
req.Version = Version
req.Type = "hosts"
return
}
func SynchronizeHosts(response HostsResponse) (req Request) {
req.HostsResponse = response
req.Version = Version
req.Type = "synchronize-hosts"
return
}

35
server/proto/net.go Normal file
View File

@ -0,0 +1,35 @@
package proto
import (
"encoding/json"
"net"
"time"
)
func Command(target string, request interface{}, response interface{}) error {
conn, err := net.Dial("tcp", target)
if err != nil {
return err
}
defer conn.Close()
if err = json.NewEncoder(conn).Encode(request); err != nil {
return err
}
return json.NewDecoder(conn).Decode(response)
}
func CommandTimeout(target string, request interface{}, response interface{}, timeout time.Duration) error {
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return err
}
defer conn.Close()
if err = json.NewEncoder(conn).Encode(request); err != nil {
return err
}
return json.NewDecoder(conn).Decode(response)
}

88
server/scan/scanner.go Normal file
View File

@ -0,0 +1,88 @@
package scan
import (
"fmt"
"musique/server/proto"
"net"
"sync"
"time"
)
type Network struct {
FirstAddress string
MaxHostsCount int
}
const timeoutTCPHosts = time.Duration(1) * time.Second
func nextIP(ip net.IP) net.IP {
// FIXME Proper next IP address in network calculation
next := make([]byte, 4)
copy(next, ip)
next[3]++
return next
}
// AvailableNetworks returns all IPv4 networks that are available to the host
func AvailableNetworks() ([]Network, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, fmt.Errorf("getting interfaces info: %v", err)
}
networks := []Network{}
for _, addr := range addrs {
_, ipNet, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if ipNet.IP.IsGlobalUnicast() && ipNet.IP.To4() != nil {
// FIXME We assume mask /24. This is a reasonable assumption performance wise
// but may lead to inability to recognize some of the host in network
networks = append(networks, Network{nextIP(ipNet.IP).String(), 254})
}
}
return networks, nil
}
type Response struct {
proto.HandshakeResponse
Address string
}
// TCPHosts returns all TCP hosts that are in given networks on one of given ports
func TCPHosts(networks []Network, ports []uint16) <-chan Response {
ips := make(chan Response, 32)
wg := sync.WaitGroup{}
for _, network := range networks {
ip := net.ParseIP(network.FirstAddress)
for i := 0; i < network.MaxHostsCount; i++ {
for _, port := range ports {
wg.Add(1)
go func(ip net.IP, port uint16) {
defer wg.Done()
target := fmt.Sprintf("%s:%d", ip, port)
var hs proto.HandshakeResponse
err := proto.CommandTimeout(target, proto.Handshake(), &hs, timeoutTCPHosts)
if err == nil {
ips <- Response{hs, target}
}
}(ip, port)
}
ip = nextIP(ip)
}
}
go func() {
wg.Wait()
close(ips)
}()
return ips
}

View File

@ -1,53 +0,0 @@
package main
import (
"fmt"
"net"
"sync"
"time"
)
func scan() []string {
var wg sync.WaitGroup
ips := make(chan string, 256)
ifaces, _ := net.Interfaces()
for _, iface := range ifaces {
addrs, _ := iface.Addrs()
for _, addr := range addrs {
ipv4, _, _ := net.ParseCIDR(addr.String())
if ipv4.IsGlobalUnicast() && ipv4.To4() != nil {
ipv4 = ipv4.To4()
ipv4 = ipv4.Mask(ipv4.DefaultMask())
for i := 1; i < 255; i++ {
localIP := make([]byte, 4)
copy(localIP, ipv4)
wg.Add(1)
go func(ip net.IP) {
conn, dialErr := net.DialTimeout("tcp", ip.String()+":8081", time.Duration(1)*time.Second)
if dialErr == nil {
ips <- ip.String() + ":8081"
conn.Close()
}
wg.Done()
}(localIP)
ipv4[3]++
}
}
}
}
go func() {
wg.Wait()
close(ips)
}()
information := []string{}
for ip := range ips {
fmt.Println("Response from " + ip)
information = append(information, ip)
}
return information
}