Initial commit

This commit is contained in:
Kioubit 2023-03-06 20:02:36 +02:00
commit d952c6a454
8 changed files with 559 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.idea/

29
README.md Normal file
View file

@ -0,0 +1,29 @@
# ColorPing
![IPv6 Canvas example](screenshot.png?raw=true)
## How does it work?
Each IPv6 address in a /64 IPv6 subnet is associated to one pixel with color (RGB) information.
When an address is pinged, the corresponding pixel is changed on the canvas and displayed
to all viewers via a webpage.
## Setup
Run and assign a /64 IPv6 subnet to the created interface named `canvas`.
The program needs to run as root or with the `CAP_NET_ADMIN` capability
### Example
```
./ColorPing
ip addr add fdcf:8538:9ad5:3333::1/64 dev canvas
ip link set up canvas
```
### Ping format
```
????:????:????:????:XXXX:YYYY:11RR:GGBB
```
Where:
- ``????`` can be anything
- ``XXXX`` must be the target X coordinate of the canvas, encoded as hexadecimal
- ``YYYY`` must be the target Y coordinate of the canvas, encoded as hexadecimal
- ``RR`` target "red" value (0-255), encoded as hexadecimal
- ``GG`` target "green" value (0-255), encoded as hexadecimal
- ``BB`` target "blue" value (0-255), encoded as hexadecimal

7
go.mod Normal file
View file

@ -0,0 +1,7 @@
module ColorPing
go 1.20
require github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
require golang.org/x/sys v0.6.0 // indirect

4
go.sum Normal file
View file

@ -0,0 +1,4 @@
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

220
http.go Normal file
View file

@ -0,0 +1,220 @@
package main
import (
"embed"
"html/template"
"log"
"math"
"net"
"net/http"
"strings"
"sync"
"time"
)
//go:embed template.html
var embedFS embed.FS
var htmlTemplate *template.Template
type clientState int
const (
INITIAL = 0
ACTIVE = iota
)
type client struct {
channel chan string
state clientState
}
var (
clientCounter uint32 = 0
clientCounterMutex sync.Mutex
clients = make(map[uint32]*client)
clientMutex sync.RWMutex
)
func getClientID() uint32 {
clientCounterMutex.Lock()
defer clientCounterMutex.Unlock()
clientCounter++
if clientCounter == math.MaxUint32 {
clientCounter = 0
clearClients()
}
return clientCounter
}
func clearClients() {
clientMutex.Lock()
defer clientMutex.Unlock()
clients = make(map[uint32]*client)
}
func httpServer() {
var err error
htmlTemplate = template.Must(template.ParseFS(embedFS, "template.html"))
http.HandleFunc("/stream", stream)
http.HandleFunc("/", serveRoot)
err = http.ListenAndServe("0.0.0.0:9090", nil)
if err != nil {
log.Fatalln(err)
}
}
func getInterfaceBaseIP() string {
iFace, err := net.InterfaceByName(interfaceName)
if err != nil {
return ""
}
addresses, err := iFace.Addrs()
if err != nil {
return ""
}
gua := ""
ula := ""
for _, v := range addresses {
addr := v.String()
if !strings.Contains(addr, ":") {
continue
}
_, anet, err := net.ParseCIDR(addr)
if err != nil {
continue
}
if anet.IP.IsLinkLocalUnicast() {
continue
}
if anet.IP.IsGlobalUnicast() {
gua = strings.Split(anet.String(), "/")[0]
}
if anet.IP.IsPrivate() {
ula = strings.Split(anet.String(), "/")[0]
}
}
if gua != "" {
return gua
} else {
return ula
}
}
func serveRoot(w http.ResponseWriter, r *http.Request) {
if r.RequestURI != "/" {
w.WriteHeader(404)
_, _ = w.Write([]byte("404 not found"))
return
}
type pageData struct {
BaseIP string
CanvasWidth int
CanvasHeight int
}
baseIP := getInterfaceBaseIP()
if len(baseIP) == 21 {
baseIP = strings.TrimSuffix(baseIP, ":")
}
err := htmlTemplate.Execute(w, pageData{
BaseIP: baseIP,
CanvasHeight: 512,
CanvasWidth: 512,
})
if err != nil {
log.Println(err)
}
}
func streamServer() {
for {
clientMutex.RLock()
if len(clients) == 0 {
for {
if len(clients) == 0 {
clientMutex.RUnlock()
time.Sleep(1 * time.Second)
clientMutex.RLock()
} else {
break
}
}
}
requiresInitial := false
requiresUpdate := false
for _, v := range clients {
if v.state == INITIAL {
requiresInitial = true
} else {
requiresUpdate = true
}
if requiresInitial && requiresUpdate {
break
}
}
dataInitial, dataUpdate := getPicture(requiresInitial, requiresUpdate)
for _, v := range clients {
if v.state == INITIAL {
v.state = ACTIVE
select {
case v.channel <- dataInitial:
default:
continue
}
} else {
if dataUpdate != "0" {
select {
case v.channel <- dataUpdate:
default:
continue
}
}
}
time.Sleep(400 * time.Millisecond)
}
clientMutex.RUnlock()
time.Sleep(10 * time.Millisecond)
}
}
func stream(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
messageChan := make(chan string, 40)
id := getClientID()
newClient := client{
channel: messageChan,
state: INITIAL,
}
clientMutex.Lock()
clients[id] = &newClient
clientMutex.Unlock()
go func() {
// Listen for connection close
<-r.Context().Done()
clientMutex.Lock()
close(messageChan)
delete(clients, id)
clientMutex.Unlock()
}()
for {
data := <-messageChan
if data == "" {
return
}
_, _ = w.Write([]byte(data))
flusher.Flush()
}
}

182
main.go Normal file
View file

@ -0,0 +1,182 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"github.com/songgao/water"
"image"
"image/color"
"image/png"
"log"
"sync"
)
const interfaceName = "canvas"
const handlerCount = 4
func main() {
prePopulatePixelArray()
packetChan := make(chan *[]byte, 1000)
for i := 0; i < handlerCount; i++ {
go packetHandler(packetChan)
}
go startInterface(packetChan)
go streamServer()
fmt.Println("Kioubit ColorPing started")
fmt.Println("Interface name:", interfaceName)
httpServer()
}
func prePopulatePixelArray() {
for x := 0; x < len(pixelArray); x++ {
for y := 0; y < len(pixelArray[x]); y++ {
pixelArray[x][y] = &pixel{
r: uint8(0),
g: uint8(0),
b: uint8(0),
}
}
}
}
func startInterface(packetChan chan *[]byte) {
config := water.Config{
DeviceType: water.TUN,
}
config.Name = interfaceName
iFace, err := water.New(config)
if err != nil {
log.Fatal(err)
}
for {
packet := make([]byte, 2000)
n, err := iFace.Read(packet)
if err != nil {
log.Fatal(err)
}
packet = packet[:n]
packetChan <- &packet
}
}
func packetHandler(packetChan chan *[]byte) {
for {
packet := <-packetChan
if len(*packet) < 40 {
continue
}
if (*packet)[0] != 0x60 {
continue
}
destinationAddress := (*packet)[24:40]
relevant := destinationAddress[8:]
// FORMAT: XXXX:YYYY:11RR:GGBB
x := binary.BigEndian.Uint16(relevant[0:2])
y := binary.BigEndian.Uint16(relevant[2:4])
if relevant[4] != 0x11 {
continue
}
r := relevant[5]
g := relevant[6]
b := relevant[7]
if x > 512 || y > 512 {
continue
}
obj := pixelArray[x][y]
obj.Lock()
if obj.r != r || obj.g != g || obj.b != b {
obj.r = r
obj.g = g
obj.b = b
obj.changed = true
}
obj.Unlock()
}
}
type pixel struct {
sync.Mutex
r uint8
g uint8
b uint8
changed bool
}
// 0 - 513
var pixelArray [513][513]*pixel
func getPicture(fullUpdate bool, incrementalUpdate bool) (string, string) {
anyChange := false
canvasFullUpdate := image.NewRGBA(image.Rect(0, 0, 512, 512))
canvasIncrementalUpdate := image.NewRGBA(image.Rect(0, 0, 512, 512))
for x := 0; x < len(pixelArray); x++ {
for y := 0; y < len(pixelArray[x]); y++ {
obj := pixelArray[x][y]
obj.Lock()
var newColor *color.RGBA
if incrementalUpdate {
if obj.changed {
newColor = &color.RGBA{
R: obj.r,
G: obj.g,
B: obj.b,
A: 255,
}
obj.changed = false
anyChange = true
canvasIncrementalUpdate.SetRGBA(x, y, *newColor)
} else if !fullUpdate {
obj.Unlock()
continue
}
}
if newColor == nil {
newColor = &color.RGBA{
R: obj.r,
G: obj.g,
B: obj.b,
A: 255,
}
}
obj.Unlock()
canvasFullUpdate.SetRGBA(x, y, *newColor)
}
}
encoder := png.Encoder{
CompressionLevel: png.BestSpeed,
}
incrementalUpdateResult := "0"
if anyChange {
buff := new(bytes.Buffer)
err := encoder.Encode(buff, canvasIncrementalUpdate)
if err != nil {
log.Println(err.Error())
}
incrementalUpdateResult = "event: u\ndata:" + base64.StdEncoding.EncodeToString(buff.Bytes()) + "\n\n"
}
fullUpdateResult := "0"
if fullUpdate {
buff := new(bytes.Buffer)
err := encoder.Encode(buff, canvasFullUpdate)
if err != nil {
log.Println(err.Error())
}
fullUpdateResult = "event: u\ndata:" + base64.StdEncoding.EncodeToString(buff.Bytes()) + "\n\n"
}
return fullUpdateResult, incrementalUpdateResult
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

116
template.html Normal file
View file

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IPV6Canvas</title>
<style>
body{
background-color: lightslategrey;
}
#display{
margin-right: auto;
margin-left: auto;
margin-top: 1em;
display: block;
border: black 2px;
}
#collapsed-information{
display: none;
position: absolute;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 10em;
top: 0;
opacity: 84%;
color: white;
background-color: rgb(100, 100, 200);
}
#information{
position: absolute;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 30em;
top: 1em;
opacity: 80%;
padding-left: 0.6em;
padding-right: 0.6em;
padding-bottom: 0.6em;
color: white;
background-color: rgb(100, 100, 200);
}
.dot {
height: 25px;
width: 25px;
background-color: #ff8200;
border-radius: 50%;
display: inline-block;
vertical-align: middle;
}
.center {
align-items: center;
display: flex;
justify-content: center;
}
</style>
</head>
<body>
<div>
<div id="collapsed-information">
<div class="center"><a style="text-decoration: underline" onclick="infoHandler(true)">Show information</a></div>
</div>
<div id="information">
<h2>IPv6 Canvas</h2>
<b>ping {{.BaseIP}}XXXX:YYYY:11RR:GGBB</b>
<br>Substitute coordinates and color, then ping. Values are hexadecimal.<br>
<br>Canvas size: {{.CanvasWidth}}x{{.CanvasHeight}}<br>
Connection status: <span id="connectionStatus" class="dot"></span>
<span style="float: right"><a style="text-decoration: underline" onclick="infoHandler(false)">Collapse</a></span>
<br>
</div>
<canvas id="display" width="1024" height="1024"></canvas>
</div>
</body>
<script>
const canvas = document.getElementById("display");
const ctx = canvas.getContext("2d")
const evtSource = new EventSource("/stream");
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
//ctx.scale(1.9, 1.9)
ctx.fillStyle = "#000000";
ctx.font = "30px Arial";
ctx.fillText("Please wait...", 0, 200,170);
evtSource.addEventListener("u", (event) => {
let img = new Image();
img.src = "data:image/png;base64," + event.data;
img.onload = function () {
ctx.drawImage(img, 0, 0, 1024, 1024);
};
img.onerror = function (error) {
console.log("Img Onerror:", error);
};
});
evtSource.onerror = (err) => {
console.log(err)
document.getElementById("connectionStatus").style.setProperty("background-color","#d20000")
};
evtSource.onopen = () => {
document.getElementById("connectionStatus").style.setProperty("background-color","#00a30e")
};
function infoHandler(expand) {
if (expand) {
document.getElementById("collapsed-information").style.display = "none";
document.getElementById("information").style.display = "block";
} else {
document.getElementById("collapsed-information").style.display = "block";
document.getElementById("information").style.display = "none";
}
}
</script>
</html>