bismuthd/main.go

623 lines
14 KiB
Go

package main
import (
"context"
_ "embed"
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"git.greysoh.dev/imterah/bismuthd/client"
core "git.greysoh.dev/imterah/bismuthd/commons"
"git.greysoh.dev/imterah/bismuthd/server"
"git.greysoh.dev/imterah/bismuthd/signingclient"
"git.greysoh.dev/imterah/bismuthd/signingserver"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
"tailscale.com/net/socks5"
)
//go:embed ascii.txt
var asciiArt string
func clientEntrypoint(cCtx *cli.Context) error {
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
bismuth, err := client.New(pubKey, privKey)
log.Debugf("My key fingerprint is: %s", bismuth.PublicKey.GetFingerprint())
if err != nil {
return err
}
routeAllTraffic := cCtx.Bool("route-all-traffic")
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", cCtx.String("ip"), cCtx.String("port")))
if err != nil {
return err
}
socksServer := socks5.Server{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
ip := addr[:strings.Index(addr, ":")]
isBismuthTLD := strings.HasSuffix(ip, ".bismuth")
// There isn't a good way to make the rest of the internet compatible and also have a seperate protocol,
// so we just do this.
if isBismuthTLD || routeAllTraffic {
log.Debugf("Recieved bismuth connection to '%s'. Routing", addr)
address := addr
ip := ip
port := ""
if isBismuthTLD {
ip = ip[:strings.LastIndex(addr, ".")]
port := addr[strings.Index(addr, ":")+1:]
address = ip + ":" + port
} else {
ip = ip[:strings.LastIndex(addr, ":")]
port := addr[strings.Index(addr, ":")+1:]
address = ip + ":" + port
}
conn, err := net.Dial(network, address)
if err != nil && err.Error() != "EOF" {
log.Errorf("TCP connection to '%s:%s' failed", ip, port)
return nil, err
}
conn, returnData, err := bismuth.Conn(conn)
if err != nil && err.Error() != "EOF" {
log.Errorf("failed to initialize bismuth connection to '%s:%s': '%s'", ip, port, err.Error())
return nil, err
}
log.Debugf("Server key fingerprint for '%s' is: %s", addr, returnData.ServerPublicKey.GetFingerprint())
return conn, err
} else {
conn, err := net.Dial(network, addr)
if err != nil && err.Error() != "EOF" {
log.Errorf("TCP connection to '%s' failed", addr)
return nil, err
}
return conn, err
}
},
}
log.Info("Bismuth client is listening...")
socksServer.Serve(listener)
return nil
}
func serverEntrypoint(cCtx *cli.Context) error {
signingServers := []string{}
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
network := fmt.Sprintf("%s:%s", cCtx.String("source-ip"), cCtx.String("source-port"))
bismuth, err := server.NewBismuthServer(pubKey, privKey, signingServers, core.XChaCha20Poly1305)
bismuth.HandleConnection = func(connBismuth net.Conn, _ *server.ClientMetadata) error {
connDialed, err := net.Dial("tcp", network)
if err != nil {
return err
}
bismuthBuffer := make([]byte, 65535)
dialBuffer := make([]byte, 65535)
go func() {
defer connDialed.Close()
defer connBismuth.Close()
for {
len, err := connBismuth.Read(bismuthBuffer)
if err != nil {
log.Errorf("failed to read from bismuth server: '%s'", err.Error())
return
}
_, err = connDialed.Write(bismuthBuffer[:len])
if err != nil {
log.Errorf("failed to write to the proxied server: '%s'", err.Error())
return
}
}
}()
go func() {
defer connDialed.Close()
defer connBismuth.Close()
for {
len, err := connDialed.Read(dialBuffer)
if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") {
log.Errorf("failed to read from proxied server: '%s'", err.Error())
return
}
_, err = connBismuth.Write(dialBuffer[:len])
if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") {
log.Errorf("failed to write to bismuth server: '%s'", err.Error())
return
}
}
}()
return nil
}
if err != nil {
return err
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", cCtx.String("dest-ip"), cCtx.String("dest-port")))
if err != nil {
return err
}
defer listener.Close()
log.Info("Bismuth server is listening...")
for {
conn, err := listener.Accept()
if err != nil {
log.Warnf("failed to accept connection: '%s'", err.Error())
continue
}
log.Debugf("Recieved connection from '%s'", conn.RemoteAddr().String())
go func() {
err := bismuth.HandleProxy(conn)
if err != nil && err.Error() != "EOF" {
log.Warnf("connection crashed/dropped during proxy handling: '%s'", err.Error())
}
}()
}
}
func signingServerEntrypoint(cCtx *cli.Context) error {
log.Warn("Using the built-in bismuth signing server in production is a horrible idea as it has no validation!")
log.Warn("Consider writing using a custom solution that's based on the signing server code, rather than the default implementation.")
signServers := []string{}
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", cCtx.String("ip"), cCtx.String("port")))
if err != nil {
return err
}
bismuthServer, err := server.NewBismuthServer(pubKey, privKey, signServers, core.XChaCha20Poly1305)
if err != nil {
return nil
}
// I'd like to use the SigningServer struct, but I can't really do that
_, err = signingserver.New(bismuthServer)
if err != nil {
return nil
}
defer listener.Close()
log.Info("Bismuth signing server is listening...")
for {
conn, err := listener.Accept()
if err != nil {
log.Warn(err.Error())
continue
}
log.Debugf("Recieved connection from '%s'", conn.RemoteAddr().String())
go func() {
err = bismuthServer.HandleProxy(conn)
if err != nil && err.Error() != "EOF" {
log.Warnf("connection crashed/dropped during proxy handling: '%s'", err.Error())
return
}
}()
}
}
func verifyCert(cCtx *cli.Context) error {
domainList := strings.Split(cCtx.String("domain-names"), ":")
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
bismuthClient, err := client.New(pubKey, privKey)
if err != nil {
return err
}
dialedConn, err := net.Dial("tcp", cCtx.String("signing-server"))
if err != nil {
return err
}
conn, certResults, err := bismuthClient.Conn(dialedConn)
if err != nil {
return err
}
if certResults.OverallTrustScore < 50 {
return fmt.Errorf("overall trust score is below 50% for certificate")
}
fmt.Println("Sending signing request to sign server...")
hasBeenTrusted, err := signingclient.RequestDomainToBeTrusted(conn, domainList, "")
if hasBeenTrusted {
fmt.Println("Server has been successfully signed.")
} else {
fmt.Println("Server has not been successfully signed.")
os.Exit(1)
}
return nil
}
func signCert(cCtx *cli.Context) error {
domainList := strings.Split(cCtx.String("domain-names"), ":")
keyFingerprint, err := hex.DecodeString(cCtx.String("key-fingerprint"))
if err != nil {
return err
}
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
bismuthClient, err := client.New(pubKey, privKey)
if err != nil {
return err
}
dialedConn, err := net.Dial("tcp", cCtx.String("signing-server"))
if err != nil {
return err
}
conn, certResults, err := bismuthClient.Conn(dialedConn)
if err != nil {
return err
}
if certResults.OverallTrustScore < 50 {
return fmt.Errorf("overall trust score is below 50% for certificate")
}
isTrusted, err := signingclient.IsDomainTrusted(conn, keyFingerprint, domainList)
fmt.Printf("is certificate trusted: %t\n", isTrusted)
if !isTrusted {
os.Exit(1)
}
return nil
}
func main() {
fmt.Println(asciiArt)
fmt.Print("Implementation of the Bismuth protocol\n\n")
logLevel := os.Getenv("BISMUTHD_LOG_LEVEL")
if logLevel != "" {
switch logLevel {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "fatal":
log.SetLevel(log.FatalLevel)
}
}
app := &cli.App{
Name: "bismuthd",
Usage: "reference implementation of the bismuth protocol",
EnableBashCompletion: true,
Commands: []*cli.Command{
{
Name: "client",
Aliases: []string{"c"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
Required: true,
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
Required: true,
},
&cli.StringFlag{
Name: "ip",
Usage: "IP to listen on for SOCKS5 server",
Value: "127.0.0.1",
},
&cli.StringFlag{
Name: "port",
Usage: "port to listen on for SOCKS5 server",
Value: "1080",
},
&cli.BoolFlag{
Name: "disable-gui",
Usage: "if set, disables the GUI and automatically accepts all certificates (not recommended)",
},
&cli.BoolFlag{
Name: "route-all-traffic",
Usage: "if set, routes all traffic through Bismuth, instead of just IPs that end with the fictional TLD '.bismuth'",
},
},
Usage: "client for the Bismuth protocol",
Action: clientEntrypoint,
},
{
Name: "server",
Aliases: []string{"s"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
Required: true,
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
Required: true,
},
&cli.StringFlag{
Name: "source-ip",
Usage: "IP to connect to",
Required: true,
},
&cli.StringFlag{
Name: "source-port",
Usage: "port to connect to",
Required: true,
},
&cli.StringFlag{
Name: "signing-servers",
Usage: "servers trusting/\"signing\" the public key. seperated using colons",
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
},
&cli.StringFlag{
Name: "dest-ip",
Usage: "IP to listen on",
Value: "0.0.0.0",
},
&cli.StringFlag{
Name: "dest-port",
Usage: "port to listen on",
Required: true,
},
},
Usage: "server for the Bismuth protocol",
Action: serverEntrypoint,
},
{
Name: "test-sign-server",
Aliases: []string{"tss"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
Required: true,
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
Required: true,
},
&cli.StringFlag{
Name: "ip",
Usage: "IP to listen on",
Value: "0.0.0.0",
},
&cli.StringFlag{
Name: "port",
Usage: "port to listen on",
Value: "9090",
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
},
&cli.StringFlag{
Name: "signing-server",
Usage: "domain names the key is authorized to use. seperated using colons",
},
},
Usage: "test signing server for the Bismuth protocol",
Action: signingServerEntrypoint,
},
{
Name: "sign-tool",
Aliases: []string{"st"},
Subcommands: []*cli.Command{
{
Name: "is-verified",
Aliases: []string{"i", "iv", "cv"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "key-fingerprint",
Usage: "fingerprint of key",
Required: true,
},
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
Required: true,
},
&cli.StringFlag{
Name: "signing-server",
Usage: "signing server to use",
Required: true,
},
},
Usage: "check if a certificate is verified for Bismuth",
Action: signCert,
},
{
Name: "verify-cert",
Aliases: []string{"v", "vc"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
Required: true,
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
Required: true,
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
Required: true,
},
&cli.StringFlag{
Name: "signing-server",
Usage: "signing server to use",
Required: true,
},
},
Usage: "verifies certificate for Bismuth",
Action: verifyCert,
},
},
Usage: "signing tool for Bismuth",
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}