Skip to content

Commit 3dfcf7b

Browse files
author
0x7fffff92
committed
for larepass
1 parent 2de4d31 commit 3dfcf7b

20 files changed

Lines changed: 438 additions & 38 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Windows amd64: tailscaled.exe + tailscale-ffi.dll → Release assets on publish, or artifact on manual run.
2+
name: Windows release binaries
3+
4+
on:
5+
workflow_dispatch:
6+
release:
7+
types: [published]
8+
9+
jobs:
10+
build-and-upload:
11+
runs-on: windows-2022
12+
13+
env:
14+
CGO_ENABLED: "1"
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-go@v5
19+
with:
20+
go-version-file: go.mod
21+
cache: true
22+
23+
- name: Build tailscaled.exe
24+
run: go build -trimpath -ldflags="-s -w" -o tailscaled.exe ./cmd/tailscaled
25+
26+
- name: Build tailscale-ffi.dll
27+
run: go build -trimpath -buildmode=c-shared -ldflags="-s -w" -o tailscale-ffi.dll ./cmd/tailscale-ffi
28+
29+
- if: github.event_name == 'release'
30+
env:
31+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
run: gh release upload "${{ github.event.release.tag_name }}" tailscaled.exe tailscale-ffi.dll --clobber
33+
34+
- if: github.event_name == 'workflow_dispatch'
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: windows-amd64-${{ github.run_id }}
38+
path: |
39+
tailscaled.exe
40+
tailscale-ffi.dll

cmd/tailscale-ffi/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# tailscale-ffi
2+
3+
Windows C-shared library (FFI) for Tailscale/LarePass, used by other languages or native apps to call into the client.
4+
5+
## Build (Windows, amd64, cgo)
6+
7+
```batch
8+
set GOOS=windows
9+
set GOARCH=amd64
10+
set CGO_ENABLED=1
11+
go build -v -buildmode=c-shared -o tailscale-ffi.dll ./cmd/tailscale-ffi
12+
```
13+
14+
This produces `tailscale-ffi.dll` and `tailscale-ffi.h`. The package is Windows + CGO only; building elsewhere (or with `CGO_ENABLED=0`) fails on purpose.
15+
16+
## Exported C API
17+
18+
| Symbol | Purpose |
19+
|--------|---------|
20+
| `RunWithArgs(argstr *char)` | Run CLI with space-separated args; returns empty string on success. |
21+
| `WatchIPN(argstr *char, initial bool, callback Callback)` | Subscribe to IPN notifications; callback receives JSON. |
22+
| `SetCookie(cookiestr *char)` | Set dev store key `Cookie` (for control auth). |
23+
| `GetPrefs()` | Return current prefs as JSON string. |
24+
| `GetStatus()` | Return current status as JSON string. |
25+
| `GetNetcheck()` | Run netcheck and return report as JSON. |
26+
| `TailscalePing(ipStr *char, timeout int)` | Ping a Tailscale IP; timeout in seconds; returns JSON result. |
27+
28+
The client uses `paths.DefaultTailscaledSocket()` so it talks to the same daemon (LarePass pipe) as the rest of the build.

cmd/tailscale-ffi/tailscale-ffi.go

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
//go:build windows && cgo
2+
3+
/*
4+
set GOOS=windows
5+
set GOARCH=amd64
6+
set CGO_ENABLED=1
7+
go build -v -buildmode=c-shared -ldflags="-s -w" -o tailscale-ffi.dll ./cmd/tailscale-ffi
8+
*/
9+
10+
package main
11+
12+
import (
13+
"context"
14+
"encoding/json"
15+
"errors"
16+
"fmt"
17+
"io"
18+
"log"
19+
"net/http"
20+
"net/netip"
21+
"os"
22+
"strings"
23+
"time"
24+
"unsafe"
25+
26+
"tailscale.com/client/local"
27+
cli "tailscale.com/cmd/tailscale/cli"
28+
"tailscale.com/envknob"
29+
"tailscale.com/ipn"
30+
"tailscale.com/net/netcheck"
31+
"tailscale.com/net/netmon"
32+
"tailscale.com/net/portmapper"
33+
"tailscale.com/paths"
34+
"tailscale.com/tailcfg"
35+
"tailscale.com/types/logger"
36+
"tailscale.com/util/eventbus"
37+
)
38+
39+
/*
40+
#include <stdio.h>
41+
42+
// Inline C stubs for function pointers
43+
typedef void (*Callback)();
44+
static inline void call_out(Callback ptr, void *data) {
45+
(ptr)(data);
46+
}
47+
*/
48+
import "C"
49+
50+
func main() {
51+
// fmt.Println(RunWithArgs(C.CString("aaaa"), C.CString("bbbb")))
52+
}
53+
54+
//export RunWithArgs
55+
func RunWithArgs(argstr *C.char) string {
56+
arg_str := C.GoString(argstr)
57+
fmt.Printf("args: %v\n", arg_str)
58+
args := strings.Split(arg_str, " ")
59+
fmt.Println(args)
60+
if err := cli.Run(args); err != nil {
61+
log.Printf("cli.Run error: %+v\n", err)
62+
// os.Exit(1)
63+
return "some error"
64+
}
65+
66+
return ""
67+
}
68+
69+
var localClient local.Client
70+
71+
func init() {
72+
localClient.Socket = paths.DefaultTailscaledSocket()
73+
}
74+
75+
//export WatchIPN
76+
func WatchIPN(argstr *C.char, initial bool, callback C.Callback) *C.char {
77+
go func() {
78+
var watchIPNArgs struct {
79+
netmap bool
80+
initial bool
81+
showPrivateKey bool
82+
}
83+
watchIPNArgs.netmap = true
84+
watchIPNArgs.initial = initial
85+
watchIPNArgs.showPrivateKey = false
86+
87+
ctx := context.Background()
88+
89+
var mask ipn.NotifyWatchOpt
90+
if watchIPNArgs.initial {
91+
mask = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
92+
}
93+
if !watchIPNArgs.showPrivateKey {
94+
mask |= ipn.NotifyNoPrivateKeys
95+
}
96+
watcher, err := localClient.WatchIPNBus(ctx, mask)
97+
if err != nil {
98+
log.Println("WatchIPNBus", err)
99+
return
100+
}
101+
defer watcher.Close()
102+
log.Printf("Connected.\n")
103+
for {
104+
n, err := watcher.Next()
105+
if err != nil {
106+
log.Println("watcher.Next() -> ", err)
107+
j, _ := json.MarshalIndent(n, "", "\t")
108+
//printf("%s\n", j)
109+
110+
C.call_out(callback, unsafe.Pointer(C.CString(string(j))))
111+
return
112+
}
113+
if !watchIPNArgs.netmap {
114+
n.NetMap = nil
115+
}
116+
j, _ := json.MarshalIndent(n, "", "\t")
117+
//printf("%s\n", j)
118+
119+
C.call_out(callback, unsafe.Pointer(C.CString(string(j))))
120+
if initial {
121+
break
122+
}
123+
}
124+
}()
125+
return C.CString("") //C.CString(string(j))
126+
}
127+
128+
//export SetCookie
129+
func SetCookie(cookiestr *C.char) bool {
130+
cookie := C.GoString(cookiestr)
131+
fmt.Printf("args: %v\n", cookie)
132+
ctx := context.Background()
133+
err := localClient.SetDevStoreKeyValue(ctx, "Cookie", cookie)
134+
if err != nil {
135+
log.Println("SetDevStoreKeyValue", err)
136+
return false
137+
}
138+
139+
log.Println("set cookie successful")
140+
return true
141+
}
142+
143+
//export GetPrefs
144+
func GetPrefs() *C.char {
145+
ctx := context.Background()
146+
prefs, err := localClient.GetPrefs(ctx)
147+
if err != nil {
148+
return C.CString("{}")
149+
}
150+
151+
j, _ := json.MarshalIndent(prefs, "", "\t")
152+
log.Println(string(j))
153+
154+
return C.CString(string(j))
155+
}
156+
157+
//export GetStatus
158+
func GetStatus() *C.char {
159+
ctx := context.Background()
160+
st, err := localClient.Status(ctx)
161+
if err != nil {
162+
return C.CString("{}")
163+
}
164+
165+
j, _ := json.MarshalIndent(st, "", " ")
166+
log.Println(string(j))
167+
168+
return C.CString(string(j))
169+
}
170+
171+
var netcheckArgs struct {
172+
format string
173+
every time.Duration
174+
verbose bool
175+
}
176+
177+
//export GetNetcheck
178+
func GetNetcheck() *C.char {
179+
netcheckArgs.format = "json"
180+
netcheckArgs.every = 0
181+
netcheckArgs.verbose = false
182+
183+
ctx := context.Background()
184+
logf := logger.WithPrefix(log.Printf, "portmap: ")
185+
bus := eventbus.New()
186+
netMon, err := netmon.New(bus, logf)
187+
if err != nil {
188+
//return err
189+
return C.CString("{}")
190+
}
191+
// New API: portmapper requires Config with EventBus (replaces old NewClient(logger.Discard, netMon, nil, nil))
192+
pm := portmapper.NewClient(portmapper.Config{
193+
EventBus: bus,
194+
Logf: logger.Discard,
195+
NetMon: netMon,
196+
})
197+
pm.SetGatewayLookupFunc(netMon.GatewayAndSelfIP)
198+
199+
c := &netcheck.Client{
200+
PortMapper: pm,
201+
UseDNSCache: false, // always resolve, don't cache
202+
}
203+
if netcheckArgs.verbose {
204+
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
205+
c.Verbose = true
206+
} else {
207+
c.Logf = logger.Discard
208+
}
209+
210+
if strings.HasPrefix(netcheckArgs.format, "json") {
211+
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
212+
}
213+
214+
if err := c.Standalone(ctx, envknob.String("TS_DEBUG_NETCHECK_UDP_BIND")); err != nil {
215+
fmt.Fprintln(os.Stderr, "netcheck: UDP test failure:", err)
216+
}
217+
218+
dm, err := localClient.CurrentDERPMap(ctx)
219+
noRegions := dm != nil && len(dm.Regions) == 0
220+
if noRegions {
221+
log.Printf("No DERP map from tailscaled; using default.")
222+
}
223+
if err != nil || noRegions {
224+
dm, err = prodDERPMap(ctx, http.DefaultClient)
225+
if err != nil {
226+
//return err
227+
return C.CString("{}")
228+
}
229+
}
230+
231+
t0 := time.Now()
232+
report, err := c.GetReport(ctx, dm, nil) // new API: third arg opts *GetReportOpts
233+
d := time.Since(t0)
234+
if netcheckArgs.verbose {
235+
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)
236+
}
237+
if err != nil {
238+
//return fmt.Errorf("netcheck: %w", err)
239+
return C.CString("{}")
240+
}
241+
j, _ := json.MarshalIndent(report, "", "\t")
242+
log.Println(string(j))
243+
244+
return C.CString(string(j))
245+
}
246+
247+
func portMapping(r *netcheck.Report) string {
248+
if !r.AnyPortMappingChecked() {
249+
return "not checked"
250+
}
251+
var got []string
252+
if r.UPnP.EqualBool(true) {
253+
got = append(got, "UPnP")
254+
}
255+
if r.PMP.EqualBool(true) {
256+
got = append(got, "NAT-PMP")
257+
}
258+
if r.PCP.EqualBool(true) {
259+
got = append(got, "PCP")
260+
}
261+
return strings.Join(got, ", ")
262+
}
263+
264+
func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, error) {
265+
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
266+
if err != nil {
267+
return nil, fmt.Errorf("create prodDERPMap request: %w", err)
268+
}
269+
res, err := httpc.Do(req)
270+
if err != nil {
271+
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
272+
}
273+
defer res.Body.Close()
274+
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
275+
if err != nil {
276+
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
277+
}
278+
if res.StatusCode != 200 {
279+
return nil, fmt.Errorf("fetch prodDERPMap: %v: %s", res.Status, b)
280+
}
281+
var derpMap tailcfg.DERPMap
282+
if err = json.Unmarshal(b, &derpMap); err != nil {
283+
return nil, fmt.Errorf("fetch prodDERPMap: %w", err)
284+
}
285+
return &derpMap, nil
286+
}
287+
288+
//export TailscalePing
289+
func TailscalePing(ipStr *C.char, timeout int) *C.char {
290+
ip := C.GoString(ipStr)
291+
fmt.Printf("args: %v\n", ip)
292+
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
293+
st, err := localClient.Ping(ctx, netip.MustParseAddr(ip), tailcfg.PingDisco)
294+
cancel()
295+
if err != nil {
296+
if errors.Is(err, context.DeadlineExceeded) {
297+
fmt.Printf("ping %q time out\n", ip)
298+
}
299+
return C.CString("{}")
300+
}
301+
302+
j, _ := json.MarshalIndent(st, "", " ")
303+
log.Println(string(j))
304+
305+
return C.CString(string(j))
306+
}

cmd/tailscaled/tailscaled.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func defaultTunName() string {
7272
case "openbsd":
7373
return "tun"
7474
case "windows":
75-
return "Tailscale"
75+
return "LarePass"
7676
case "darwin":
7777
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
7878
// as a magic value that uses/creates any free number.
@@ -373,7 +373,7 @@ func ipnServerOpts() (o serverOptions) {
373373
// If an absolute --state is provided but not --statedir, try to derive
374374
// a state directory.
375375
if o.VarRoot == "" && filepath.IsAbs(args.statepath) {
376-
if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "tailscale") {
376+
if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "larepass") {
377377
o.VarRoot = dir
378378
}
379379
}

0 commit comments

Comments
 (0)