client-interface-cleanup #6

Merged
johnnylee merged 14 commits from client-interface-cleanup into main 2025-09-17 08:00:13 +00:00
11 changed files with 261 additions and 14 deletions
Showing only changes of commit e458e43d83 - Show all commits

View File

@ -65,9 +65,10 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
Type=simple
User=user
WorkingDirectory=/home/user/
ExecStart=/home/user/vppn -name mynetwork -hub-address https://my.hub -api-key 1234567890
ExecStart=/home/user/vppn run my_net_name https://my.hub my_api_key
Restart=always
RestartSec=8
TimeoutStopSec=24
[Install]
WantedBy=multi-user.target

View File

@ -7,5 +7,5 @@ import (
func main() {
log.SetFlags(0)
peer.Main()
peer.Main2()
}

2
go.mod
View File

@ -1,6 +1,6 @@
module vppn
go 1.24.1
go 1.25.1
require (
git.crumpington.com/lib/go v0.9.0

View File

@ -29,6 +29,10 @@ func configDir(netName string) string {
return filepath.Join(d, ".vppn", netName)
}
func lockFilePath(netName string) string {
return filepath.Join(configDir(netName), "__lock__")
}
func peerConfigPath(netName string) string {
return filepath.Join(configDir(netName), "config.json")
}
@ -41,6 +45,10 @@ func startupCountPath(netName string) string {
return filepath.Join(configDir(netName), "startup_count.json")
}
func statusSocketPath(netName string) string {
return filepath.Join(configDir(netName), "status.sock")
}
func storeJson(x any, outPath string) error {
outDir := filepath.Dir(outPath)
_ = os.MkdirAll(outDir, 0700)

View File

@ -19,7 +19,7 @@ const (
controlCipherOverhead = 16
dataCipherOverhead = 16
signOverhead = 64
signingOverhead = 64
pingInterval = 8 * time.Second
timeoutInterval = 30 * time.Second

153
peer/main2.go Normal file
View File

@ -0,0 +1,153 @@
package peer
import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
)
// Usage:
//
// vppn netName run
// vppn netName status
func Main2() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `%s COMMAND [ARGUMENTS...]
Available commands:
run
status
`, os.Args[0])
os.Exit(1)
}
if len(os.Args) < 2 {
printUsage()
}
command := os.Args[1]
switch command {
case "run":
main_run()
case "status":
main_status()
default:
printUsage()
}
}
// ----------------------------------------------------------------------------
type mainArgs struct {
NetName string
HubAddress string
APIKey string
}
func main_run() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `Usage: %s run NETWORK_NAME HUB_ADDRESS API_KEY
NETWORK_NAME
Unique name of the network interface created. The network name
shouldn't change between invocations of the application.
HUB_ADDRESS
The address of the hub server. This should also contain the scheme, for
example https://hub.domain.com/.
API_KEY
The API key assigned to this peer by the hub.
`, os.Args[0])
os.Exit(1)
}
if len(os.Args) != 5 {
printUsage()
}
args := mainArgs{
NetName: os.Args[2],
HubAddress: os.Args[3],
APIKey: os.Args[4],
}
newPeerMain(args).Run()
}
// ----------------------------------------------------------------------------
func main_status() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `Usage: %s status NETWORK_NAME
NETWORK_NAME
Unique name of the network interface created.
`, os.Args[0])
os.Exit(1)
}
if len(os.Args) != 3 {
printUsage()
}
netName := os.Args[2]
client := http.Client{
Transport: &http.Transport{
Dial: func(_, _ string) (net.Conn, error) {
return net.Dial("unix", statusSocketPath(netName))
},
},
}
getURL := "http://unix" + statusSocketPath(netName)
resp, err := client.Get(getURL)
if err != nil {
log.Fatalf("Failed to get response: %v", err)
}
report := StatusReport{}
if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
log.Fatalf("Failed to decode status report: %v", err)
}
b := strings.Builder{}
for _, status := range report.Remotes {
b.WriteString(fmt.Sprintf("%3d ", status.PeerIP))
if status.Up {
b.WriteString("UP ")
} else {
b.WriteString("DOWN ")
}
if status.Relay && status.Direct {
b.WriteString("RELAY ")
} else if status.Server {
b.WriteString("SERVER ")
} else {
b.WriteString("CLIENT ")
}
if status.Direct {
b.WriteString("DIRECT ")
} else {
b.WriteString("RELAYED ")
}
b.WriteString(fmt.Sprintf("%45s ", status.DirectAddr))
b.WriteString(status.Name)
b.WriteString("\n")
}
fmt.Print(b.String())
}

View File

@ -15,16 +15,16 @@ func createLocalDiscoveryPacket(localIP byte, signingKey []byte) []byte {
}
buf := make([]byte, headerSize)
h.Marshal(buf)
out := make([]byte, headerSize+signOverhead)
out := make([]byte, headerSize+signingOverhead)
return sign.Sign(out[:0], buf, (*[64]byte)(signingKey))
}
func headerFromLocalDiscoveryPacket(pkt []byte) (h Header, ok bool) {
if len(pkt) != headerSize+signOverhead {
if len(pkt) != headerSize+signingOverhead {
return
}
h.Parse(pkt[signOverhead:])
h.Parse(pkt[signingOverhead:])
ok = true
return
}

View File

@ -13,6 +13,8 @@ import (
"net/url"
"os"
"vppn/m"
"git.crumpington.com/lib/go/flock"
)
type peerMain struct {
@ -20,12 +22,7 @@ type peerMain struct {
ifReader *IFReader
connReader *ConnReader
hubPoller *HubPoller
}
type mainArgs struct {
NetName string
HubAddress string
APIKey string
lockFile *os.File
}
func newPeerMain(args mainArgs) *peerMain {
@ -33,6 +30,14 @@ func newPeerMain(args mainArgs) *peerMain {
log.Printf("[Main] "+s, args...)
}
lockFile, err := flock.TryLock(lockFilePath(args.NetName))
if err != nil {
log.Fatalf("Failed to open lock file: %v", err)
}
if lockFile == nil {
log.Fatalf("Failed to obtain file lock.")
}
config, err := loadPeerConfig(args.NetName)
if err != nil {
logf("Failed to load configuration: %v", err)
@ -100,11 +105,15 @@ func newPeerMain(args mainArgs) *peerMain {
log.Fatalf("Failed to create hub poller: %v", err)
}
// Start status server.
go runStatusServer(g, statusSocketPath(args.NetName))
return &peerMain{
Globals: g,
ifReader: NewIFReader(g),
connReader: NewConnReader(g, conn),
hubPoller: hubPoller,
lockFile: lockFile,
}
}

View File

@ -106,6 +106,25 @@ func (r *Remote) encryptControl(conf remoteConfig, packet []byte) []byte {
return conf.ControlCipher.Encrypt(h, packet, packet[len(packet):cap(packet)])
}
func (r *Remote) Status() (RemoteStatus, bool) {
conf := r.conf()
if conf.Peer == nil {
return RemoteStatus{}, false
}
return RemoteStatus{
PeerIP: conf.Peer.PeerIP,
Up: conf.Up,
Name: conf.Peer.Name,
PublicIP: conf.Peer.PublicIP,
Port: conf.Peer.Port,
Relay: conf.Peer.Relay,
Server: conf.Server,
Direct: conf.Direct,
DirectAddr: conf.DirectAddr,
}, true
}
// ----------------------------------------------------------------------------
// SendDataTo sends a data packet to the remote, called by the IFReader.

View File

@ -171,7 +171,7 @@ func (r *remoteFSM) stateServer_onSyn(msg controlMsg[packetSyn]) {
conf.DirectAddr = msg.SrcAddr
// Update data cipher if the key has changed.
if !conf.DataCipher.HasKey(p.SharedKey) {
if conf.DataCipher == nil || !conf.DataCipher.HasKey(p.SharedKey) {
conf.DataCipher = newDataCipherFromKey(p.SharedKey)
}

57
peer/statusserver.go Normal file
View File

@ -0,0 +1,57 @@
package peer
import (
"encoding/json"
"log"
"net"
"net/http"
"net/netip"
"os"
)
type StatusReport struct {
Remotes []RemoteStatus
}
type RemoteStatus struct {
PeerIP byte
Up bool
Name string
PublicIP []byte
Port uint16
Relay bool
Server bool
Direct bool
DirectAddr netip.AddrPort
}
func runStatusServer(g Globals, socketPath string) {
_ = os.RemoveAll(socketPath)
handler := func(w http.ResponseWriter, r *http.Request) {
report := StatusReport{
Remotes: make([]RemoteStatus, 0, 255),
}
for i := range g.RemotePeers {
remote := g.RemotePeers[i].Load()
status, ok := remote.Status()
if !ok {
continue
}
report.Remotes = append(report.Remotes, status)
}
json.NewEncoder(w).Encode(report)
}
server := http.Server{
Handler: http.HandlerFunc(handler),
}
unixListener, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatalf("Failed to bind to unix socket: %v", err)
}
if err := server.Serve(unixListener); err != nil {
log.Fatalf("Failed to serve on unix socket: %v", err)
}
}