Skip to content

Commit 24baa26

Browse files
committed
Add code for proof of concept
1 parent a369d05 commit 24baa26

File tree

7 files changed

+287
-0
lines changed

7 files changed

+287
-0
lines changed

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
dynv6 SRV Updater
2+
=================
3+
4+
This program sends signed UDP packets to dynv6 to keep the port of SRV records up to date.
5+
It only works with ed25519 keys.
6+
7+
## Installation
8+
9+
go get github.com/dynv6/srv-updater
10+
11+
If you don't have a ed25519 key, generate one:
12+
13+
openssh-keygen -t ed25519
14+
15+
## Usage
16+
17+
The SRV updater uses raw sockets. Therefore It needs root privilege or `CAP_NET_RAW` capability.
18+
19+
### Command line arguments
20+
21+
```
22+
-dst-host string
23+
the destination host (default "dynv6.com")
24+
-dst-port int
25+
the destination port (default 55)
26+
-fqdn string
27+
the hostname of the record you want to update (default "_service._tcp.example.com")
28+
-interval duration
29+
sending interval (default 1m)
30+
-key string
31+
private key (default "~/.ssh/id_ed25519")
32+
-priority uint
33+
the priority of the record you want to update (default 10)
34+
-src-ip string
35+
the local ip address (default should be determined)
36+
-src-port int
37+
the local port (default 10000)
38+
-weight uint
39+
the weight of the record you want to update (default 1)
40+
```
41+
42+
## Debugging
43+
44+
If the server does not understand the packet, it responds with a UDP packet containing an error message.
45+
You can read it with network protocol analyzers like tcpdump or wireshark.

go.mod

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/dynv6/srv-updater
2+
3+
go 1.15
4+
5+
require (
6+
github.com/google/gopacket v1.1.18
7+
github.com/stretchr/testify v1.6.1
8+
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
9+
)

go.sum

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY=
4+
github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8+
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
9+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
10+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
11+
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
12+
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
13+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
14+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
15+
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
16+
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
17+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
18+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
19+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
21+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

goreleaser.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
builds:
2+
- main: .
3+
binary: srv-updater
4+
goos:
5+
- darwin
6+
- linux
7+
goarch:
8+
- amd64
9+
- arm64
10+
- arm

main.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package main
2+
3+
import (
4+
"encoding/base64"
5+
"flag"
6+
"fmt"
7+
"io/ioutil"
8+
"log"
9+
"net"
10+
"os"
11+
"strings"
12+
"time"
13+
14+
"golang.org/x/crypto/ssh"
15+
)
16+
17+
func main() {
18+
packet := Packet{}
19+
flag.StringVar(&packet.FQDN, "fqdn", "_service._tcp.example.com", "the hostname of the record you want to update")
20+
flag.UintVar(&packet.Priority, "priority", 10, "the priority of the record you want to update")
21+
flag.UintVar(&packet.Weight, "weight", 1, "the weight of the record you want to update")
22+
23+
interval := flag.Duration("interval", time.Minute, "sending interval")
24+
keyPath := flag.String("key", "~/.ssh/id_ed25519", "private key")
25+
dstPort := flag.Int("dst-port", 55, "the destination port")
26+
dstHost := flag.String("dst-host", "dynv6.com", "the destination host")
27+
srcPort := flag.Int("src-port", 10000, "the local port")
28+
ip := flag.String("src-ip", defaultIP(), "the local ip address")
29+
30+
flag.Parse()
31+
32+
// No arguments given?
33+
if len(os.Args) < 2 {
34+
fmt.Printf("Usage of %s:\n", os.Args[0])
35+
flag.PrintDefaults()
36+
os.Exit(1)
37+
}
38+
39+
// Expand home directory
40+
if str := *keyPath; strings.HasPrefix(str, "~/") {
41+
home, _ := os.UserHomeDir()
42+
home += str[1:]
43+
keyPath = &home
44+
}
45+
46+
privKey := loadPrivateKey(*keyPath)
47+
48+
// Marshal JSON data
49+
packet.Timestamp = time.Now().Unix()
50+
packet.Key = base64.RawStdEncoding.EncodeToString(privKey.PublicKey().Marshal())
51+
payload := packet.MarshalAndSign(privKey)
52+
53+
// Parse local IP address
54+
srcIP := net.ParseIP(*ip)
55+
56+
// Resolve destination host
57+
dstIP, err := net.ResolveIPAddr("ip", *dstHost)
58+
if err != nil {
59+
log.Panicln("unable to resolve destination host:", err)
60+
}
61+
62+
// open raw socket
63+
conn, err := openSocket()
64+
if err != nil {
65+
panic(err)
66+
}
67+
defer conn.Close()
68+
69+
srcAddr := net.UDPAddr{
70+
IP: srcIP,
71+
Port: *srcPort,
72+
}
73+
dstAddr := net.UDPAddr{
74+
IP: dstIP.IP,
75+
Port: *dstPort,
76+
}
77+
78+
// send packet method
79+
sendPacket := func() {
80+
log.Printf("sending from %s to %s", srcAddr.String(), dstAddr.String())
81+
b, err := buildUDPPacket(dstAddr, srcAddr, payload)
82+
if err != nil {
83+
panic(err)
84+
}
85+
86+
_, err = conn.WriteTo(b, &net.IPAddr{IP: dstIP.IP})
87+
if err != nil {
88+
log.Println(err)
89+
}
90+
}
91+
92+
// start sending packets
93+
sendPacket()
94+
for range time.NewTicker(*interval).C {
95+
sendPacket()
96+
}
97+
}
98+
99+
func loadPrivateKey(path string) ssh.Signer {
100+
// Load private key
101+
pemBytes, err := ioutil.ReadFile(path)
102+
if err != nil {
103+
panic(err)
104+
}
105+
106+
// Parse private key
107+
privKey, err := ssh.ParsePrivateKey(pemBytes)
108+
if err != nil {
109+
panic(err)
110+
}
111+
112+
return privKey
113+
}

net.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net"
7+
"os"
8+
"syscall"
9+
10+
"github.com/google/gopacket"
11+
"github.com/google/gopacket/layers"
12+
)
13+
14+
func openSocket() (net.PacketConn, error) {
15+
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
16+
if err != nil {
17+
return nil, fmt.Errorf("failed open socket: %w", err)
18+
}
19+
syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
20+
21+
return net.FilePacketConn(os.NewFile(uintptr(fd), fmt.Sprintf("fd %d", fd)))
22+
}
23+
24+
func defaultIP() string {
25+
conn, err := net.Dial("udp", "8.8.8.8:80")
26+
if err != nil {
27+
log.Fatal(err)
28+
}
29+
defer conn.Close()
30+
31+
return conn.LocalAddr().(*net.UDPAddr).IP.String()
32+
}
33+
34+
func buildUDPPacket(dst, src net.UDPAddr, payload []byte) ([]byte, error) {
35+
buffer := gopacket.NewSerializeBuffer()
36+
37+
ip := &layers.IPv4{
38+
DstIP: dst.IP,
39+
SrcIP: src.IP,
40+
Version: 4,
41+
TTL: 64,
42+
Protocol: layers.IPProtocolUDP,
43+
}
44+
udp := &layers.UDP{
45+
SrcPort: layers.UDPPort(src.Port),
46+
DstPort: layers.UDPPort(dst.Port),
47+
}
48+
if err := udp.SetNetworkLayerForChecksum(ip); err != nil {
49+
return nil, fmt.Errorf("failed calculate checksum: %w", err)
50+
}
51+
if err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}, ip, udp, gopacket.Payload(payload)); err != nil {
52+
return nil, fmt.Errorf("failed to serialize packet: %w", err)
53+
}
54+
55+
return buffer.Bytes(), nil
56+
}

packet.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
crand "crypto/rand"
5+
"encoding/json"
6+
7+
"golang.org/x/crypto/ssh"
8+
)
9+
10+
type Packet struct {
11+
FQDN string `json:"fqdn"`
12+
Weight uint `json:"weight"`
13+
Priority uint `json:"priority"`
14+
Key string `json:"key"`
15+
Timestamp int64 `json:"ts"`
16+
}
17+
18+
func (p *Packet) MarshalAndSign(signer ssh.Signer) []byte {
19+
jsonBytes, _ := json.Marshal(p)
20+
21+
// Sign JSON data
22+
signature, err := signer.Sign(crand.Reader, jsonBytes)
23+
if err != nil {
24+
panic(err)
25+
}
26+
27+
// Build payload
28+
payload := make([]byte, len(jsonBytes)+1+len(signature.Blob))
29+
copy(payload, jsonBytes)
30+
copy(payload[len(jsonBytes)+1:], signature.Blob)
31+
32+
return payload
33+
}

0 commit comments

Comments
 (0)