refactor: extract session/netcfg/tunnel, add mac/dhcp/policy-route flags
- Split cmd/softether-go into main.go (flags, reconnect loop) and session.go (session lifecycle, DHCP orchestration) - Extract network config to pkg/netcfg (TAP config, routing, DNS, policy routes) - Move frame bridging to pkg/client/tunnel.go as Bridge() method - Add -mac, -dhcp, -policy-route-table CLI flags - Add SetMAC() to pkg/tap for deterministic DHCP assignments - Update all docs to reflect new structure and flags Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
846ed96ff4
commit
17c1063e1f
10 changed files with 495 additions and 332 deletions
|
|
@ -8,9 +8,13 @@ Standalone SoftEther VPN client written in Go. Connects to SoftEther VPN servers
|
|||
# Build
|
||||
go build -o softether-go ./cmd/softether-go/
|
||||
|
||||
# Connect
|
||||
# Connect (minimal)
|
||||
softether-go -host vpn.example.com -user admin -pass secret -plain-password
|
||||
|
||||
# Connect with full network setup
|
||||
softether-go -host vpn.example.com -user admin -pass secret \
|
||||
-plain-password -tap vpn0 -accept-default-gateway -accept-dns
|
||||
-plain-password -tap vpn0 -mac 5E:3B:6F:63:A8:3E \
|
||||
-accept-default-gateway -accept-dns -policy-route-table 200
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ import (
|
|||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.sagar.ch/sagar/softether-go/pkg/client"
|
||||
"git.sagar.ch/sagar/softether-go/pkg/dhcp"
|
||||
"git.sagar.ch/sagar/softether-go/pkg/netcfg"
|
||||
"git.sagar.ch/sagar/softether-go/pkg/tap"
|
||||
)
|
||||
|
||||
|
|
@ -30,11 +28,14 @@ func main() {
|
|||
password := flag.String("pass", "", "Authentication password")
|
||||
plainPass := flag.Bool("plain-password", false, "Send password as plaintext (AuthType 2)")
|
||||
tapName := flag.String("tap", "", "TAP interface name (empty = kernel-assigned)")
|
||||
macAddr := flag.String("mac", "", "TAP interface MAC address (e.g. 5E:3B:6F:63:A8:3E)")
|
||||
insecure := flag.Bool("insecure", false, "Skip TLS certificate verification")
|
||||
dhcpEnabled := flag.Bool("dhcp", true, "Run built-in DHCP client after connecting")
|
||||
reconnectDelay := flag.Duration("reconnect-delay", 5*time.Second, "Delay between reconnection attempts")
|
||||
acceptDefaultGW := flag.Bool("accept-default-gateway", false, "Install DHCP-provided gateway as default route")
|
||||
acceptStaticRoutes := flag.Bool("accept-static-routes", false, "Install DHCP classless static routes (option 121/249)")
|
||||
acceptDNS := flag.Bool("accept-dns", false, "Set /etc/resolv.conf from DHCP-provided DNS servers")
|
||||
policyRouteTable := flag.Int("policy-route-table", 0, "Policy routing table: route replies from VPN IP back through VPN gateway")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
|
|
@ -53,14 +54,10 @@ func main() {
|
|||
InsecureSkipVerify: *insecure,
|
||||
}
|
||||
|
||||
// Resolve server IP for host route
|
||||
serverIP := resolveHost(*host)
|
||||
|
||||
// Add host route to server via current default gateway
|
||||
cleanupRoute := addServerRoute(serverIP)
|
||||
serverIP := netcfg.ResolveHost(*host)
|
||||
cleanupRoute := netcfg.AddServerRoute(serverIP)
|
||||
defer cleanupRoute()
|
||||
|
||||
// Open TAP device once — reused across reconnections
|
||||
dev, err := tap.Open(*tapName)
|
||||
if err != nil {
|
||||
log.Fatalf("tap open: %v", err)
|
||||
|
|
@ -68,6 +65,16 @@ func main() {
|
|||
defer dev.Close()
|
||||
log.Printf("tap interface: %s", dev.Name)
|
||||
|
||||
if *macAddr != "" {
|
||||
hwAddr, err := net.ParseMAC(*macAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid mac: %v", err)
|
||||
}
|
||||
if err := dev.SetMAC(hwAddr); err != nil {
|
||||
log.Fatalf("set mac: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := dev.SetUp(); err != nil {
|
||||
log.Fatalf("tap up: %v", err)
|
||||
}
|
||||
|
|
@ -78,12 +85,19 @@ func main() {
|
|||
}
|
||||
log.Printf("tap mac: %s", mac)
|
||||
|
||||
// Signal handling
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
netOpts := netcfg.Options{
|
||||
DHCP: *dhcpEnabled,
|
||||
AcceptDefaultGW: *acceptDefaultGW,
|
||||
AcceptStaticRoutes: *acceptStaticRoutes,
|
||||
AcceptDNS: *acceptDNS,
|
||||
PolicyRouteTable: *policyRouteTable,
|
||||
}
|
||||
|
||||
for {
|
||||
err := runSession(cfg, dev, mac, *acceptDefaultGW, *acceptStaticRoutes, *acceptDNS, sig)
|
||||
err := runSession(cfg, dev, mac, netOpts, sig)
|
||||
if err == errShutdown {
|
||||
log.Println("shutting down")
|
||||
return
|
||||
|
|
@ -99,287 +113,3 @@ func main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errShutdown = fmt.Errorf("shutdown requested")
|
||||
|
||||
// runSession connects, runs DHCP, and bridges frames until disconnection or signal.
|
||||
func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, acceptDefaultGW, acceptStaticRoutes, acceptDNS bool, sig chan os.Signal) error {
|
||||
log.Printf("connecting to %s:%d hub=%s user=%s", cfg.Host, cfg.Port, cfg.Hub, cfg.Username)
|
||||
|
||||
sess, err := client.Connect(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect: %w", err)
|
||||
}
|
||||
log.Printf("connected: session=%s connection=%s", sess.SessionName, sess.ConnectionName)
|
||||
|
||||
tunnel := client.NewTunnel(sess)
|
||||
tunnel.StartKeepalive()
|
||||
defer tunnel.Close()
|
||||
|
||||
dhcpClient := dhcp.NewClient(mac)
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
// bridgeReady gates TAP writes — closed once DHCP completes
|
||||
bridgeReady := make(chan struct{})
|
||||
|
||||
// Server → TAP: start reading immediately so DHCP responses are received
|
||||
go func() {
|
||||
for {
|
||||
frames, err := tunnel.ReadFrames()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("read from server: %w", err)
|
||||
return
|
||||
}
|
||||
for _, frame := range frames {
|
||||
dhcpClient.FeedFrame(frame)
|
||||
select {
|
||||
case <-bridgeReady:
|
||||
if _, err := dev.Write(frame); err != nil {
|
||||
errCh <- fmt.Errorf("write to tap: %w", err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
// Drop non-DHCP frames until bridge is ready
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Run DHCP through the tunnel
|
||||
lease, err := runDHCP(dhcpClient, tunnel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcp: %w", err)
|
||||
}
|
||||
|
||||
// Configure TAP interface with lease
|
||||
cleanup, err := configureTAP(dev.Name, lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure tap: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Open the bridge — TAP writes now flow
|
||||
close(bridgeReady)
|
||||
|
||||
// TAP → Server
|
||||
go func() {
|
||||
buf := make([]byte, 1600)
|
||||
for {
|
||||
n, err := dev.Read(buf)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("read from tap: %w", err)
|
||||
return
|
||||
}
|
||||
frame := make([]byte, n)
|
||||
copy(frame, buf[:n])
|
||||
if err := tunnel.WriteFrames([][]byte{frame}); err != nil {
|
||||
errCh <- fmt.Errorf("write to server: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for error or signal
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case <-sig:
|
||||
return errShutdown
|
||||
}
|
||||
}
|
||||
|
||||
// runDHCP performs a DHCP exchange through the VPN tunnel.
|
||||
func runDHCP(dc *dhcp.Client, tunnel *client.Tunnel) (*dhcp.Lease, error) {
|
||||
log.Println("starting DHCP exchange...")
|
||||
|
||||
sendFrame := func(frame []byte) error {
|
||||
return tunnel.WriteFrames([][]byte{frame})
|
||||
}
|
||||
|
||||
// Run DHCP with a generous timeout — server might take a moment
|
||||
lease, err := dc.Run(sendFrame, 10*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ones, _ := lease.SubnetMask.Size()
|
||||
log.Printf("dhcp lease: ip=%s/%d gw=%s dns=%v ttl=%v",
|
||||
lease.ClientIP, ones, lease.Gateway, lease.DNS, lease.LeaseTime)
|
||||
for _, r := range lease.Routes {
|
||||
log.Printf("dhcp route: %s via %s", r.Dest.String(), r.Gateway)
|
||||
}
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
// configureTAP sets the IP address, routes, and DNS on the TAP interface.
|
||||
// Returns a cleanup function that undoes the changes.
|
||||
func configureTAP(ifname string, lease *dhcp.Lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS bool) (func(), error) {
|
||||
noop := func() {}
|
||||
ones, _ := lease.SubnetMask.Size()
|
||||
addr := fmt.Sprintf("%s/%d", lease.ClientIP, ones)
|
||||
|
||||
if err := run("ip", "addr", "add", addr, "dev", ifname); err != nil {
|
||||
return noop, fmt.Errorf("ip addr add: %w", err)
|
||||
}
|
||||
|
||||
if acceptStaticRoutes && len(lease.Routes) > 0 {
|
||||
for _, r := range lease.Routes {
|
||||
dest := r.Dest.String()
|
||||
if dest == "0.0.0.0/0" {
|
||||
if acceptDefaultGW {
|
||||
if err := run("ip", "route", "add", "default", "via", r.Gateway.String(), "dev", ifname, "metric", "50"); err != nil {
|
||||
log.Printf("warning: static default route: %v", err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := run("ip", "route", "add", dest, "via", r.Gateway.String(), "dev", ifname); err != nil {
|
||||
log.Printf("warning: static route %s via %s: %v", dest, r.Gateway, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if acceptDefaultGW && lease.Gateway != nil && !hasDefaultRoute(lease.Routes) {
|
||||
if err := run("ip", "route", "add", "default", "via", lease.Gateway.String(), "dev", ifname, "metric", "50"); err != nil {
|
||||
log.Printf("warning: default route: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DNS
|
||||
var savedResolv []byte
|
||||
if acceptDNS && len(lease.DNS) > 0 {
|
||||
savedResolv = backupResolv()
|
||||
writeResolv(lease.DNS)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
run("ip", "addr", "flush", "dev", ifname)
|
||||
if savedResolv != nil {
|
||||
restoreResolv(savedResolv)
|
||||
}
|
||||
}
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
// hasDefaultRoute checks if classless static routes include a default route (0.0.0.0/0).
|
||||
// Per RFC 3442, when option 121 is present it overrides option 3 (Router).
|
||||
func hasDefaultRoute(routes []dhcp.Route) bool {
|
||||
for _, r := range routes {
|
||||
ones, bits := r.Dest.Mask.Size()
|
||||
if ones == 0 && bits == 32 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const resolvPath = "/etc/resolv.conf"
|
||||
|
||||
func backupResolv() []byte {
|
||||
data, err := os.ReadFile(resolvPath)
|
||||
if err != nil {
|
||||
log.Printf("warning: backup resolv.conf: %v", err)
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func writeResolv(servers []net.IP) {
|
||||
var buf strings.Builder
|
||||
buf.WriteString("# Generated by softether-go\n")
|
||||
for _, ip := range servers {
|
||||
fmt.Fprintf(&buf, "nameserver %s\n", ip)
|
||||
}
|
||||
if err := os.WriteFile(resolvPath, []byte(buf.String()), 0644); err != nil {
|
||||
log.Printf("warning: write resolv.conf: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("dns: set nameservers %v", servers)
|
||||
}
|
||||
|
||||
func restoreResolv(saved []byte) {
|
||||
if err := os.WriteFile(resolvPath, saved, 0644); err != nil {
|
||||
log.Printf("warning: restore resolv.conf: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("dns: restored resolv.conf")
|
||||
}
|
||||
|
||||
// resolveHost resolves a hostname to an IPv4 address. If it's already an IP, returns it.
|
||||
func resolveHost(host string) net.IP {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip
|
||||
}
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
log.Printf("warning: could not resolve %s: %v", host, err)
|
||||
return nil
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.To4()
|
||||
}
|
||||
}
|
||||
log.Printf("warning: no IPv4 address for %s", host)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addServerRoute adds a host route to the VPN server via the current default gateway.
|
||||
// Returns a cleanup function that removes the route.
|
||||
func addServerRoute(serverIP net.IP) func() {
|
||||
noop := func() {}
|
||||
if serverIP == nil {
|
||||
return noop
|
||||
}
|
||||
|
||||
gw, dev := getDefaultGateway()
|
||||
if gw == nil {
|
||||
log.Println("warning: no default gateway found, skipping server route")
|
||||
return noop
|
||||
}
|
||||
|
||||
route := serverIP.String() + "/32"
|
||||
args := []string{"route", "add", route, "via", gw.String()}
|
||||
if dev != "" {
|
||||
args = append(args, "dev", dev)
|
||||
}
|
||||
|
||||
if err := run("ip", args...); err != nil {
|
||||
log.Printf("warning: add server route: %v", err)
|
||||
return noop
|
||||
}
|
||||
log.Printf("added route: %s via %s", route, gw)
|
||||
|
||||
return func() {
|
||||
run("ip", "route", "del", route)
|
||||
log.Printf("removed route: %s", route)
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultGateway parses `ip route` output to find the current default gateway.
|
||||
func getDefaultGateway() (net.IP, string) {
|
||||
out, err := exec.Command("ip", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return nil, ""
|
||||
}
|
||||
// Format: "default via 10.0.0.1 dev eth0 ..."
|
||||
fields := strings.Fields(string(out))
|
||||
var gw net.IP
|
||||
var dev string
|
||||
for i, f := range fields {
|
||||
if f == "via" && i+1 < len(fields) {
|
||||
gw = net.ParseIP(fields[i+1])
|
||||
}
|
||||
if f == "dev" && i+1 < len(fields) {
|
||||
dev = fields[i+1]
|
||||
}
|
||||
}
|
||||
return gw, dev
|
||||
}
|
||||
|
||||
func run(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
|
|||
91
cmd/softether-go/session.go
Normal file
91
cmd/softether-go/session.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.sagar.ch/sagar/softether-go/pkg/client"
|
||||
"git.sagar.ch/sagar/softether-go/pkg/dhcp"
|
||||
"git.sagar.ch/sagar/softether-go/pkg/netcfg"
|
||||
"git.sagar.ch/sagar/softether-go/pkg/tap"
|
||||
)
|
||||
|
||||
var errShutdown = fmt.Errorf("shutdown requested")
|
||||
|
||||
func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts netcfg.Options, sig chan os.Signal) error {
|
||||
log.Printf("connecting to %s:%d hub=%s user=%s", cfg.Host, cfg.Port, cfg.Hub, cfg.Username)
|
||||
|
||||
sess, err := client.Connect(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect: %w", err)
|
||||
}
|
||||
log.Printf("connected: session=%s connection=%s", sess.SessionName, sess.ConnectionName)
|
||||
|
||||
tunnel := client.NewTunnel(sess)
|
||||
tunnel.StartKeepalive()
|
||||
defer tunnel.Close()
|
||||
|
||||
// DHCP client feeds on server frames if enabled
|
||||
var dhcpClient *dhcp.Client
|
||||
var onFrame client.FrameHandler
|
||||
if opts.DHCP {
|
||||
dhcpClient = dhcp.NewClient(mac)
|
||||
onFrame = func(frame []byte) error {
|
||||
dhcpClient.FeedFrame(frame)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Start bridge in background
|
||||
bridgeErr := make(chan error, 1)
|
||||
go func() {
|
||||
bridgeErr <- tunnel.Bridge(dev.Read, dev.Write, onFrame)
|
||||
}()
|
||||
|
||||
// Run DHCP and configure network if enabled
|
||||
if opts.DHCP {
|
||||
lease, err := runDHCP(dhcpClient, tunnel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcp: %w", err)
|
||||
}
|
||||
|
||||
cleanup, err := netcfg.ConfigureTAP(dev.Name, lease, opts.AcceptDefaultGW, opts.AcceptStaticRoutes, opts.AcceptDNS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure tap: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if opts.PolicyRouteTable > 0 && lease.Gateway != nil {
|
||||
defer netcfg.ConfigurePolicyRoute(dev.Name, lease, opts.PolicyRouteTable)()
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-bridgeErr:
|
||||
return err
|
||||
case <-sig:
|
||||
return errShutdown
|
||||
}
|
||||
}
|
||||
|
||||
func runDHCP(dc *dhcp.Client, tunnel *client.Tunnel) (*dhcp.Lease, error) {
|
||||
log.Println("starting DHCP exchange...")
|
||||
|
||||
lease, err := dc.Run(func(frame []byte) error {
|
||||
return tunnel.WriteFrames([][]byte{frame})
|
||||
}, 10*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ones, _ := lease.SubnetMask.Size()
|
||||
log.Printf("dhcp lease: ip=%s/%d gw=%s dns=%v ttl=%v",
|
||||
lease.ClientIP, ones, lease.Gateway, lease.DNS, lease.LeaseTime)
|
||||
for _, r := range lease.Routes {
|
||||
log.Printf("dhcp route: %s via %s", r.Dest.String(), r.Gateway)
|
||||
}
|
||||
return lease, nil
|
||||
}
|
||||
|
|
@ -45,6 +45,18 @@ Key details:
|
|||
- String values use BufStr encoding: `uint32(strlen+1)` followed by `strlen` bytes (no null terminator on wire)
|
||||
- HTTP-transported Packs include a `pencore` dummy element with random padding
|
||||
|
||||
## Session lifecycle
|
||||
|
||||
After connecting, a session proceeds as:
|
||||
|
||||
1. **Start bridge** — bidirectional frame forwarding between the tunnel and TAP device begins immediately in the background
|
||||
2. **DHCP exchange** — if enabled, DHCP frames are sent through the tunnel while the bridge is already running. The DHCP client intercepts server frames via a callback (`FeedFrame`) before they reach the TAP device
|
||||
3. **Configure TAP** — IP address, routes, DNS, and policy routing are applied from the DHCP lease
|
||||
4. **Wait** — the session blocks until the bridge errors (server disconnect, network failure) or a signal arrives
|
||||
5. **Cleanup** — TAP addresses are flushed, DNS is restored, policy routes are removed
|
||||
|
||||
On disconnect, the reconnect loop waits and starts a new session with a fresh handshake and DHCP exchange.
|
||||
|
||||
## DHCP through the tunnel
|
||||
|
||||
SoftEther operates at Layer 2 — the tunnel carries raw Ethernet frames. The built-in DHCP client constructs complete Ethernet/IP/UDP/DHCP frames and sends them through the tunnel's frame transport.
|
||||
|
|
@ -62,7 +74,7 @@ DHCP Client VPN Tunnel DHCP Server (on VPN)
|
|||
│ │ │
|
||||
```
|
||||
|
||||
The DHCP client starts reading tunnel frames **before** sending DISCOVER, so responses are not missed. Non-DHCP frames received during the exchange are dropped (the TAP bridge is not yet active).
|
||||
The frame bridge runs concurrently with the DHCP exchange. All server frames pass through the `FeedFrame` callback, which identifies DHCP responses by transaction ID. Non-DHCP frames are written to the TAP device as normal (though the TAP has no IP yet, so the OS drops them).
|
||||
|
||||
### Why raw frames?
|
||||
|
||||
|
|
@ -81,19 +93,6 @@ The VPN tunnel transports Ethernet frames, and the DHCP exchange must happen *in
|
|||
|
||||
SoftEther servers with `DHCPForce` policy discard any packet whose source IP is not in the server's IP table with `DhcpAllocated=true`. When a session disconnects, the server calls `HubPaFree` which deletes **all** MAC and IP table entries for that session. The new session has no entries, so all traffic is dropped until a fresh DHCP exchange creates a new `DhcpAllocated=true` entry.
|
||||
|
||||
## Reconnection
|
||||
|
||||
On disconnect (TCP error, server timeout, etc.), the client:
|
||||
|
||||
1. Flushes IP addresses from the TAP interface
|
||||
2. Restores `/etc/resolv.conf` if DNS was modified
|
||||
3. Waits `-reconnect-delay` (default 5s)
|
||||
4. Establishes a new TLS connection and repeats the full handshake
|
||||
5. Runs a fresh DHCP exchange through the new tunnel
|
||||
6. Reconfigures the TAP interface with the new lease
|
||||
|
||||
The TAP device itself persists across reconnections — only its IP configuration is reset. The host route to the VPN server also persists.
|
||||
|
||||
## Routing
|
||||
|
||||
### Server host route
|
||||
|
|
@ -120,6 +119,19 @@ The metric ensures it takes precedence over any existing default route with a hi
|
|||
|
||||
With `-accept-static-routes`, classless static routes from DHCP option 121 or 249 are installed. A `0.0.0.0/0` entry in static routes is treated as a default gateway and only installed if `-accept-default-gateway` is also set. Per RFC 3442, option 121 takes precedence over option 3 (Router) when present.
|
||||
|
||||
### Policy routing
|
||||
|
||||
With `-policy-route-table N`, the client sets up policy routing for asymmetric return paths:
|
||||
|
||||
```
|
||||
ip route replace default via <VPN_GW> dev <TAP> table N
|
||||
ip rule add from <VPN_IP> table N
|
||||
```
|
||||
|
||||
This is needed when the VPN server has port forwards to the client. Without policy routing, inbound traffic arrives via the VPN tunnel but reply packets use the default route (home router) instead of going back through the tunnel. The remote host sees replies from a different IP and drops them.
|
||||
|
||||
The policy route is cleaned up on disconnect and re-applied with each new DHCP lease (since the VPN IP may change).
|
||||
|
||||
## Keepalive
|
||||
|
||||
The client sends keepalive packets every 3 seconds. A keepalive is `uint32(0xFFFFFFFF) + uint32(randSize) + randData`. The server sends keepalives in the same format. Keepalive packets are silently consumed and never forwarded to the TAP device.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ Standalone SoftEther VPN client written in Go. Connects to SoftEther VPN servers
|
|||
- Automatic reconnection with fresh DHCP on each reconnect
|
||||
- Host route to VPN server via existing default gateway (prevents routing loops)
|
||||
- Classless static routes (DHCP option 121/249, RFC 3442)
|
||||
- Policy routing for asymmetric return paths (VPN port forwards)
|
||||
- DNS configuration from DHCP lease (backup/restore of `/etc/resolv.conf`)
|
||||
- Deterministic MAC address support for stable DHCP assignments
|
||||
- Hashed password (SHA-0) and plaintext password (RADIUS/external) authentication
|
||||
- Single static binary, Linux only
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@
|
|||
```
|
||||
softether-go/
|
||||
├── cmd/softether-go/
|
||||
│ └── main.go CLI entry point
|
||||
│ ├── main.go Flag parsing, TAP setup, reconnect loop
|
||||
│ └── session.go Session lifecycle, DHCP orchestration
|
||||
├── pkg/
|
||||
│ ├── client/
|
||||
│ │ ├── client.go SoftEther handshake and session
|
||||
│ │ ├── tunnel.go TCP block framing and keepalive
|
||||
│ │ ├── tunnel.go TCP block framing, keepalive, frame bridging
|
||||
│ │ └── crypto.go SHA-0 and password hashing
|
||||
│ ├── protocol/
|
||||
│ │ ├── http.go HTTP transport layer
|
||||
│ │ ├── http.go TLS connection, HTTP transport layer
|
||||
│ │ └── pack.go Pack binary serialization
|
||||
│ ├── dhcp/
|
||||
│ │ └── dhcp.go DHCP client (raw Ethernet frames)
|
||||
│ ├── netcfg/
|
||||
│ │ └── netcfg.go TAP configuration, routing, DNS management
|
||||
│ └── tap/
|
||||
│ └── tap.go Linux TAP device management
|
||||
├── docs/ Documentation
|
||||
|
|
@ -27,18 +30,17 @@ softether-go/
|
|||
|
||||
### `cmd/softether-go`
|
||||
|
||||
CLI entry point. Handles flag parsing, signal handling, and the reconnection loop. On each session:
|
||||
- Connects to the server
|
||||
- Runs DHCP through the tunnel
|
||||
- Configures the TAP interface (IP, routes, DNS)
|
||||
- Bridges Ethernet frames between the TAP device and the VPN tunnel
|
||||
- Cleans up on disconnect and retries
|
||||
CLI entry point, split into two files:
|
||||
|
||||
**`main.go`** — flag parsing, TAP device creation, MAC configuration, signal handling, and the reconnect loop. Calls `runSession` for each connection attempt.
|
||||
|
||||
**`session.go`** — one VPN session lifecycle: connect to server, start bridge, run DHCP, configure TAP (IP/routes/DNS/policy routing), and wait for disconnect or signal. Also contains `runDHCP` which orchestrates the DHCP exchange through the tunnel.
|
||||
|
||||
### `pkg/client`
|
||||
|
||||
**`client.go`** — implements the SoftEther handshake: TLS connect, signature upload, hello/auth/welcome pack exchange. Exports `Connect(Config) (*Session, error)` and the `Config`/`Session` types.
|
||||
|
||||
**`tunnel.go`** — TCP block framing after the HTTP handshake. `ReadFrames()` reads batches of Ethernet frames from the server. `WriteFrames()` sends batches. `StartKeepalive()` sends periodic keepalive packets (every 3s) to prevent server timeout.
|
||||
**`tunnel.go`** — TCP block framing after the HTTP handshake completes. `ReadFrames()` reads batches of Ethernet frames from the server. `WriteFrames()` sends batches. `Bridge()` runs bidirectional frame forwarding between the tunnel and a TAP device, with an optional `FrameHandler` callback for intercepting frames (used by DHCP). `StartKeepalive()` sends periodic keepalive packets (every 3s).
|
||||
|
||||
**`crypto.go`** — SHA-0 implementation (differs from SHA-1 only in the message schedule — no left-rotate). `HashPassword()` produces `SHA0(password)`. `SecurePassword()` produces `SHA0(hashed + serverRandom)`.
|
||||
|
||||
|
|
@ -52,6 +54,10 @@ CLI entry point. Handles flag parsing, signal handling, and the reconnection loo
|
|||
|
||||
**`dhcp.go`** — DHCP client that constructs complete Ethernet/IP/UDP/DHCP frames. The full DHCP exchange (DISCOVER → OFFER → REQUEST → ACK) runs through the VPN tunnel's frame transport. Parses lease information including classless static routes (option 121/249, RFC 3442).
|
||||
|
||||
### `pkg/netcfg`
|
||||
|
||||
**`netcfg.go`** — network configuration for the VPN tunnel. `ConfigureTAP()` sets IP address, routes, and DNS on the TAP interface from a DHCP lease. `ConfigurePolicyRoute()` sets up policy routing for asymmetric return paths. `AddServerRoute()` adds a host route to the VPN server via the current default gateway. `ResolveHost()` resolves hostnames to IPv4.
|
||||
|
||||
### `pkg/tap`
|
||||
|
||||
**`tap.go`** — Linux TAP (Layer 2) device management via `/dev/net/tun`. Opens TAP devices with `IFF_TAP | IFF_NO_PI`, reads/writes raw Ethernet frames. Provides `MAC()` to get the hardware address and `SetUp()` to bring the interface up.
|
||||
**`tap.go`** — Linux TAP (Layer 2) device management via `/dev/net/tun`. Opens TAP devices with `IFF_TAP | IFF_NO_PI`, reads/writes raw Ethernet frames. Provides `MAC()` and `SetMAC()` for hardware address management, and `SetUp()` to bring the interface up.
|
||||
|
|
|
|||
|
|
@ -19,11 +19,14 @@ softether-go [flags]
|
|||
| `-port` | `443` | Server port |
|
||||
| `-hub` | `DEFAULT` | Virtual hub name |
|
||||
| `-tap` | *(auto)* | TAP interface name (kernel-assigned if empty) |
|
||||
| `-mac` | *(auto)* | TAP interface MAC address (e.g. `5E:3B:6F:63:A8:3E`) |
|
||||
| `-plain-password` | `false` | Send password as plaintext (AuthType 2, for RADIUS/external auth) |
|
||||
| `-insecure` | `false` | Skip TLS certificate verification |
|
||||
| `-dhcp` | `true` | Run built-in DHCP client after connecting |
|
||||
| `-accept-default-gateway` | `false` | Install DHCP-provided gateway as default route |
|
||||
| `-accept-static-routes` | `false` | Install DHCP classless static routes (option 121/249) |
|
||||
| `-accept-dns` | `false` | Set `/etc/resolv.conf` from DHCP-provided DNS servers |
|
||||
| `-policy-route-table` | `0` | Policy routing table number (0 = disabled) |
|
||||
| `-reconnect-delay` | `5s` | Delay between reconnection attempts |
|
||||
|
||||
## Authentication
|
||||
|
|
@ -46,6 +49,18 @@ softether-go -host vpn.example.com -user admin -pass secret -plain-password
|
|||
|
||||
These flags control what the client does with the DHCP lease it receives from the VPN server.
|
||||
|
||||
### `-mac`
|
||||
|
||||
Sets a specific MAC address on the TAP interface before connecting. Useful for deterministic DHCP assignments — the server sees the same MAC across reconnects and can assign the same IP.
|
||||
|
||||
```bash
|
||||
softether-go -host vpn.example.com -user admin -mac 5E:3B:6F:63:A8:3E
|
||||
```
|
||||
|
||||
### `-dhcp`
|
||||
|
||||
Enabled by default. Runs the built-in DHCP client through the VPN tunnel after connecting. Disable with `-dhcp=false` if the TAP interface will be configured manually or by an external DHCP client.
|
||||
|
||||
### `-accept-default-gateway`
|
||||
|
||||
Adds a default route via the DHCP-provided gateway on the TAP interface with metric 50. Before doing this, the client adds a `/32` host route to the VPN server via the current default gateway so the tunnel itself is not routed through the VPN.
|
||||
|
|
@ -62,6 +77,19 @@ If a static route entry has destination `0.0.0.0/0` (default route), it is only
|
|||
|
||||
Overwrites `/etc/resolv.conf` with the DNS servers from the DHCP lease. The original file is backed up in memory and restored when the session ends (disconnect, reconnect, or shutdown).
|
||||
|
||||
### `-policy-route-table`
|
||||
|
||||
Enables policy routing for asymmetric return paths. Set to a routing table number (e.g. `200`). When enabled, the client adds:
|
||||
|
||||
```
|
||||
ip rule add from <VPN_IP> table 200
|
||||
ip route replace default via <VPN_GW> dev <TAP> table 200
|
||||
```
|
||||
|
||||
This ensures reply packets from the VPN IP are routed back through the VPN tunnel, not the default route. Needed when the VPN server forwards ports to the client — without it, reply packets leave via the home router and get dropped.
|
||||
|
||||
Cleaned up on disconnect and shutdown.
|
||||
|
||||
## Examples
|
||||
|
||||
Minimal connection:
|
||||
|
|
@ -69,7 +97,7 @@ Minimal connection:
|
|||
softether-go -host vpn.example.com -user admin -pass secret
|
||||
```
|
||||
|
||||
Full setup with routing and DNS:
|
||||
Full setup with routing, DNS, and policy routing:
|
||||
```bash
|
||||
softether-go \
|
||||
-host vpn.example.com \
|
||||
|
|
@ -79,20 +107,17 @@ softether-go \
|
|||
-pass secret \
|
||||
-plain-password \
|
||||
-tap vpn0 \
|
||||
-mac 5E:3B:6F:63:A8:3E \
|
||||
-insecure \
|
||||
-accept-default-gateway \
|
||||
-accept-static-routes \
|
||||
-accept-dns
|
||||
-accept-dns \
|
||||
-policy-route-table 200
|
||||
```
|
||||
|
||||
Named TAP interface with custom reconnect delay:
|
||||
No DHCP (manual configuration):
|
||||
```bash
|
||||
softether-go \
|
||||
-host vpn.example.com \
|
||||
-user admin \
|
||||
-pass secret \
|
||||
-tap myvpn \
|
||||
-reconnect-delay 10s
|
||||
softether-go -host vpn.example.com -user admin -pass secret -dhcp=false -tap vpn0
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
|
@ -114,5 +139,5 @@ The container needs `iproute2` installed (`apk add iproute2` on Alpine) for the
|
|||
|
||||
## Signals
|
||||
|
||||
- **SIGINT / SIGTERM** — clean shutdown: closes tunnel, flushes TAP addresses, restores DNS, removes server host route
|
||||
- **SIGINT / SIGTERM** — clean shutdown: closes tunnel, flushes TAP addresses, restores DNS, removes server host route, cleans up policy routes
|
||||
- During reconnect delay, a signal triggers immediate shutdown instead of waiting
|
||||
|
|
|
|||
|
|
@ -98,6 +98,62 @@ func (t *Tunnel) WriteFrames(frames [][]byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// FrameHandler is called for each Ethernet frame received from the server.
|
||||
// Returning a non-nil error stops the bridge.
|
||||
type FrameHandler func(frame []byte) error
|
||||
|
||||
// Bridge runs bidirectional frame forwarding between the tunnel and a TAP device.
|
||||
// tapRead reads one Ethernet frame into buf, returning (n, err).
|
||||
// tapWrite writes one Ethernet frame.
|
||||
// onFrame is called for each server frame before writing to TAP (e.g. for DHCP).
|
||||
// Blocks until an error occurs on either direction.
|
||||
func (t *Tunnel) Bridge(tapRead func(buf []byte) (int, error), tapWrite func(buf []byte) (int, error), onFrame FrameHandler) error {
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
// Server → TAP
|
||||
go func() {
|
||||
for {
|
||||
frames, err := t.ReadFrames()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("read from server: %w", err)
|
||||
return
|
||||
}
|
||||
for _, frame := range frames {
|
||||
if onFrame != nil {
|
||||
if err := onFrame(frame); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := tapWrite(frame); err != nil {
|
||||
errCh <- fmt.Errorf("write to tap: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// TAP → Server
|
||||
go func() {
|
||||
buf := make([]byte, 1600)
|
||||
for {
|
||||
n, err := tapRead(buf)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("read from tap: %w", err)
|
||||
return
|
||||
}
|
||||
frame := make([]byte, n)
|
||||
copy(frame, buf[:n])
|
||||
if err := t.WriteFrames([][]byte{frame}); err != nil {
|
||||
errCh <- fmt.Errorf("write to server: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
// StartKeepalive sends periodic keepalive packets to prevent the server from
|
||||
// timing out the connection. Must be called after the session enters tunnel mode.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1779
|
||||
|
|
|
|||
216
pkg/netcfg/netcfg.go
Normal file
216
pkg/netcfg/netcfg.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Package netcfg manages network configuration for the VPN tunnel:
|
||||
// TAP interface addressing, routing (default, static, policy), DNS, and
|
||||
// server host routes.
|
||||
package netcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"git.sagar.ch/sagar/softether-go/pkg/dhcp"
|
||||
)
|
||||
|
||||
// Options controls which DHCP-provided network settings to apply.
|
||||
type Options struct {
|
||||
DHCP bool
|
||||
AcceptDefaultGW bool
|
||||
AcceptStaticRoutes bool
|
||||
AcceptDNS bool
|
||||
PolicyRouteTable int
|
||||
}
|
||||
|
||||
// ConfigureTAP sets the IP address, routes, and DNS on a TAP interface from a DHCP lease.
|
||||
// Returns a cleanup function that undoes the changes.
|
||||
func ConfigureTAP(ifname string, lease *dhcp.Lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS bool) (func(), error) {
|
||||
noop := func() {}
|
||||
ones, _ := lease.SubnetMask.Size()
|
||||
addr := fmt.Sprintf("%s/%d", lease.ClientIP, ones)
|
||||
|
||||
if err := run("ip", "addr", "add", addr, "dev", ifname); err != nil {
|
||||
return noop, fmt.Errorf("ip addr add: %w", err)
|
||||
}
|
||||
|
||||
if acceptStaticRoutes && len(lease.Routes) > 0 {
|
||||
for _, r := range lease.Routes {
|
||||
dest := r.Dest.String()
|
||||
if dest == "0.0.0.0/0" {
|
||||
if acceptDefaultGW {
|
||||
if err := run("ip", "route", "add", "default", "via", r.Gateway.String(), "dev", ifname, "metric", "50"); err != nil {
|
||||
log.Printf("warning: static default route: %v", err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := run("ip", "route", "add", dest, "via", r.Gateway.String(), "dev", ifname); err != nil {
|
||||
log.Printf("warning: static route %s via %s: %v", dest, r.Gateway, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if acceptDefaultGW && lease.Gateway != nil && !hasDefaultRoute(lease.Routes) {
|
||||
if err := run("ip", "route", "add", "default", "via", lease.Gateway.String(), "dev", ifname, "metric", "50"); err != nil {
|
||||
log.Printf("warning: default route: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var savedResolv []byte
|
||||
if acceptDNS && len(lease.DNS) > 0 {
|
||||
savedResolv = backupResolv()
|
||||
writeResolv(lease.DNS)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
run("ip", "addr", "flush", "dev", ifname)
|
||||
if savedResolv != nil {
|
||||
restoreResolv(savedResolv)
|
||||
}
|
||||
}
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
// ConfigurePolicyRoute sets up policy routing so packets from the VPN IP are routed
|
||||
// back through the VPN gateway. Needed when the VPN server forwards ports to the
|
||||
// client — without it, reply packets use the default route instead of the VPN tunnel.
|
||||
func ConfigurePolicyRoute(ifname string, lease *dhcp.Lease, table int) func() {
|
||||
t := fmt.Sprintf("%d", table)
|
||||
clientIP := lease.ClientIP.String()
|
||||
gw := lease.Gateway.String()
|
||||
|
||||
run("ip", "rule", "del", "table", t)
|
||||
run("ip", "route", "replace", "default", "via", gw, "dev", ifname, "table", t)
|
||||
if err := run("ip", "rule", "add", "from", clientIP, "table", t); err != nil {
|
||||
log.Printf("warning: policy rule: %v", err)
|
||||
} else {
|
||||
log.Printf("policy route: from %s via %s dev %s table %s", clientIP, gw, ifname, t)
|
||||
}
|
||||
|
||||
return func() {
|
||||
run("ip", "rule", "del", "table", t)
|
||||
run("ip", "route", "del", "default", "table", t)
|
||||
log.Printf("policy route: cleaned up table %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
// AddServerRoute adds a /32 host route to the VPN server via the current default gateway.
|
||||
// This prevents routing loops when the VPN's default route is installed.
|
||||
// Returns a cleanup function that removes the route.
|
||||
func AddServerRoute(serverIP net.IP) func() {
|
||||
noop := func() {}
|
||||
if serverIP == nil {
|
||||
return noop
|
||||
}
|
||||
|
||||
gw, dev := getDefaultGateway()
|
||||
if gw == nil {
|
||||
log.Println("warning: no default gateway found, skipping server route")
|
||||
return noop
|
||||
}
|
||||
|
||||
route := serverIP.String() + "/32"
|
||||
args := []string{"route", "add", route, "via", gw.String()}
|
||||
if dev != "" {
|
||||
args = append(args, "dev", dev)
|
||||
}
|
||||
|
||||
if err := run("ip", args...); err != nil {
|
||||
log.Printf("warning: add server route: %v", err)
|
||||
return noop
|
||||
}
|
||||
log.Printf("added route: %s via %s", route, gw)
|
||||
|
||||
return func() {
|
||||
run("ip", "route", "del", route)
|
||||
log.Printf("removed route: %s", route)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveHost resolves a hostname to an IPv4 address. If it's already an IP, returns it.
|
||||
func ResolveHost(host string) net.IP {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip
|
||||
}
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
log.Printf("warning: could not resolve %s: %v", host, err)
|
||||
return nil
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.To4()
|
||||
}
|
||||
}
|
||||
log.Printf("warning: no IPv4 address for %s", host)
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasDefaultRoute(routes []dhcp.Route) bool {
|
||||
for _, r := range routes {
|
||||
ones, bits := r.Dest.Mask.Size()
|
||||
if ones == 0 && bits == 32 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const resolvPath = "/etc/resolv.conf"
|
||||
|
||||
func backupResolv() []byte {
|
||||
data, err := os.ReadFile(resolvPath)
|
||||
if err != nil {
|
||||
log.Printf("warning: backup resolv.conf: %v", err)
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func writeResolv(servers []net.IP) {
|
||||
var buf strings.Builder
|
||||
buf.WriteString("# Generated by softether-go\n")
|
||||
for _, ip := range servers {
|
||||
fmt.Fprintf(&buf, "nameserver %s\n", ip)
|
||||
}
|
||||
if err := os.WriteFile(resolvPath, []byte(buf.String()), 0644); err != nil {
|
||||
log.Printf("warning: write resolv.conf: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("dns: set nameservers %v", servers)
|
||||
}
|
||||
|
||||
func restoreResolv(saved []byte) {
|
||||
if err := os.WriteFile(resolvPath, saved, 0644); err != nil {
|
||||
log.Printf("warning: restore resolv.conf: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("dns: restored resolv.conf")
|
||||
}
|
||||
|
||||
func getDefaultGateway() (net.IP, string) {
|
||||
out, err := exec.Command("ip", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return nil, ""
|
||||
}
|
||||
fields := strings.Fields(string(out))
|
||||
var gw net.IP
|
||||
var dev string
|
||||
for i, f := range fields {
|
||||
if f == "via" && i+1 < len(fields) {
|
||||
gw = net.ParseIP(fields[i+1])
|
||||
}
|
||||
if f == "dev" && i+1 < len(fields) {
|
||||
dev = fields[i+1]
|
||||
}
|
||||
}
|
||||
return gw, dev
|
||||
}
|
||||
|
||||
func run(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
@ -106,6 +106,27 @@ func (d *Device) MAC() (net.HardwareAddr, error) {
|
|||
return mac, nil
|
||||
}
|
||||
|
||||
// SetMAC sets the hardware (MAC) address of the TAP interface.
|
||||
func (d *Device) SetMAC(mac net.HardwareAddr) error {
|
||||
sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("socket: %w", err)
|
||||
}
|
||||
defer unix.Close(sock)
|
||||
|
||||
var ifr [40]byte
|
||||
copy(ifr[:ifnameSize], d.Name)
|
||||
ifr[ifnameSize] = 1 // sa_family = ARPHRD_ETHER
|
||||
ifr[ifnameSize+1] = 0
|
||||
copy(ifr[ifnameSize+2:ifnameSize+8], mac)
|
||||
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCSIFHWADDR, uintptr(unsafe.Pointer(&ifr[0])))
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("SIOCSIFHWADDR: %w", errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUp brings the TAP interface up (equivalent to `ip link set <name> up`).
|
||||
func (d *Device) SetUp() error {
|
||||
sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue