Initial commit of utilities
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
126
cmd/irc-simple-responder/main.go
Normal file
126
cmd/irc-simple-responder/main.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/irc/hbot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
channelsArg := flag.String("channels", "", "channels to join, separated by commas")
|
||||
serverArg := flag.String("server", "", "server and port")
|
||||
nickArg := flag.String("nick", "", "nickname")
|
||||
passwordArg := flag.String("password-file", "", "optional file with password")
|
||||
messageArg := flag.String("message", "", "message with which to respond")
|
||||
flag.Parse()
|
||||
if matched, _ := regexp.MatchString(`^(#[a-zA-Z0-9_-]+,)*(#[a-zA-Z0-9_-]+)$`, *channelsArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid channels")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(*serverArg); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Invalid server")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\[\]_-]{1,16}$`, *nickArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid nick")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
password := ""
|
||||
if len(*passwordArg) > 0 {
|
||||
f, err := os.Open(*passwordArg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
password, err = bufio.NewReader(f).ReadString('\n')
|
||||
f.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to read password file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(password) > 0 && password[len(password)-1] == '\n' {
|
||||
password = password[:len(password)-1]
|
||||
}
|
||||
}
|
||||
if len(*messageArg) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Missing message")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
const messengerTimeout = time.Minute * 10
|
||||
messengers := make(map[string]time.Time, 1024)
|
||||
var messengersMu sync.Mutex
|
||||
go func() {
|
||||
for range time.Tick(messengerTimeout / 20) {
|
||||
messengersMu.Lock()
|
||||
for nick, last := range messengers {
|
||||
if time.Since(last) >= messengerTimeout {
|
||||
delete(messengers, nick)
|
||||
}
|
||||
}
|
||||
messengersMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
bot := hbot.NewBot(&hbot.Config{
|
||||
Host: *serverArg,
|
||||
Nick: *nickArg,
|
||||
User: hbot.CommonBotUserPrefix + *nickArg,
|
||||
Channels: strings.Split(*channelsArg, ","),
|
||||
Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf},
|
||||
Password: password,
|
||||
})
|
||||
bot.AddTrigger(hbot.Trigger{
|
||||
Condition: func(b *hbot.Bot, m *hbot.Message) bool {
|
||||
if m.Command != "PRIVMSG" || strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) || strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix) {
|
||||
return false
|
||||
}
|
||||
nick := strings.ToLower(m.Prefix.Name)
|
||||
messengersMu.Lock()
|
||||
defer messengersMu.Unlock()
|
||||
if last, ok := messengers[nick]; ok && time.Since(last) < messengerTimeout {
|
||||
return false
|
||||
}
|
||||
if len(messengers) > 1024*1024*1024 {
|
||||
return false
|
||||
}
|
||||
messengers[nick] = time.Now()
|
||||
return true
|
||||
},
|
||||
Action: func(b *hbot.Bot, m *hbot.Message) {
|
||||
message := *messageArg
|
||||
target := m.Prefix.Name
|
||||
if strings.Contains(m.Param(0), "#") {
|
||||
target = m.Param(0)
|
||||
if target[0] == '@' || target[0] == '+' {
|
||||
target = target[1:]
|
||||
}
|
||||
message = m.Prefix.Name + ": " + message
|
||||
}
|
||||
log.Printf("Responding to %q in %q", m.Prefix.String(), target)
|
||||
b.Msg(target, message)
|
||||
},
|
||||
})
|
||||
for {
|
||||
bot.Run()
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
||||
177
cmd/ircmirror/ircwriters.go
Normal file
177
cmd/ircmirror/ircwriters.go
Normal file
@@ -0,0 +1,177 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"container/list"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/irc/hbot"
|
||||
)
|
||||
|
||||
var words []string
|
||||
|
||||
func init() {
|
||||
f, err := os.Open("/usr/share/dict/words")
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to open dictionary: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
matcher := regexp.MustCompile(`^[a-zA-Z0-9_-]{3,}$`)
|
||||
for scanner.Scan() {
|
||||
word := scanner.Text()
|
||||
if !matcher.MatchString(word) {
|
||||
continue
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
if len(words) == 0 {
|
||||
log.Fatalln("Did not find any words in dictionary")
|
||||
}
|
||||
}
|
||||
|
||||
func randomNick() string {
|
||||
return words[rand.Intn(len(words))] + words[rand.Intn(len(words))] + strconv.Itoa(rand.Intn(10000))
|
||||
}
|
||||
|
||||
type ircWriter struct {
|
||||
mu sync.Mutex
|
||||
bot *hbot.Bot
|
||||
nick string
|
||||
mungedNick string
|
||||
usageElem *list.Element
|
||||
name string
|
||||
}
|
||||
|
||||
type ircWriters struct {
|
||||
mu sync.Mutex
|
||||
byNick map[string]*ircWriter
|
||||
byUsage list.List
|
||||
server string
|
||||
channel string
|
||||
}
|
||||
|
||||
func (writers *ircWriters) runWriter(name string, dialer func(network, address string) (net.Conn, error)) {
|
||||
writer := &ircWriter{name: name}
|
||||
startNick := randomNick()
|
||||
logf := func(format string, args ...interface{}) {
|
||||
log.Printf("[DST %s] "+format, append([]interface{}{name}, args...)...)
|
||||
}
|
||||
writer.bot = hbot.NewBot(&hbot.Config{
|
||||
Host: writers.server,
|
||||
Nick: startNick,
|
||||
User: hbot.CommonBotUserPrefix + startNick,
|
||||
Channels: []string{writers.channel},
|
||||
Dial: dialer,
|
||||
Logger: hbot.Logger{Verbosef: logf, Errorf: logf},
|
||||
})
|
||||
go func() {
|
||||
<-writer.bot.Joined()
|
||||
writers.mu.Lock()
|
||||
defer writers.mu.Unlock()
|
||||
writer.mu.Lock()
|
||||
defer writer.mu.Unlock()
|
||||
writer.usageElem = writers.byUsage.PushBack(writer)
|
||||
}()
|
||||
writer.bot.AddTrigger(hbot.Trigger{
|
||||
Condition: func(bot *hbot.Bot, m *hbot.Message) bool {
|
||||
return (m.Command == "436" || m.Command == "433") && len(writer.nick) > 0
|
||||
},
|
||||
Action: func(bot *hbot.Bot, m *hbot.Message) {
|
||||
logf("Failed with nick %q, trying %q\n", writer.mungedNick, writer.mungedNick+"_")
|
||||
writer.mungedNick += "_"
|
||||
if len(writer.mungedNick) > 16 {
|
||||
usidx := strings.IndexByte(writer.mungedNick, '_')
|
||||
uslen := len(writer.mungedNick[usidx:])
|
||||
if uslen >= 16 {
|
||||
writer.mungedNick = writer.nick + "-"
|
||||
if len(writer.mungedNick) > 16 {
|
||||
writer.mungedNick = writer.nick[:15] + "-"
|
||||
}
|
||||
} else {
|
||||
writer.mungedNick = writer.mungedNick[:16-uslen] + writer.mungedNick[usidx:]
|
||||
}
|
||||
}
|
||||
bot.SetNick(writer.mungedNick)
|
||||
},
|
||||
})
|
||||
writer.bot.Run()
|
||||
writers.mu.Lock()
|
||||
writer.mu.Lock()
|
||||
if writer.usageElem != nil {
|
||||
writers.byUsage.Remove(writer.usageElem)
|
||||
delete(writers.byNick, writer.nick)
|
||||
}
|
||||
writer.mu.Unlock()
|
||||
writers.mu.Unlock()
|
||||
}
|
||||
|
||||
func (writers *ircWriters) getWriter(nick string) *ircWriter {
|
||||
writers.mu.Lock()
|
||||
if writer, ok := writers.byNick[nick]; ok {
|
||||
writer.mu.Lock()
|
||||
if writer.nick == nick {
|
||||
writers.byUsage.MoveToFront(writer.usageElem)
|
||||
writers.mu.Unlock()
|
||||
if writer.bot.Nick() != writer.mungedNick {
|
||||
log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick)
|
||||
writer.bot.SetNick(writer.mungedNick)
|
||||
}
|
||||
return writer
|
||||
}
|
||||
writer.mu.Unlock()
|
||||
}
|
||||
if writers.byUsage.Len() == 0 {
|
||||
writers.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
writer := writers.byUsage.Back().Value.(*ircWriter)
|
||||
writer.mu.Lock()
|
||||
delete(writers.byNick, writer.nick)
|
||||
writer.nick = nick
|
||||
writer.mungedNick = nick + "-"
|
||||
if len(writer.mungedNick) > 16 {
|
||||
writer.mungedNick = nick[:15] + "-"
|
||||
}
|
||||
writers.byNick[nick] = writer
|
||||
writers.byUsage.MoveToFront(writer.usageElem)
|
||||
writers.mu.Unlock()
|
||||
log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick)
|
||||
writer.bot.SetNick(writer.mungedNick)
|
||||
return writer
|
||||
}
|
||||
|
||||
func (writers *ircWriters) queueMessage(from, message string) {
|
||||
writer := writers.getWriter(from)
|
||||
if writer == nil {
|
||||
time.AfterFunc(time.Second*3, func() {
|
||||
writers.queueMessage(from, message)
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Printf("[DST %s] Queueing message from %q\n", writer.name, from)
|
||||
writer.bot.Msg(writers.channel, message)
|
||||
writer.mu.Unlock()
|
||||
}
|
||||
|
||||
func newIrcWriterGroup(server, channel string) *ircWriters {
|
||||
return &ircWriters{
|
||||
mu: sync.Mutex{},
|
||||
byNick: make(map[string]*ircWriter, 400),
|
||||
server: server,
|
||||
channel: channel,
|
||||
}
|
||||
}
|
||||
61
cmd/ircmirror/keycache.go
Normal file
61
cmd/ircmirror/keycache.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const privateKeyCacheFile = "./private-key.cache"
|
||||
|
||||
func loadCachedPrivateKey() ([]byte, error) {
|
||||
bytes, err := os.ReadFile(privateKeyCacheFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Println("Loading cached private key from file")
|
||||
privateKeyB64 := strings.TrimSpace(string(bytes))
|
||||
privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(privateKey) != 32 {
|
||||
return nil, errors.New("invalid private key")
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func saveCachedPrivateKey(privateKey []byte) error {
|
||||
if len(privateKey) != 32 {
|
||||
return errors.New("invalid private key")
|
||||
}
|
||||
return os.WriteFile(privateKeyCacheFile, []byte(base64.StdEncoding.EncodeToString(privateKey)+"\n"), 0600)
|
||||
}
|
||||
|
||||
func loadOrGeneratePrivateKey() ([]byte, error) {
|
||||
privateKey, err := loadCachedPrivateKey()
|
||||
if err == nil {
|
||||
return privateKey, nil
|
||||
}
|
||||
log.Println("Generating new private key")
|
||||
var k [32]byte
|
||||
_, err = rand.Read(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
err = saveCachedPrivateKey(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k[:], nil
|
||||
}
|
||||
158
cmd/ircmirror/main.go
Normal file
158
cmd/ircmirror/main.go
Normal file
@@ -0,0 +1,158 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/irc/hbot"
|
||||
)
|
||||
|
||||
func raiseFileLimit() error {
|
||||
var lim unix.Rlimit
|
||||
err := unix.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lim.Cur == lim.Max {
|
||||
return nil
|
||||
}
|
||||
log.Printf("Raising file limit from %d to %d\n", lim.Cur, lim.Max)
|
||||
lim.Cur = lim.Max
|
||||
return unix.Setrlimit(syscall.RLIMIT_NOFILE, &lim)
|
||||
}
|
||||
|
||||
func main() {
|
||||
accountIdArg := flag.String("account-id", "", "account ID number")
|
||||
srcChannelArg := flag.String("src-channel", "", "source channel")
|
||||
dstChannelArg := flag.String("dst-channel", "", "destination channel")
|
||||
srcServerArg := flag.String("src-server", "", "source server")
|
||||
dstServerArg := flag.String("dst-server", "", "destination server")
|
||||
flag.Parse()
|
||||
if matched, _ := regexp.MatchString(`^[0-9]+$`, *accountIdArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid account ID")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *srcChannelArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid source channel")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *dstChannelArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid destination channel")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(*srcServerArg); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Invalid source server")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(*dstServerArg); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Invalid destination server")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
privateKey, err := loadOrGeneratePrivateKey()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
endpoints, err := getVpnEndpoints()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
vpnConf, err := registerVpnConf(privateKey, *accountIdArg)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
err = raiseFileLimit()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
dialers, err := makeDialers(vpnConf, endpoints)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if len(dialers) < 2 {
|
||||
log.Fatalln("Not enough dialers returned")
|
||||
}
|
||||
writers := newIrcWriterGroup(*dstServerArg, *dstChannelArg)
|
||||
const writersPerIP = 10
|
||||
type nextDialer struct {
|
||||
sync.Mutex
|
||||
*dialer
|
||||
}
|
||||
dialNext := func(nd *nextDialer, v4 bool) {
|
||||
var last *dialer
|
||||
for {
|
||||
nd.Lock()
|
||||
if nd.dialer == last {
|
||||
nd.dialer = &dialers[rand.Intn(len(dialers))]
|
||||
}
|
||||
last = nd.dialer
|
||||
nd.Unlock()
|
||||
name := last.name
|
||||
dial := last.dial
|
||||
if v4 {
|
||||
name += "-v4"
|
||||
dial, _ = last.splitByAf()
|
||||
} else {
|
||||
name += "-v6"
|
||||
_, dial = last.splitByAf()
|
||||
}
|
||||
writers.runWriter(name, dial)
|
||||
}
|
||||
}
|
||||
for i := 0; i < vpnProviderMaxEndpointsInParallel/2; i++ {
|
||||
nd := new(nextDialer)
|
||||
for i := 0; i < writersPerIP; i++ {
|
||||
go dialNext(nd, true)
|
||||
go dialNext(nd, false)
|
||||
}
|
||||
}
|
||||
|
||||
srcDial := dialers[rand.Intn(len(dialers))]
|
||||
logf := func(format string, args ...interface{}) {
|
||||
log.Printf("[SRC %s] "+format, append([]interface{}{srcDial.name}, args...)...)
|
||||
}
|
||||
bot := hbot.NewBot(&hbot.Config{
|
||||
Host: *srcServerArg,
|
||||
Nick: randomNick(),
|
||||
Channels: []string{*srcChannelArg},
|
||||
Dial: srcDial.dial,
|
||||
Logger: hbot.Logger{Verbosef: logf, Errorf: logf},
|
||||
})
|
||||
bot.AddTrigger(hbot.Trigger{
|
||||
Condition: func(b *hbot.Bot, m *hbot.Message) bool {
|
||||
return m.Param(0) == *srcChannelArg && m.Command == "PRIVMSG" &&
|
||||
!strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) &&
|
||||
!strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix)
|
||||
},
|
||||
Action: func(b *hbot.Bot, m *hbot.Message) {
|
||||
writers.queueMessage(m.Prefix.Name, m.Trailing())
|
||||
},
|
||||
})
|
||||
for {
|
||||
bot.Run()
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
||||
84
cmd/ircmirror/net.go
Normal file
84
cmd/ircmirror/net.go
Normal file
@@ -0,0 +1,84 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"golang.zx2c4.com/wireguard/conn"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
)
|
||||
|
||||
func makeNet(conf *vpnConf, endpoint vpnEndpoint) (*device.Device, *netstack.Net, error) {
|
||||
var localAddresses, dnsServers []net.IP
|
||||
for _, ip := range conf.ips {
|
||||
localAddresses = append(localAddresses, ip.IPAddr().IP)
|
||||
}
|
||||
for _, ip := range conf.dnses {
|
||||
dnsServers = append(dnsServers, ip.IPAddr().IP)
|
||||
}
|
||||
tun, stack, err := netstack.CreateNetTUN(localAddresses, dnsServers, 1420)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
logf := func(format string, args ...interface{}) {
|
||||
log.Printf("[NET %s] "+format, append([]interface{}{endpoint.name}, args...)...)
|
||||
}
|
||||
dev := device.NewDevice(tun, conn.NewStdNetBind(), &device.Logger{logf, logf})
|
||||
err = dev.IpcSet(fmt.Sprintf("private_key=%s\npublic_key=%s\nendpoint=%s\nallowed_ip=0.0.0.0/0\nallowed_ip=::/0\n",
|
||||
hex.EncodeToString(conf.privateKey), hex.EncodeToString(endpoint.publicKey), endpoint.endpoint.String()))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = dev.Up()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return dev, stack, nil
|
||||
}
|
||||
|
||||
type dialer struct {
|
||||
name string
|
||||
dial func(network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
func makeDialers(conf *vpnConf, endpoints []vpnEndpoint) (dialers []dialer, err error) {
|
||||
var devs []*device.Device
|
||||
defer func() {
|
||||
if err != nil {
|
||||
for _, dev := range devs {
|
||||
dev.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
for _, endpoint := range endpoints {
|
||||
dev, stack, err := makeNet(conf, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
devs = append(devs, dev)
|
||||
dialers = append(dialers, dialer{endpoint.name, stack.Dial})
|
||||
}
|
||||
return dialers, nil
|
||||
}
|
||||
|
||||
func (d *dialer) splitByAf() (v4, v6 func(network, address string) (net.Conn, error)) {
|
||||
return func(network, address string) (net.Conn, error) {
|
||||
if len(network) == 3 {
|
||||
network += "4"
|
||||
}
|
||||
return d.dial(network, address)
|
||||
}, func(network, address string) (net.Conn, error) {
|
||||
if len(network) == 3 {
|
||||
network += "6"
|
||||
}
|
||||
return d.dial(network, address)
|
||||
}
|
||||
}
|
||||
137
cmd/ircmirror/vpnprovider.go
Normal file
137
cmd/ircmirror/vpnprovider.go
Normal file
@@ -0,0 +1,137 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
const vpnProviderMaxEndpointsInParallel = 20
|
||||
|
||||
type vpnEndpoint struct {
|
||||
publicKey []byte
|
||||
endpoint netaddr.IPPort
|
||||
name string
|
||||
country string
|
||||
}
|
||||
|
||||
type vpnConf struct {
|
||||
privateKey []byte
|
||||
ips []netaddr.IP
|
||||
dnses []netaddr.IP
|
||||
}
|
||||
|
||||
type apiRelaysWireGuardV1Key []byte
|
||||
|
||||
func (key *apiRelaysWireGuardV1Key) UnmarshalText(text []byte) error {
|
||||
k, err := base64.StdEncoding.DecodeString(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(k) != 32 {
|
||||
return errors.New("key must be 32 bytes")
|
||||
}
|
||||
*key = k
|
||||
return nil
|
||||
}
|
||||
|
||||
type apiRelaysWireGuardV1Relay struct {
|
||||
Hostname string `json:"hostname"`
|
||||
EndpointV4 netaddr.IP `json:"ipv4_addr_in"`
|
||||
EndpointV6 netaddr.IP `json:"ipv6_addr_in"`
|
||||
PublicKey apiRelaysWireGuardV1Key `json:"public_key"`
|
||||
MultihopPort uint16 `json:"multihop_port"`
|
||||
}
|
||||
|
||||
type apiRelaysWireGuardV1City struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"Longitude"`
|
||||
Relays []apiRelaysWireGuardV1Relay `json:"relays"`
|
||||
}
|
||||
|
||||
type apiRelaysWireGuardV1Country struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Cities []apiRelaysWireGuardV1City `json:"cities"`
|
||||
}
|
||||
type apiRelaysWireGuardV1Root struct {
|
||||
Countries []apiRelaysWireGuardV1Country `json:"countries"`
|
||||
}
|
||||
|
||||
func getVpnEndpoints() ([]vpnEndpoint, error) {
|
||||
log.Println("Getting VPN server list")
|
||||
resp, err := http.Get("https://api.mullvad.net/public/relays/wireguard/v1/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var relays apiRelaysWireGuardV1Root
|
||||
err = json.Unmarshal(bytes, &relays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var endpoints []vpnEndpoint
|
||||
for _, country := range relays.Countries {
|
||||
for _, city := range country.Cities {
|
||||
for _, relay := range city.Relays {
|
||||
endpoints = append(endpoints, vpnEndpoint{
|
||||
publicKey: relay.PublicKey,
|
||||
endpoint: netaddr.IPPortFrom(relay.EndpointV4, 51820),
|
||||
name: strings.TrimSuffix(relay.Hostname, "-wireguard"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func registerVpnConf(privateKey []byte, accountId string) (*vpnConf, error) {
|
||||
log.Println("Registering VPN private key")
|
||||
publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.PostForm("https://api.mullvad.net/wg/", url.Values{
|
||||
"account": {accountId},
|
||||
"pubkey": {base64.StdEncoding.EncodeToString(publicKey)},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match, _ := regexp.Match(`^[0-9a-f:/.,]+$`, ipBytes); !match {
|
||||
return nil, fmt.Errorf("registration rejected: %q", string(ipBytes))
|
||||
}
|
||||
conf := &vpnConf{privateKey: privateKey, dnses: []netaddr.IP{netaddr.MustParseIP("193.138.218.74")}}
|
||||
for _, ipStr := range strings.Split(string(ipBytes), ",") {
|
||||
ip, err := netaddr.ParseIPPrefix(strings.TrimSpace(ipStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.ips = append(conf.ips, ip.IP())
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
178
cmd/wurgurboo/cgit.go
Normal file
178
cmd/wurgurboo/cgit.go
Normal file
@@ -0,0 +1,178 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CgitCommit struct {
|
||||
Text string `xml:",chardata"`
|
||||
Title string `xml:"title"`
|
||||
Updated string `xml:"updated"`
|
||||
Author struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name"`
|
||||
Email string `xml:"email"`
|
||||
} `xml:"author"`
|
||||
Published string `xml:"published"`
|
||||
Link struct {
|
||||
Text string `xml:",chardata"`
|
||||
Rel string `xml:"rel,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Href string `xml:"href,attr"`
|
||||
} `xml:"link"`
|
||||
ID string `xml:"id"`
|
||||
Content []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
Div struct {
|
||||
Text string `xml:",chardata"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
Pre string `xml:"pre"`
|
||||
} `xml:"div"`
|
||||
} `xml:"content"`
|
||||
}
|
||||
|
||||
type cgitFeed struct {
|
||||
XMLName xml.Name `xml:"feed"`
|
||||
Text string `xml:",chardata"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
Title string `xml:"title"`
|
||||
Subtitle string `xml:"subtitle"`
|
||||
Link struct {
|
||||
Text string `xml:",chardata"`
|
||||
Rel string `xml:"rel,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Href string `xml:"href,attr"`
|
||||
} `xml:"link"`
|
||||
Entry []CgitCommit `xml:"entry"`
|
||||
}
|
||||
|
||||
type cgitFeedStatus struct {
|
||||
repo string
|
||||
title string
|
||||
seenEntries map[[32]byte]bool
|
||||
orderedEntries list.List
|
||||
}
|
||||
|
||||
type CgitFeedMonitorer struct {
|
||||
feeds []*cgitFeedStatus
|
||||
updates chan CgitFeedUpdate
|
||||
ticker *time.Ticker
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type CgitFeedUpdate struct {
|
||||
RepoTitle string
|
||||
Commit *CgitCommit
|
||||
}
|
||||
|
||||
func NewCgitFeedMonitorer(pollInterval time.Duration) *CgitFeedMonitorer {
|
||||
fm := &CgitFeedMonitorer{
|
||||
updates: make(chan CgitFeedUpdate, 32),
|
||||
ticker: time.NewTicker(pollInterval),
|
||||
}
|
||||
fm.wg.Add(1)
|
||||
go func() {
|
||||
defer fm.wg.Done()
|
||||
for range fm.ticker.C {
|
||||
fm.mu.Lock()
|
||||
for _, feed := range fm.feeds {
|
||||
fm.wg.Add(1)
|
||||
go func(feed *cgitFeedStatus) {
|
||||
defer fm.wg.Done()
|
||||
fm.updateFeed(feed, true)
|
||||
}(feed)
|
||||
}
|
||||
fm.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
return fm
|
||||
}
|
||||
|
||||
func (fm *CgitFeedMonitorer) Stop() {
|
||||
fm.ticker.Stop()
|
||||
fm.wg.Wait()
|
||||
close(fm.updates)
|
||||
}
|
||||
|
||||
func (fm *CgitFeedMonitorer) Updates() <-chan CgitFeedUpdate {
|
||||
return fm.updates
|
||||
}
|
||||
|
||||
func (fm *CgitFeedMonitorer) AddFeed(repo string) {
|
||||
go func() {
|
||||
status := &cgitFeedStatus{
|
||||
repo: repo,
|
||||
title: path.Base(repo),
|
||||
seenEntries: make(map[[32]byte]bool, 1024),
|
||||
}
|
||||
fm.updateFeed(status, false)
|
||||
fm.mu.Lock()
|
||||
fm.feeds = append(fm.feeds, status)
|
||||
fm.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (fm *CgitFeedMonitorer) updateFeed(fs *cgitFeedStatus, alert bool) {
|
||||
feed, err := fs.fetchFeed()
|
||||
if err != nil {
|
||||
log.Printf("Unable to fetch commits for %q: %v", fs.title, err)
|
||||
return
|
||||
}
|
||||
for i := len(feed.Entry) - 1; i >= 0; i-- {
|
||||
commit := &feed.Entry[i]
|
||||
var commitID [32]byte
|
||||
if hex.DecodedLen(len(commit.ID)) > len(commitID) {
|
||||
continue
|
||||
}
|
||||
n, err := hex.Decode(commitID[:], []byte(commit.ID))
|
||||
if err != nil || n < 20 {
|
||||
continue
|
||||
}
|
||||
if _, ok := fs.seenEntries[commitID]; ok {
|
||||
continue
|
||||
}
|
||||
fs.seenEntries[commitID] = true
|
||||
fs.orderedEntries.PushBack(commitID)
|
||||
for len(fs.seenEntries) > 1024 {
|
||||
first := fs.orderedEntries.Front()
|
||||
delete(fs.seenEntries, first.Value.([32]byte))
|
||||
fs.orderedEntries.Remove(first)
|
||||
}
|
||||
if alert {
|
||||
fm.updates <- CgitFeedUpdate{fs.title, commit}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *cgitFeedStatus) fetchFeed() (*cgitFeed, error) {
|
||||
resp, err := http.Get(fs.repo + "/atom/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var feed cgitFeed
|
||||
err = xml.Unmarshal(bytes, &feed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feed, nil
|
||||
}
|
||||
101
cmd/wurgurboo/main.go
Normal file
101
cmd/wurgurboo/main.go
Normal file
@@ -0,0 +1,101 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/irc/hbot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
channelArg := flag.String("channel", "", "channel to join")
|
||||
serverArg := flag.String("server", "", "server and port")
|
||||
nickArg := flag.String("nick", "", "nickname")
|
||||
passwordArg := flag.String("password-file", "", "optional file with password")
|
||||
flag.Parse()
|
||||
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *channelArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid channel")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(*serverArg); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Invalid server")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\[\]_-]{1,16}$`, *nickArg); !matched {
|
||||
fmt.Fprintln(os.Stderr, "Invalid nick")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
password := ""
|
||||
if len(*passwordArg) > 0 {
|
||||
f, err := os.Open(*passwordArg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
password, err = bufio.NewReader(f).ReadString('\n')
|
||||
f.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to read password file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(password) > 0 && password[len(password)-1] == '\n' {
|
||||
password = password[:len(password)-1]
|
||||
}
|
||||
}
|
||||
|
||||
feeds := NewCgitFeedMonitorer(time.Second * 10)
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-linux/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-tools/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-linux-compat/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-windows/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-go/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-freebsd/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-openbsd/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-android/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/android-wireguard-module-builder/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-apple/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wireguard-rs/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wintun/")
|
||||
feeds.AddFeed("https://git.zx2c4.com/wg-dynamic/")
|
||||
|
||||
bot := hbot.NewBot(&hbot.Config{
|
||||
Host: *serverArg,
|
||||
Nick: *nickArg,
|
||||
Realname: "Your Friendly Neighborhood WurGur Bot",
|
||||
Channels: []string{*channelArg},
|
||||
Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf},
|
||||
Password: password,
|
||||
})
|
||||
|
||||
urlShortener := regexp.MustCompile(`(.*\?id=[a-f0-9]{8})([a-f0-9]+)$`)
|
||||
go func() {
|
||||
for commit := range feeds.Updates() {
|
||||
<-bot.Joined()
|
||||
log.Printf("New commit %s in %s", commit.Commit.ID, commit.RepoTitle)
|
||||
url := commit.Commit.Link.Href
|
||||
if matches := urlShortener.FindStringSubmatch(url); len(matches) == 3 {
|
||||
url = matches[1]
|
||||
}
|
||||
bot.Msg(*channelArg, fmt.Sprintf("\x01ACTION found a new commit in \x0303%s\x0f - \x0306%s\x0f - %s\x01", commit.RepoTitle, commit.Commit.Title, url))
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
bot.Run()
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user