Compare commits

...

5 Commits

Author SHA1 Message Date
nervuri cfcbbb0962 add proper read & write timeouts 2023-03-11 20:44:39 +00:00
nervuri 0a124def84 explicitly make minimum TLS version = 1.0 2023-03-11 20:44:39 +00:00
nervuri 2eedb5537f add comments to explain prefixConn
Also change a few variable names, for clarification.
2023-03-11 15:16:42 +00:00
nervuri 72b3259428 move HTML and gemtext to separate files 2023-03-11 12:48:41 +00:00
nervuri c611d46b4f output errors to stderr 2023-03-11 12:00:39 +00:00
4 changed files with 142 additions and 120 deletions

View File

@ -27,8 +27,7 @@ func dropPrivileges(userToSwitchTo string) {
// Check supplementary groups. // Check supplementary groups.
groups, err := syscall.Getgroups() groups, err := syscall.Getgroups()
if err != nil { if err != nil {
fmt.Println(err) fatalError(err)
os.Exit(1)
} }
for _, groupID := range groups { for _, groupID := range groups {
if groupID == 0 { if groupID == 0 {
@ -43,60 +42,53 @@ func dropPrivileges(userToSwitchTo string) {
fmt.Println("When running as root, use the -u option to switch to an unprivileged user.") fmt.Println("When running as root, use the -u option to switch to an unprivileged user.")
os.Exit(1) os.Exit(1)
} else if rootPrimaryGroup || rootSupplementaryGroup { } else if rootPrimaryGroup || rootSupplementaryGroup {
fmt.Println("The user running the program is in the root group;") fatalError("The user running the program is in the root group;\n" +
fmt.Println("use the -u option to switch to an unprivileged user.") "use the -u option to switch to an unprivileged user.")
os.Exit(1)
} }
} else { // userToSwitchTo != "" } else { // userToSwitchTo != ""
// Get user and group IDs for the user we want to switch to. // Get user and group IDs for the user we want to switch to.
userInfo, err := user.Lookup(userToSwitchTo) userInfo, err := user.Lookup(userToSwitchTo)
if err != nil { if err != nil {
fmt.Println(err) fatalError(err)
os.Exit(1)
} }
// Convert group id and user id from string to int. // Convert group id and user id from string to int.
gid, err := strconv.Atoi(userInfo.Gid) gid, err := strconv.Atoi(userInfo.Gid)
if err != nil { if err != nil {
fmt.Println(err) fatalError(err)
os.Exit(1)
} }
uid, err := strconv.Atoi(userInfo.Uid) uid, err := strconv.Atoi(userInfo.Uid)
if err != nil { if err != nil {
fmt.Println(err) fatalError(err)
os.Exit(1)
} }
// If the user we want to switch to has root privileges, stop execution. // If the user we want to switch to has root privileges, stop execution.
if uid == 0 || gid == 0 { if uid == 0 || gid == 0 {
fmt.Println("Running as root is not allowed.") fatalError("Running as root is not allowed.")
os.Exit(1)
} }
// Unset supplementary group IDs. // Unset supplementary group IDs.
err = syscall.Setgroups([]int{}) err = syscall.Setgroups([]int{})
if err != nil { if err != nil {
fmt.Println("Failed to unset supplementary group IDs: " + err.Error()) fmt.Fprintln(os.Stderr,
"Failed to unset supplementary group IDs: "+err.Error())
if rootSupplementaryGroup { if rootSupplementaryGroup {
fmt.Println("Failed to drop root privileges. Exiting...") fatalError("Failed to drop root privileges. Exiting...")
os.Exit(1)
} }
} }
// Set group ID (real and effective). // Set group ID (real and effective).
err = syscall.Setgid(gid) err = syscall.Setgid(gid)
if err != nil { if err != nil {
fmt.Println("Failed to set group ID: " + err.Error()) fmt.Fprintln(os.Stderr, "Failed to set group ID: "+err.Error())
if rootPrimaryGroup { if rootPrimaryGroup {
fmt.Println("Failed to drop root privileges. Exiting...") fatalError("Failed to drop root privileges. Exiting...")
os.Exit(1)
} }
} }
// Set user ID (real and effective). // Set user ID (real and effective).
err = syscall.Setuid(uid) err = syscall.Setuid(uid)
if err != nil { if err != nil {
fmt.Println("Failed to set user ID: " + err.Error()) fmt.Fprintln(os.Stderr, "Failed to set user ID: "+err.Error())
if rootUser { if rootUser {
fmt.Println("Failed to drop root privileges. Exiting...") fatalError("Failed to drop root privileges. Exiting...")
os.Exit(1)
} }
} }

22
index.gmi Normal file
View File

@ -0,0 +1,22 @@
# TLS Client Hello Mirror
=> /json/v1 Your browser's TLS Client Hello, reflected as JSON
This test:
* reflects the complete Client Hello message, preserving the order in which TLS parameters and extensions are sent;
* can be used to check for TLS privacy pitfalls (session resumption, TLS fingerprinting, system time exposure);
* supports multiple protocols (currently HTTP and Gemini);
* is free as in freedom and trivial to self-host.
JSON only, for now. The API is largely stable - fields may be added, but existing fields will not be modified or removed. IANA-assigned codes for TLS parameters and extensions are available at:
=> https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml TLS parameters
=> https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml TLS extensions
Note that these lists do not include draft extensions and GREASE values. Missing values will be documented here as the project evolves.
_____________________
=> https://nervuri.net/ Author: nervuri
=> https://tildegit.org/nervuri/client-hello-mirror Source (contributions welcome)
=> https://www.gnu.org/licenses/agpl-3.0.en.html License: AGPL-3.0-or-later

62
index.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#000">
<meta name="referrer" content="no-referrer">
<title>TLS Client Hello Mirror</title>
<style>
:root {
color-scheme: dark;
}
::selection {
color: #FFF;
background-color: #070;
}
body {
color: #DDD;
background-color: #000;
margin: 1em auto;
max-width: 38em;
padding: 0 .62em;
font: 1.1em/1.62 sans-serif;
}
@media print{
body{
max-width: none;
}
}
a:link {color:#EEE;}
a:visited {color:#EEE;}
a:hover {color:#FFF;}
a:active {color:#FFF;}
</style>
<main>
<center>
<h1>TLS Client Hello Mirror</h1>
</center>
<h3><a href="/json/v1">Your browser's TLS Client Hello, reflected as JSON</a></h3>
<p>This test:
<ul>
<li>reflects the complete Client Hello message, preserving the order in which TLS parameters and extensions are sent;</li>
<li>can be used to check for TLS privacy pitfalls (<a href="https://svs.informatik.uni-hamburg.de/publications/2018/2018-12-06-Sy-ACSAC-Tracking_Users_across_the_Web_via_TLS_Session_Resumption.pdf">session resumption</a>, <a href="https://tlsfingerprint.io/">TLS fingerprinting</a>, <a href="https://datatracker.ietf.org/doc/html/draft-mathewson-no-gmtunixtime">system time exposure</a>);</li>
<li>supports both HTTP and <a href="https://gemini.circumlunar.space/">Gemini</a>;</li>
<li>is <a href="https://www.gnu.org/philosophy/free-sw.en.html">free as in freedom</a> and trivial to self-host.</li>
</ul>
</p>
<p>JSON only, for now, but a UI is on <a href="https://tildegit.org/nervuri/client-hello-mirror#roadmap">the roadmap</a>.</p>
<p>The API is largely stable - fields may be added, but existing fields will not be modified or removed. IANA-assigned codes for TLS parameters and extensions are documented at:
<ul>
<li><a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml">TLS parameters</a></li>
<li><a href="https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml">TLS extensions</a></li>
</ul>
Note that these lists do not include draft extensions and <a href="https://datatracker.ietf.org/doc/html/rfc8701">GREASE</a> values. Missing values will be documented here as the project evolves.
</p>
</main>
<hr>
<footer>
Author: <a href="https://nervuri.net/">nervuri</a><br>
<a href="https://tildegit.org/nervuri/client-hello-mirror">Source</a> (contributions welcome)<br>
License: <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL-3.0-or-later</a>
</footer>
</html>

140
server.go
View File

@ -6,18 +6,22 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/tls" "crypto/tls"
_ "embed"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"io" "io"
"log" "log"
"net" "net"
"net/url" "net/url"
"os"
"strings" "strings"
"time" "time"
) )
const readTimeout = 10 // seconds
const writeTimeout = 10 // seconds
type tlsConnectionInfo struct { type tlsConnectionInfo struct {
TlsVersion uint16 `json:"tls_version"` TlsVersion uint16 `json:"tls_version"`
CipherSuite uint16 `json:"cipher_suite"` CipherSuite uint16 `json:"cipher_suite"`
@ -25,6 +29,9 @@ type tlsConnectionInfo struct {
NegotiatedProtocol string `json:"alpn_negotiated_protocol"` // ALPN NegotiatedProtocol string `json:"alpn_negotiated_protocol"` // ALPN
} }
// Connection wrapper that enables exposing the Client Hello to the
// request handler.
// See https://github.com/FiloSottile/mostly-harmless/tree/main/talks/asyncnet
type prefixConn struct { type prefixConn struct {
net.Conn net.Conn
io.Reader io.Reader
@ -34,91 +41,19 @@ func (c prefixConn) Read(p []byte) (int, error) {
return c.Reader.Read(p) return c.Reader.Read(p)
} }
const html = `<!DOCTYPE html> // Output to stderr and exit with error code 1.
<html lang="en"> // Like log.Fatal, but without the date&time prefix.
<meta charset="utf-8"> // Used before starting the server loop.
<meta name="viewport" content="width=device-width, initial-scale=1"> func fatalError(err ...any) {
<meta name="theme-color" content="#000"> logger := log.New(os.Stderr, "", 0)
<meta name="referrer" content="no-referrer"> logger.Fatal(err...)
<title>TLS Client Hello Mirror</title>
<style>
:root {
color-scheme: dark;
} }
::selection {
color: #FFF;
background-color: #070;
}
body {
color: #DDD;
background-color: #000;
margin: 1em auto;
max-width: 38em;
padding: 0 .62em;
font: 1.1em/1.62 sans-serif;
}
@media print{
body{
max-width: none;
}
}
a:link {color:#EEE;}
a:visited {color:#EEE;}
a:hover {color:#FFF;}
a:active {color:#FFF;}
</style>
<main>
<center>
<h1>TLS Client Hello Mirror</h1>
</center>
<h3><a href="/json/v1">Your browser's TLS Client Hello, reflected as JSON</a></h3>
<p>This test:
<ul>
<li>reflects the complete Client Hello message, preserving the order in which TLS parameters and extensions are sent;</li>
<li>can be used to check for TLS privacy pitfalls (<a href="https://svs.informatik.uni-hamburg.de/publications/2018/2018-12-06-Sy-ACSAC-Tracking_Users_across_the_Web_via_TLS_Session_Resumption.pdf">session resumption</a>, <a href="https://tlsfingerprint.io/">TLS fingerprinting</a>, <a href="https://datatracker.ietf.org/doc/html/draft-mathewson-no-gmtunixtime">system time exposure</a>);</li>
<li>supports both HTTP and <a href="https://gemini.circumlunar.space/">Gemini</a>;</li>
<li>is <a href="https://www.gnu.org/philosophy/free-sw.en.html">free as in freedom</a> and trivial to self-host.</li>
</ul>
</p>
<p>JSON only, for now, but a UI is on <a href="https://tildegit.org/nervuri/client-hello-mirror#roadmap">the roadmap</a>.</p>
<p>The API is largely stable - fields may be added, but existing fields will not be modified or removed. IANA-assigned codes for TLS parameters and extensions are documented at:
<ul>
<li><a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml">TLS parameters</a></li>
<li><a href="https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml">TLS extensions</a></li>
</ul>
Note that these lists do not include draft extensions and <a href="https://datatracker.ietf.org/doc/html/rfc8701">GREASE</a> values. Missing values will be documented here as the project evolves.
</p>
</main>
<hr>
<footer>
Author: <a href="https://nervuri.net/">nervuri</a><br>
<a href="https://tildegit.org/nervuri/client-hello-mirror">Source</a> (contributions welcome)<br>
License: <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL-3.0-or-later</a>
</footer>
</html>`
const gemtext = `# TLS Client Hello Mirror //go:embed index.html
var html string
=> /json/v1 Your browser's TLS Client Hello, reflected as JSON //go:embed index.gmi
var gemtext string
This test:
* reflects the complete Client Hello message, preserving the order in which TLS parameters and extensions are sent;
* can be used to check for TLS privacy pitfalls (session resumption, TLS fingerprinting, system time exposure);
* supports multiple protocols (currently HTTP and Gemini);
* is free as in freedom and trivial to self-host.
JSON only, for now. The API is largely stable - fields may be added, but existing fields will not be modified or removed. IANA-assigned codes for TLS parameters and extensions are available at:
=> https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml TLS parameters
=> https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml TLS extensions
Note that these lists do not include draft extensions and GREASE values. Missing values will be documented here as the project evolves.
_____________________
=> https://nervuri.net/ Author: nervuri
=> https://tildegit.org/nervuri/client-hello-mirror Source (contributions welcome)
=> https://www.gnu.org/licenses/agpl-3.0.en.html License: AGPL-3.0-or-later`
// Copy the Client Hello message before starting the TLS handshake. // Copy the Client Hello message before starting the TLS handshake.
func peek(conn net.Conn, tlsConfig *tls.Config) { func peek(conn net.Conn, tlsConfig *tls.Config) {
@ -144,21 +79,24 @@ func peek(conn net.Conn, tlsConfig *tls.Config) {
} }
rawClientHello := buf.Bytes() rawClientHello := buf.Bytes()
// "Put back" the Client Hello bytes we just read, so that they can be
// used in the TLS handshake. Concatenate the read bytes with the
// unread bytes using a MultiReader, inside a connection wrapper.
pConn := prefixConn{ pConn := prefixConn{
Conn: conn, Conn: conn,
Reader: io.MultiReader(&buf, conn), Reader: io.MultiReader(&buf, conn),
} }
server := tls.Server(pConn, tlsConfig) tlsConnection := tls.Server(pConn, tlsConfig)
err = server.Handshake() err = tlsConnection.Handshake()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
tlsHandler(server, rawClientHello) requestHandler(tlsConnection, rawClientHello)
} }
func tlsHandler(conn *tls.Conn, rawClientHello []byte) { func requestHandler(conn *tls.Conn, rawClientHello []byte) {
defer conn.Close() defer conn.Close()
scanner := bufio.NewScanner(conn) scanner := bufio.NewScanner(conn)
@ -169,7 +107,6 @@ func tlsHandler(conn *tls.Conn, rawClientHello []byte) {
log.Println(err) log.Println(err)
return return
} }
//log.Println(line)
var protocol string var protocol string
var path string // requested page var path string // requested page
@ -275,28 +212,25 @@ func main() {
flag.Parse() flag.Parse()
hostAndPort = flag.Arg(0) hostAndPort = flag.Arg(0)
if certFile == "" || keyFile == "" || hostAndPort == "" { if certFile == "" || keyFile == "" || hostAndPort == "" {
fmt.Println("usage: client-hello-mirror -c cert.pem -k key.pem [-u user] host:port") fatalError("usage: client-hello-mirror -c cert.pem -k key.pem [-u user] host:port")
return
} }
// Load cert // Load cert
cert, err := tls.LoadX509KeyPair(certFile, keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil { if err != nil {
log.Fatal(err) fatalError(err)
return
} }
// TLS config // TLS config
tlsConfig := tls.Config{ tlsConfig := tls.Config{
Certificates: []tls.Certificate{cert}, Certificates: []tls.Certificate{cert},
//MaxVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS10,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
// Listen for connections // Listen for connections
ln, err := net.Listen("tcp", hostAndPort) ln, err := net.Listen("tcp", hostAndPort)
if err != nil { if err != nil {
log.Println(err) fatalError(err)
return
} }
defer ln.Close() defer ln.Close()
@ -305,12 +239,24 @@ func main() {
log.Println("Server started") log.Println("Server started")
for { for {
// Wait for a connection.
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
log.Println("Error accepting: ", err.Error()) log.Println("Error accepting: ", err.Error())
continue continue
} }
conn.SetReadDeadline(time.Now().Add(5 * time.Second)) // timeout // Set connection timeouts.
err = conn.SetReadDeadline(time.Now().Add(readTimeout * time.Second))
if err != nil {
log.Println("SetReadDeadline error: ", err.Error())
continue
}
err = conn.SetWriteDeadline(time.Now().Add(writeTimeout * time.Second))
if err != nil {
log.Println("SetWriteDeadline error: ", err.Error())
continue
}
// Process the request.
go peek(conn, &tlsConfig) go peek(conn, &tlsConfig)
} }
} }